mirror of
https://github.com/redis/redis.git
synced 2026-06-08 16:24:26 -04:00
Merge branch 'unstable' into two-level-pel
This commit is contained in:
commit
4940ccfbf0
70 changed files with 2130 additions and 4474 deletions
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: make
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install libc6-dev-i386 gcc-multilib g++-multilib
|
||||
sudo apt-get update && sudo apt-get install libc6-dev-i386 gcc-multilib
|
||||
make REDIS_CFLAGS='-Werror' 32bit
|
||||
|
||||
build-libc-malloc:
|
||||
|
|
@ -79,7 +79,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: make
|
||||
run: |
|
||||
dnf -y install which gcc gcc-c++ make
|
||||
dnf -y install which gcc make
|
||||
make REDIS_CFLAGS='-Werror'
|
||||
|
||||
build-old-chain-jemalloc:
|
||||
|
|
@ -96,7 +96,6 @@ jobs:
|
|||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
||||
apt-get update
|
||||
apt-get install -y make gcc-4.8 g++-4.8
|
||||
apt-get install -y make gcc-4.8
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
|
||||
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
|
||||
make CC=gcc REDIS_CFLAGS='-Werror'
|
||||
|
|
|
|||
2
.github/workflows/codecov.yml
vendored
2
.github/workflows/codecov.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
make lcov
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./src/redis.info
|
||||
|
|
|
|||
11
.github/workflows/coverity.yml
vendored
11
.github/workflows/coverity.yml
vendored
|
|
@ -14,9 +14,11 @@ jobs:
|
|||
- uses: actions/checkout@main
|
||||
- name: Download and extract the Coverity Build Tool
|
||||
run: |
|
||||
wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=redis-unstable" -O cov-analysis-linux64.tar.gz
|
||||
wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=${COVERITY_SCAN_TOKEN}&project=redis-unstable" -O cov-analysis-linux64.tar.gz
|
||||
mkdir cov-analysis-linux64
|
||||
tar xzf cov-analysis-linux64.tar.gz --strip 1 -C cov-analysis-linux64
|
||||
env:
|
||||
COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
|
||||
- name: Install Redis dependencies
|
||||
run: sudo apt install -y gcc tcl8.6 tclx procps libssl-dev
|
||||
- name: Build with cov-build
|
||||
|
|
@ -26,7 +28,10 @@ jobs:
|
|||
tar czvf cov-int.tgz cov-int
|
||||
curl \
|
||||
--form project=redis-unstable \
|
||||
--form email=${{ secrets.COVERITY_SCAN_EMAIL }} \
|
||||
--form token=${{ secrets.COVERITY_SCAN_TOKEN }} \
|
||||
--form email="${COVERITY_SCAN_EMAIL}" \
|
||||
--form token="${COVERITY_SCAN_TOKEN}" \
|
||||
--form file=@cov-int.tgz \
|
||||
https://scan.coverity.com/builds
|
||||
env:
|
||||
COVERITY_SCAN_EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }}
|
||||
COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
|
||||
|
|
|
|||
32
.github/workflows/daily.yml
vendored
32
.github/workflows/daily.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
|||
repository: ${{ env.GITHUB_REPOSITORY }}
|
||||
ref: ${{ env.GITHUB_HEAD_REF }}
|
||||
- name: make
|
||||
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST'
|
||||
run: make REDIS_CFLAGS='-Werror -DREDIS_TEST -DDEBUG_ASSERTIONS'
|
||||
- name: testprep
|
||||
run: sudo apt-get install tcl8.6 tclx
|
||||
- name: test
|
||||
|
|
@ -240,7 +240,7 @@ jobs:
|
|||
ref: ${{ env.GITHUB_HEAD_REF }}
|
||||
- name: make
|
||||
run: |
|
||||
apt-get update && apt-get install -y make gcc g++
|
||||
apt-get update && apt-get install -y make gcc
|
||||
make CC=gcc REDIS_CFLAGS='-Werror -DREDIS_TEST -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3'
|
||||
- name: testprep
|
||||
run: sudo apt-get install -y tcl8.6 tclx procps
|
||||
|
|
@ -347,7 +347,7 @@ jobs:
|
|||
ref: ${{ env.GITHUB_HEAD_REF }}
|
||||
- name: make
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install libc6-dev-i386 g++ gcc-multilib g++-multilib
|
||||
sudo apt-get update && sudo apt-get install libc6-dev-i386 gcc-multilib
|
||||
make 32bit REDIS_CFLAGS='-Werror -DREDIS_TEST'
|
||||
make -C tests/modules 32bit # the script below doesn't have an argument, we must build manually ahead of time
|
||||
- name: testprep
|
||||
|
|
@ -580,7 +580,7 @@ jobs:
|
|||
- name: testprep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install tcl8.6 tclx valgrind g++ -y
|
||||
sudo apt-get install tcl8.6 tclx valgrind -y
|
||||
- name: test
|
||||
if: true && !contains(github.event.inputs.skiptests, 'redis')
|
||||
# Note that valgrind's overhead doesn't pair well with io-threads so we
|
||||
|
|
@ -645,7 +645,7 @@ jobs:
|
|||
- name: testprep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install tcl8.6 tclx valgrind g++ -y
|
||||
sudo apt-get install tcl8.6 tclx valgrind -y
|
||||
- name: test
|
||||
if: true && !contains(github.event.inputs.skiptests, 'redis')
|
||||
run: ./runtest --valgrind --tags -iothreads --no-latency --verbose --clients 1 --timeout 2400 --dump-logs ${{github.event.inputs.test_args}}
|
||||
|
|
@ -878,7 +878,7 @@ jobs:
|
|||
ref: ${{ env.GITHUB_HEAD_REF }}
|
||||
- name: make
|
||||
run: |
|
||||
dnf -y install which gcc make g++
|
||||
dnf -y install which gcc make
|
||||
make REDIS_CFLAGS='-Werror'
|
||||
- name: testprep
|
||||
run: |
|
||||
|
|
@ -917,7 +917,7 @@ jobs:
|
|||
ref: ${{ env.GITHUB_HEAD_REF }}
|
||||
- name: make
|
||||
run: |
|
||||
dnf -y install which gcc make openssl-devel openssl g++
|
||||
dnf -y install which gcc make openssl-devel openssl
|
||||
make BUILD_TLS=module REDIS_CFLAGS='-Werror'
|
||||
- name: testprep
|
||||
run: |
|
||||
|
|
@ -960,7 +960,7 @@ jobs:
|
|||
ref: ${{ env.GITHUB_HEAD_REF }}
|
||||
- name: make
|
||||
run: |
|
||||
dnf -y install which gcc make openssl-devel openssl g++
|
||||
dnf -y install which gcc make openssl-devel openssl
|
||||
make BUILD_TLS=module REDIS_CFLAGS='-Werror'
|
||||
- name: testprep
|
||||
run: |
|
||||
|
|
@ -1093,9 +1093,6 @@ jobs:
|
|||
(github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) &&
|
||||
!contains(github.event.inputs.skipjobs, 'freebsd')
|
||||
timeout-minutes: 360
|
||||
env:
|
||||
CC: clang
|
||||
CXX: clang++
|
||||
steps:
|
||||
- name: prep
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
|
|
@ -1224,7 +1221,7 @@ jobs:
|
|||
if: true && !contains(github.event.inputs.skiptests, 'cluster')
|
||||
run: ./runtest-cluster --log-req-res --dont-clean --force-resp3 ${{github.event.inputs.cluster_test_args}}
|
||||
- name: Install Python dependencies
|
||||
uses: py-actions/py-dependency-install@v4
|
||||
uses: py-actions/py-dependency-install@30aa0023464ed4b5b116bd9fbdab87acf01a484e # v4.1.0
|
||||
with:
|
||||
path: "./utils/req-res-validator/requirements.txt"
|
||||
- name: validator
|
||||
|
|
@ -1260,9 +1257,8 @@ jobs:
|
|||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
||||
apt-get update
|
||||
apt-get install -y make gcc-4.8 g++-4.8
|
||||
apt-get install -y make gcc-4.8
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
|
||||
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
|
||||
make CC=gcc REDIS_CFLAGS='-Werror'
|
||||
- name: testprep
|
||||
run: apt-get install -y tcl tcltls tclx
|
||||
|
|
@ -1306,10 +1302,9 @@ jobs:
|
|||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
||||
apt-get update
|
||||
apt-get install -y make gcc-4.8 g++-4.8 openssl libssl-dev
|
||||
apt-get install -y make gcc-4.8 openssl libssl-dev
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
|
||||
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
|
||||
make CC=gcc CXX=g++ BUILD_TLS=module REDIS_CFLAGS='-Werror'
|
||||
make CC=gcc BUILD_TLS=module REDIS_CFLAGS='-Werror'
|
||||
- name: testprep
|
||||
run: |
|
||||
apt-get install -y tcl tcltls tclx
|
||||
|
|
@ -1357,9 +1352,8 @@ jobs:
|
|||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
|
||||
apt-get update
|
||||
apt-get install -y make gcc-4.8 g++-4.8 openssl libssl-dev
|
||||
apt-get install -y make gcc-4.8 openssl libssl-dev
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
|
||||
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 100
|
||||
make BUILD_TLS=module CC=gcc REDIS_CFLAGS='-Werror'
|
||||
- name: testprep
|
||||
run: |
|
||||
|
|
|
|||
244
.github/workflows/post-release-automation.yml
vendored
244
.github/workflows/post-release-automation.yml
vendored
|
|
@ -5,17 +5,15 @@ on:
|
|||
types: [published]
|
||||
|
||||
jobs:
|
||||
automate-release-scripts:
|
||||
# Only run for the main redis/redis repository (not forks)
|
||||
# Note: Only users with write access can publish releases, providing implicit authorization
|
||||
extract-release-info:
|
||||
if: github.repository == 'redis/redis'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
tag_name: ${{ steps.release-info.outputs.tag_name }}
|
||||
release_type: ${{ steps.release-info.outputs.release_type }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for git archive
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Extract and validate release information
|
||||
id: release-info
|
||||
|
|
@ -23,15 +21,12 @@ jobs:
|
|||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Extract tag name from the release event (via env var to prevent injection)
|
||||
echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "Release tag: ${TAG_NAME}"
|
||||
|
||||
# Get the latest release tag
|
||||
LATEST_TAG=$(gh release view --json tagName --jq '.tagName')
|
||||
echo "Latest release tag from gh cli: ${LATEST_TAG}"
|
||||
echo "Latest release tag(from gh release): ${LATEST_TAG}"
|
||||
|
||||
# Determine release type by comparing with latest release
|
||||
if [[ "${TAG_NAME}" == "${LATEST_TAG}" ]]; then
|
||||
echo "release_type=latest" >> $GITHUB_OUTPUT
|
||||
echo "Detected latest release: ${TAG_NAME}"
|
||||
|
|
@ -40,116 +35,131 @@ jobs:
|
|||
echo "Detected non-latest release: ${TAG_NAME} (latest is ${LATEST_TAG})"
|
||||
fi
|
||||
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "RELEASE_TAG=${{ steps.release-info.outputs.tag_name }}" >> $GITHUB_ENV
|
||||
echo "RELEASE_TYPE=${{ steps.release-info.outputs.release_type }}" >> $GITHUB_ENV
|
||||
echo "Environment variables set:"
|
||||
echo " RELEASE_TAG: ${{ steps.release-info.outputs.tag_name }}"
|
||||
echo " RELEASE_TYPE: ${{ steps.release-info.outputs.release_type }}"
|
||||
|
||||
create-tarball:
|
||||
needs: extract-release-info
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG_NAME: ${{ needs.extract-release-info.outputs.tag_name }}
|
||||
outputs:
|
||||
sha256: ${{ steps.checksum.outputs.sha256 }}
|
||||
size_mb: ${{ steps.size.outputs.size_mb }}
|
||||
size_warning: ${{ steps.size.outputs.size_warning }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ env.TAG_NAME }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create tarball
|
||||
id: create-tarball
|
||||
run: ./utils/releasetools/01_create_tarball.sh "$TAG_NAME"
|
||||
|
||||
- name: Verify tarball size
|
||||
id: size
|
||||
run: |
|
||||
echo "Creating tarball for version ${RELEASE_TAG}..."
|
||||
# TODO: Implement tarball creation using utils/releasetools/01_create_tarball.sh
|
||||
# ./utils/releasetools/01_create_tarball.sh ${RELEASE_TAG}
|
||||
|
||||
# Placeholder: Verify tarball was created
|
||||
# TARBALL_PATH="/tmp/redis-${RELEASE_TAG}.tar.gz"
|
||||
# if [ ! -f "${TARBALL_PATH}" ]; then
|
||||
# echo "Error: Tarball not found at ${TARBALL_PATH}"
|
||||
# exit 1
|
||||
# fi
|
||||
# echo "tarball_path=${TARBALL_PATH}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✓ Tarball creation step (placeholder)"
|
||||
|
||||
- name: Upload tarball
|
||||
id: upload-tarball
|
||||
run: |
|
||||
echo "Uploading tarball for version ${RELEASE_TAG}..."
|
||||
# TODO: Implement tarball upload
|
||||
# This will require:
|
||||
# - SSH credentials/keys for upload to download.redis.io
|
||||
# - Adaptation of utils/releasetools/02_upload_tarball.sh for CI environment
|
||||
|
||||
echo "✓ Tarball upload step (placeholder)"
|
||||
|
||||
- name: Test release tarball
|
||||
id: test-release
|
||||
run: |
|
||||
echo "Testing release tarball for version ${RELEASE_TAG}..."
|
||||
# TODO: Implement release testing using utils/releasetools/03_test_release.sh
|
||||
# This will:
|
||||
# - Download the uploaded tarball
|
||||
# - Extract and build Redis
|
||||
|
||||
echo "✓ Release testing step (placeholder)"
|
||||
|
||||
- name: Update release hashes
|
||||
id: update-hashes
|
||||
run: |
|
||||
echo "Updating release hashes for version ${RELEASE_TAG}..."
|
||||
# TODO: Implement hash update using utils/releasetools/04_release_hash.sh
|
||||
# This will require:
|
||||
# - Access to redis-hashes repository
|
||||
# - Git credentials for committing and pushing
|
||||
|
||||
echo "✓ Release hashes update step (placeholder)"
|
||||
|
||||
- name: Approval gate for latest releases
|
||||
if: steps.release-info.outputs.release_type == 'latest'
|
||||
run: |
|
||||
echo "Latest release detected. Manual approval required for production deployment."
|
||||
# TODO: Implement approval workflow
|
||||
# This could use GitHub Environments with required reviewers
|
||||
# or a manual approval step
|
||||
|
||||
echo "✓ Approval gate (placeholder)"
|
||||
|
||||
- name: Update stable symlink (latest releases only)
|
||||
if: steps.release-info.outputs.release_type == 'latest'
|
||||
id: update-stable
|
||||
run: |
|
||||
echo "This is a latest release. Updating stable symlink after approval."
|
||||
# TODO: Implement stable symlink update
|
||||
# This step should only run for latest releases (not non-latest)
|
||||
# It will update the redis-stable symlink on download.redis.io
|
||||
# This is part of the upload script (02_upload_tarball.sh)
|
||||
|
||||
echo "✓ Stable symlink update step (placeholder)"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Post-Release Automation Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Release Tag:** ${{ steps.release-info.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Release Type:** ${{ steps.release-info.outputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Steps Status" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Create tarball: ${{ steps.create-tarball.outcome }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Upload tarball: ${{ steps.upload-tarball.outcome }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Test release: ${{ steps.test-release.outcome }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Update hashes: ${{ steps.update-hashes.outcome }}" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ "${{ steps.release-info.outputs.release_type }}" == "latest" ]]; then
|
||||
echo "- Update stable symlink: ${{ steps.update-stable.outcome }}" >> $GITHUB_STEP_SUMMARY
|
||||
TARBALL="/tmp/redis-${TAG_NAME}.tar.gz"
|
||||
SIZE_MB=$(du -m "$TARBALL" | cut -f1)
|
||||
echo "Tarball size: ${SIZE_MB} MB"
|
||||
echo "size_mb=${SIZE_MB}" >> $GITHUB_OUTPUT
|
||||
if [ "$SIZE_MB" -lt 3 ] || [ "$SIZE_MB" -gt 5 ]; then
|
||||
echo "::warning::Tarball size ${SIZE_MB} MB is outside expected range (3-5 MB)"
|
||||
echo "size_warning=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "size_warning=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Send Slack notification
|
||||
if: always()
|
||||
- name: Calculate SHA256 checksum
|
||||
id: checksum
|
||||
run: |
|
||||
echo "Sending Slack notification for release ${RELEASE_TAG}..."
|
||||
# TODO: Implement Slack notification
|
||||
# This will require:
|
||||
# - Slack webhook URL or bot token (stored in secrets)
|
||||
# - Determine appropriate channel (e.g., #releases, #redis-releases)
|
||||
# - Craft message with release information and workflow status
|
||||
# Example using webhook:
|
||||
# curl -X POST -H 'Content-type: application/json' \
|
||||
# --data '{"channel":"#releases","text":"Release ${RELEASE_TAG} automation completed"}' \
|
||||
# ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
TARBALL="/tmp/redis-${TAG_NAME}.tar.gz"
|
||||
SHA256=$(shasum -a 256 "$TARBALL" | cut -d' ' -f1)
|
||||
echo "SHA256: $SHA256"
|
||||
echo "sha256=$SHA256" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✓ Slack notification step (placeholder)"
|
||||
- name: Upload tarball as artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: redis-${{ env.TAG_NAME }}-tarball
|
||||
path: /tmp/redis-${{ env.TAG_NAME }}.tar.gz
|
||||
compression-level: 0
|
||||
|
||||
# approval-gate:
|
||||
# needs: [extract-release-info, create-tarball]
|
||||
# if: needs.extract-release-info.outputs.release_type == 'latest'
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Approval gate
|
||||
# run: |
|
||||
# echo "Latest release detected. Manual approval required for production deployment."
|
||||
# # TODO: Implement approval workflow
|
||||
# # This could use GitHub Environments with required reviewers
|
||||
# # or a manual approval step
|
||||
|
||||
# upload-tarball:
|
||||
# needs: [extract-release-info, create-tarball, approval-gate]
|
||||
# if: always() && !cancelled() && needs.create-tarball.result == 'success' && (needs.approval-gate.result == 'success' || needs.approval-gate.result == 'skipped')
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Upload tarball
|
||||
# run: |
|
||||
# echo "TODO: Implement tarball upload"
|
||||
# # This will require:
|
||||
# # - SSH credentials/keys for upload to download.redis.io
|
||||
# # - Adaptation of utils/releasetools/02_upload_tarball.sh for CI environment
|
||||
|
||||
# test-release-tarball:
|
||||
# needs: upload-tarball
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Test release tarball
|
||||
# run: |
|
||||
# echo "TODO: Implement release testing using utils/releasetools/03_test_release.sh"
|
||||
# # This will:
|
||||
# # - Download the uploaded tarball
|
||||
# # - Extract and build Redis
|
||||
|
||||
# update-release-hashes:
|
||||
# needs: test-release-tarball
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Update release hashes
|
||||
# run: |
|
||||
# echo "TODO: Implement hash update using utils/releasetools/04_release_hash.sh"
|
||||
# # This will require:
|
||||
# # - Access to redis-hashes repository
|
||||
# # - Git credentials for committing and pushing
|
||||
|
||||
summary-and-notify:
|
||||
needs: [extract-release-info, create-tarball] # update-release-hashes
|
||||
if: always() && github.repository == 'redis/redis'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG_NAME: ${{ needs.extract-release-info.outputs.tag_name }}
|
||||
RELEASE_TYPE: ${{ needs.extract-release-info.outputs.release_type }}
|
||||
SHA256: ${{ needs.create-tarball.outputs.sha256 }}
|
||||
SIZE_MB: ${{ needs.create-tarball.outputs.size_mb }}
|
||||
SIZE_WARNING: ${{ needs.create-tarball.outputs.size_warning }}
|
||||
steps:
|
||||
- name: Summary
|
||||
run: |
|
||||
{
|
||||
echo "## Post-Release Automation Summary"
|
||||
echo ""
|
||||
echo "- **Release Tag:** ${TAG_NAME}"
|
||||
echo "- **Release Type:** ${RELEASE_TYPE}"
|
||||
echo "- **Tarball SHA256:** ${SHA256}"
|
||||
echo "- **Tarball Size:** ${SIZE_MB} MB"
|
||||
if [ "${SIZE_WARNING}" == "true" ]; then
|
||||
echo ""
|
||||
echo "> [!WARNING]"
|
||||
echo "> Tarball size is outside expected range, check the logs for details."
|
||||
fi
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# - name: Send Slack notification
|
||||
# run: |
|
||||
# echo "TODO: Implement Slack notification"
|
||||
# # This will require:
|
||||
# # - Slack webhook URL or bot token (stored in secrets)
|
||||
# # - Determine appropriate channel (e.g., #releases, #redis-releases)
|
||||
# # - Craft message with release information and workflow status
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -30,7 +30,6 @@ deps/lua/src/luac
|
|||
deps/lua/src/liblua.a
|
||||
deps/hdr_histogram/libhdrhistogram.a
|
||||
deps/fpconv/libfpconv.a
|
||||
deps/fast_float/libfast_float.a
|
||||
tests/tls/*
|
||||
.make-*
|
||||
.prerequisites
|
||||
|
|
|
|||
7
deps/Makefile
vendored
7
deps/Makefile
vendored
|
|
@ -59,7 +59,6 @@ distclean:
|
|||
-(cd jemalloc && [ -f Makefile ] && $(MAKE) distclean) > /dev/null || true
|
||||
-(cd hdr_histogram && $(MAKE) clean) > /dev/null || true
|
||||
-(cd fpconv && $(MAKE) clean) > /dev/null || true
|
||||
-(cd fast_float && $(MAKE) clean) > /dev/null || true
|
||||
-(cd xxhash && $(MAKE) clean) > /dev/null || true
|
||||
-(rm -f .make-*)
|
||||
|
||||
|
|
@ -95,12 +94,6 @@ fpconv: .make-prerequisites
|
|||
|
||||
.PHONY: fpconv
|
||||
|
||||
fast_float: .make-prerequisites
|
||||
@printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR)
|
||||
cd fast_float && $(MAKE) libfast_float CFLAGS="$(DEPS_CFLAGS)" LDFLAGS="$(DEPS_LDFLAGS)"
|
||||
|
||||
.PHONY: fast_float
|
||||
|
||||
XXHASH_CFLAGS = -fPIC $(DEPS_CFLAGS)
|
||||
xxhash: .make-prerequisites
|
||||
@printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR)
|
||||
|
|
|
|||
27
deps/fast_float/Makefile
vendored
27
deps/fast_float/Makefile
vendored
|
|
@ -1,27 +0,0 @@
|
|||
# Fallback to gcc/g++ when $CC or $CXX is not in $PATH.
|
||||
CC ?= gcc
|
||||
CXX ?= g++
|
||||
|
||||
WARN=-Wall
|
||||
OPT=-O3
|
||||
STD=-std=c++11
|
||||
DEFS=-DFASTFLOAT_ALLOWS_LEADING_PLUS
|
||||
|
||||
FASTFLOAT_CFLAGS=$(WARN) $(OPT) $(STD) $(DEFS) $(CFLAGS)
|
||||
FASTFLOAT_LDFLAGS=$(LDFLAGS)
|
||||
|
||||
libfast_float: fast_float_strtod.o
|
||||
$(AR) -r libfast_float.a fast_float_strtod.o
|
||||
|
||||
32bit: FASTFLOAT_CFLAGS += -m32
|
||||
32bit: FASTFLOAT_LDFLAGS += -m32
|
||||
32bit: libfast_float
|
||||
|
||||
fast_float_strtod.o: fast_float_strtod.cpp
|
||||
$(CXX) $(FASTFLOAT_CFLAGS) -c fast_float_strtod.cpp $(FASTFLOAT_LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f *.o
|
||||
rm -f *.a
|
||||
rm -f *.h.gch
|
||||
rm -rf *.dSYM
|
||||
21
deps/fast_float/README.md
vendored
21
deps/fast_float/README.md
vendored
|
|
@ -1,21 +0,0 @@
|
|||
README for fast_float v6.1.4
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
We're using the fast_float library[1] in our (compiled-in)
|
||||
floating-point fast_float_strtod implementation for faster and more
|
||||
portable parsing of 64 decimal strings.
|
||||
|
||||
The single file fast_float.h is an amalgamation of the entire library,
|
||||
which can be (re)generated with the amalgamate.py script (from the
|
||||
fast_float repository) via the command
|
||||
|
||||
```
|
||||
git clone https://github.com/fastfloat/fast_float
|
||||
cd fast_float
|
||||
git checkout v6.1.4
|
||||
python3 ./script/amalgamate.py --license=MIT \
|
||||
> $REDIS_SRC/deps/fast_float/fast_float.h
|
||||
```
|
||||
|
||||
[1]: https://github.com/fastfloat/fast_float
|
||||
3838
deps/fast_float/fast_float.h
vendored
3838
deps/fast_float/fast_float.h
vendored
File diff suppressed because it is too large
Load diff
32
deps/fast_float/fast_float_strtod.cpp
vendored
32
deps/fast_float/fast_float_strtod.cpp
vendored
|
|
@ -1,32 +0,0 @@
|
|||
#include "fast_float.h"
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <cerrno>
|
||||
|
||||
/* Convert NPTR to a double using the fast_float library.
|
||||
*
|
||||
* This function behaves similarly to the standard strtod function, converting
|
||||
* the initial portion of the string pointed to by `nptr` to a `double` value,
|
||||
* using the fast_float library for high performance. If the conversion fails,
|
||||
* errno is set to EINVAL error code.
|
||||
*
|
||||
* @param nptr A pointer to the null-terminated byte string to be interpreted.
|
||||
* @param endptr A pointer to a pointer to character. If `endptr` is not NULL,
|
||||
* it will point to the character after the last character used
|
||||
* in the conversion.
|
||||
* @return The converted value as a double. If no valid conversion could
|
||||
* be performed, returns 0.0.
|
||||
* If ENDPTR is not NULL, a pointer to the character after the last one used
|
||||
* in the number is put in *ENDPTR. */
|
||||
extern "C" double fast_float_strtod(const char *nptr, char **endptr) {
|
||||
double result = 0.0;
|
||||
auto answer = fast_float::from_chars(nptr, nptr + strlen(nptr), result);
|
||||
if (answer.ec != std::errc()) {
|
||||
errno = EINVAL; // Fallback to for other errors
|
||||
}
|
||||
if (endptr != NULL) {
|
||||
*endptr = (char *)answer.ptr;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
15
deps/fast_float/fast_float_strtod.h
vendored
15
deps/fast_float/fast_float_strtod.h
vendored
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
#ifndef __FAST_FLOAT_STRTOD_H__
|
||||
#define __FAST_FLOAT_STRTOD_H__
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
double fast_float_strtod(const char *in, char **out);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __FAST_FLOAT_STRTOD_H__ */
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
SRC_DIR = src
|
||||
MODULE_VERSION = v8.7.80
|
||||
MODULE_VERSION = v8.7.90
|
||||
MODULE_REPO = https://github.com/redisbloom/redisbloom
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redisbloom.so
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
SRC_DIR = src
|
||||
MODULE_VERSION = v8.7.80
|
||||
MODULE_VERSION = v8.7.90
|
||||
MODULE_REPO = https://github.com/redisjson/redisjson
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/rejson.so
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
SRC_DIR = src
|
||||
MODULE_VERSION = v8.7.80
|
||||
MODULE_VERSION = v8.7.90
|
||||
MODULE_REPO = https://github.com/redistimeseries/redistimeseries
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redistimeseries.so
|
||||
|
||||
|
|
|
|||
|
|
@ -2039,8 +2039,9 @@ latency-monitor-threshold 0
|
|||
# (Note: not included in the 'A' class)
|
||||
# c Type-changed events generated every time a key's type changes
|
||||
# (Note: not included in the 'A' class)
|
||||
# r rate limit event
|
||||
# A Alias for g$lshzxetd, so that the "AKE" string means all the events
|
||||
# except key-miss, new key, overwritten and type-changed.
|
||||
# except key-miss, new key, overwritten, type-changed and rate-limit.
|
||||
#
|
||||
# The "notify-keyspace-events" takes as argument a string that is composed
|
||||
# of zero or multiple characters. The empty string means that notifications
|
||||
|
|
|
|||
10
src/Makefile
10
src/Makefile
|
|
@ -35,7 +35,7 @@ endif
|
|||
ifneq ($(OPTIMIZATION),-O0)
|
||||
OPTIMIZATION+=-fno-omit-frame-pointer
|
||||
endif
|
||||
DEPENDENCY_TARGETS=hiredis linenoise lua hdr_histogram fpconv fast_float xxhash
|
||||
DEPENDENCY_TARGETS=hiredis linenoise lua hdr_histogram fpconv xxhash
|
||||
NODEPS:=clean distclean
|
||||
|
||||
# Default settings
|
||||
|
|
@ -149,7 +149,7 @@ endif
|
|||
|
||||
FINAL_CFLAGS=$(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) $(REDIS_CFLAGS)
|
||||
FINAL_LDFLAGS=$(LDFLAGS) $(OPT) $(REDIS_LDFLAGS) $(DEBUG)
|
||||
FINAL_LIBS=-lm -lstdc++
|
||||
FINAL_LIBS=-lm
|
||||
DEBUG=-g -ggdb
|
||||
|
||||
# Linux ARM32 needs -latomic at linking time
|
||||
|
|
@ -257,7 +257,7 @@ ifdef OPENSSL_PREFIX
|
|||
endif
|
||||
|
||||
# Include paths to dependencies
|
||||
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src -I../deps/hdr_histogram -I../deps/fpconv -I../deps/fast_float -I../deps/xxhash
|
||||
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src -I../deps/hdr_histogram -I../deps/fpconv -I../deps/xxhash
|
||||
|
||||
# Determine systemd support and/or build preference (defaulting to auto-detection)
|
||||
BUILD_WITH_SYSTEMD=no
|
||||
|
|
@ -382,7 +382,7 @@ endif
|
|||
|
||||
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
|
||||
REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX)
|
||||
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o entry.o kvstore.o fwtree.o estore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_asm.o cluster_legacy.o cluster_slot_stats.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o flax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o keymeta.o chk.o hotkeys.o gcra.o
|
||||
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o entry.o kvstore.o fwtree.o estore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_asm.o cluster_legacy.o cluster_slot_stats.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o flax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o keymeta.o chk.o hotkeys.o gcra.o vector.o fast_float_strtod.o
|
||||
REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX)
|
||||
REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crccombine.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o cli_commands.o
|
||||
REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX)
|
||||
|
|
@ -442,7 +442,7 @@ endif
|
|||
|
||||
# redis-server
|
||||
$(REDIS_SERVER_NAME): $(REDIS_SERVER_OBJ) $(REDIS_VEC_SETS_OBJ)
|
||||
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a ../deps/hdr_histogram/libhdrhistogram.a ../deps/fpconv/libfpconv.a ../deps/fast_float/libfast_float.a ../deps/xxhash/libxxhash.a $(FINAL_LIBS)
|
||||
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a ../deps/hdr_histogram/libhdrhistogram.a ../deps/fpconv/libfpconv.a ../deps/xxhash/libxxhash.a $(FINAL_LIBS)
|
||||
|
||||
# redis-sentinel
|
||||
$(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ struct ACLCategoryItem {
|
|||
{"connection", ACL_CATEGORY_CONNECTION},
|
||||
{"transaction", ACL_CATEGORY_TRANSACTION},
|
||||
{"scripting", ACL_CATEGORY_SCRIPTING},
|
||||
{"ratelimit", ACL_CATEGORY_RATE_LIMIT},
|
||||
{NULL,0} /* Terminator. */
|
||||
};
|
||||
|
||||
|
|
|
|||
40
src/aof.c
40
src/aof.c
|
|
@ -2467,6 +2467,18 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
int rewriteGCRAObject(rio *r, robj *key, robj *o) {
|
||||
long long val;
|
||||
getLongLongFromGCRAObject(o, &val);
|
||||
|
||||
/* GCRASETVALUE <key> <tat> */
|
||||
if (rioWriteBulkCount(r,'*',3) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"GCRASETVALUE",12) == 0) return 0;
|
||||
if (rioWriteBulkObject(r,key) == 0) return 0;
|
||||
if (rioWriteBulkLongLong(r,val) == 0) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Call the module type callback in order to rewrite a data type
|
||||
* that is exported by a module and is not handled by Redis itself.
|
||||
* The function returns 0 on error, 1 on success. */
|
||||
|
|
@ -2522,6 +2534,8 @@ int rewriteObject(rio *r, robj *key, robj *o, int dbid, long long expiretime) {
|
|||
if (rewriteHashObject(r,key,o) == 0) return C_ERR;
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
if (rewriteStreamObject(r,key,o) == 0) return C_ERR;
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
if (rewriteGCRAObject(r,key,o) == 0) return C_ERR;
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
if (rewriteModuleObject(r,key,o,dbid) == 0) return C_ERR;
|
||||
} else {
|
||||
|
|
@ -2570,10 +2584,20 @@ int rewriteAppendOnlyFileRio(rio *aof) {
|
|||
if (rioWriteBulkLongLong(aof,j) == 0) goto werr;
|
||||
|
||||
kvstoreIteratorInit(&kvs_it, db->keys);
|
||||
int last_slot = -1;
|
||||
/* Iterate this DB writing every entry */
|
||||
while((de = kvstoreIteratorNext(&kvs_it)) != NULL) {
|
||||
long long expiretime;
|
||||
size_t aof_bytes_before_key = aof->processed_bytes;
|
||||
int curr_slot = kvstoreIteratorGetCurrentDictIndex(&kvs_it);
|
||||
|
||||
/* In cluster mode, dismiss bucket arrays of the previous slot
|
||||
* which won't be accessed again, to avoid CoW. */
|
||||
if (server.cluster_enabled && curr_slot != last_slot) {
|
||||
if (server.in_fork_child && last_slot != -1)
|
||||
dismissDictBucketsMemory(kvstoreGetDict(db->keys, last_slot));
|
||||
last_slot = curr_slot;
|
||||
}
|
||||
|
||||
/* Get the value object (of type kvobj) */
|
||||
kvobj *o = dictGetKV(de);
|
||||
|
|
@ -2582,12 +2606,9 @@ int rewriteAppendOnlyFileRio(rio *aof) {
|
|||
expiretime = kvobjGetExpire(o);
|
||||
|
||||
/* Skip keys that are being trimmed */
|
||||
if (server.cluster_enabled) {
|
||||
int curr_slot = kvstoreIteratorGetCurrentDictIndex(&kvs_it);
|
||||
if (isSlotInTrimJob(curr_slot)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
if (server.cluster_enabled && isSlotInTrimJob(curr_slot)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Set on stack string object for key */
|
||||
|
|
@ -2600,7 +2621,8 @@ int rewriteAppendOnlyFileRio(rio *aof) {
|
|||
* OS and possibly avoid or decrease COW. We give the dismiss
|
||||
* mechanism a hint about an estimated size of the object we stored. */
|
||||
size_t dump_size = aof->processed_bytes - aof_bytes_before_key;
|
||||
if (server.in_fork_child) dismissObject(o, dump_size);
|
||||
if (server.in_fork_child && dump_size > server.page_size/2)
|
||||
dismissObject(o, dump_size);
|
||||
|
||||
/* Update info every 1 second (approximately).
|
||||
* in order to avoid calling mstime() on each iteration, we will
|
||||
|
|
@ -2618,6 +2640,10 @@ int rewriteAppendOnlyFileRio(rio *aof) {
|
|||
debugDelay(server.rdb_key_save_delay);
|
||||
}
|
||||
kvstoreIteratorReset(&kvs_it);
|
||||
|
||||
/* Dismiss bucket arrays of kvstore in standalone mode. */
|
||||
if (server.in_fork_child && !server.cluster_enabled)
|
||||
dismissKvstoreBucketsMemory(db->keys);
|
||||
}
|
||||
serverLog(LL_NOTICE, "AOF rewrite done, %ld keys saved, %llu keys skipped.", key_count, skipped);
|
||||
return C_OK;
|
||||
|
|
|
|||
|
|
@ -1057,13 +1057,27 @@ void clusterMigrationCommand(client *c) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Returns the address of the node in the format "ip:port". */
|
||||
static const char *getNodeAddressStr(const char *node_id, int len) {
|
||||
serverAssert(node_id != NULL);
|
||||
static char buf[NET_HOST_PORT_STR_LEN];
|
||||
|
||||
clusterNode *n = clusterLookupNode(node_id, len);
|
||||
char *ip = n ? clusterNodeIp(n) : "?";
|
||||
int port = n ? (server.tls_replication ? clusterNodeTlsPort(n) :
|
||||
clusterNodeTcpPort(n)) : 0;
|
||||
formatAddr(buf, sizeof(buf), ip, port);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Log a human-readable message for ASM task lifecycle events. */
|
||||
void asmLogTaskEvent(asmTask *task, int event) {
|
||||
sds str = slotRangeArrayToString(task->slots);
|
||||
|
||||
switch (event) {
|
||||
case ASM_EVENT_IMPORT_STARTED:
|
||||
serverLog(LL_NOTICE, "Import task %s started for slots: %s", task->id, str);
|
||||
serverLog(LL_NOTICE, "Import task %s started for slots: %s, source address: %s",
|
||||
task->id, str, getNodeAddressStr(task->source, CLUSTER_NAMELEN));
|
||||
break;
|
||||
case ASM_EVENT_IMPORT_FAILED:
|
||||
serverLog(LL_NOTICE, "Import task %s failed for slots: %s", task->id, str);
|
||||
|
|
@ -1076,8 +1090,8 @@ void asmLogTaskEvent(asmTask *task, int event) {
|
|||
task->id, str, getKeyCountInSlotRangeArray(task->slots));
|
||||
break;
|
||||
case ASM_EVENT_MIGRATE_STARTED:
|
||||
serverLog(LL_NOTICE, "Migrate task %s started for slots: %s (number of keys at start: %llu)",
|
||||
task->id, str, getKeyCountInSlotRangeArray(task->slots));
|
||||
serverLog(LL_NOTICE, "Migrate task %s started for slots: %s, destination address: %s, (number of keys at start: %llu)",
|
||||
task->id, str, getNodeAddressStr(task->dest, CLUSTER_NAMELEN), getKeyCountInSlotRangeArray(task->slots));
|
||||
break;
|
||||
case ASM_EVENT_MIGRATE_FAILED:
|
||||
serverLog(LL_NOTICE, "Migrate task %s failed for slots: %s", task->id, str);
|
||||
|
|
@ -3033,7 +3047,7 @@ void asmTriggerBackgroundTrim(asmTrimCtx *trim_ctx, int migration_cleanup) {
|
|||
CLUSTER_SLOT_MASK_BITS,
|
||||
KVSTORE_ALLOCATE_DICTS_ON_DEMAND);
|
||||
estore *subexpires = estoreCreate(&subexpiresBucketsType, CLUSTER_SLOT_MASK_BITS);
|
||||
dict *stream_idmp_keys = dictCreate(&objectKeyPointerValueDictType);
|
||||
dict *stream_idmp_keys = dictCreate(&objectKeyNoValueDictType);
|
||||
|
||||
size_t total_keys = 0;
|
||||
|
||||
|
|
|
|||
124
src/commands.def
124
src/commands.def
|
|
@ -24,7 +24,8 @@ const char *COMMAND_GROUP_STR[] = {
|
|||
"geo",
|
||||
"stream",
|
||||
"bitmap",
|
||||
"module"
|
||||
"module",
|
||||
"rate_limit"
|
||||
};
|
||||
|
||||
const char *commandGroupStr(int index) {
|
||||
|
|
@ -5379,6 +5380,59 @@ struct COMMAND_ARG UNSUBSCRIBE_Args[] = {
|
|||
{MAKE_ARG("channel",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** GCRA ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* GCRA history */
|
||||
#define GCRA_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* GCRA tips */
|
||||
#define GCRA_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* GCRA key specs */
|
||||
keySpec GCRA_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* GCRA argument table */
|
||||
struct COMMAND_ARG GCRA_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max-burst",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("tokens-per-period",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("period",ARG_TYPE_DOUBLE,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,"TOKENS",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
/********** GCRASETVALUE ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* GCRASETVALUE history */
|
||||
#define GCRASETVALUE_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* GCRASETVALUE tips */
|
||||
#define GCRASETVALUE_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* GCRASETVALUE key specs */
|
||||
keySpec GCRASETVALUE_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* GCRASETVALUE argument table */
|
||||
struct COMMAND_ARG GCRASETVALUE_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("tat",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** EVAL ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
|
|
@ -9165,7 +9219,9 @@ struct COMMAND_ARG ZINCRBY_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZINTER history */
|
||||
#define ZINTER_History NULL
|
||||
commandHistory ZINTER_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9185,6 +9241,7 @@ struct COMMAND_ARG ZINTER_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZINTER argument table */
|
||||
|
|
@ -9192,7 +9249,7 @@ struct COMMAND_ARG ZINTER_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZINTER_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZINTER_aggregate_Subargs},
|
||||
{MAKE_ARG("withscores",ARG_TYPE_PURE_TOKEN,-1,"WITHSCORES",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
|
|
@ -9226,7 +9283,9 @@ struct COMMAND_ARG ZINTERCARD_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZINTERSTORE history */
|
||||
#define ZINTERSTORE_History NULL
|
||||
commandHistory ZINTERSTORE_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9246,6 +9305,7 @@ struct COMMAND_ARG ZINTERSTORE_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZINTERSTORE argument table */
|
||||
|
|
@ -9254,7 +9314,7 @@ struct COMMAND_ARG ZINTERSTORE_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZINTERSTORE_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZINTERSTORE_aggregate_Subargs},
|
||||
};
|
||||
|
||||
/********** ZLEXCOUNT ********************/
|
||||
|
|
@ -9894,7 +9954,9 @@ struct COMMAND_ARG ZSCORE_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZUNION history */
|
||||
#define ZUNION_History NULL
|
||||
commandHistory ZUNION_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9914,6 +9976,7 @@ struct COMMAND_ARG ZUNION_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZUNION argument table */
|
||||
|
|
@ -9921,7 +9984,7 @@ struct COMMAND_ARG ZUNION_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZUNION_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZUNION_aggregate_Subargs},
|
||||
{MAKE_ARG("withscores",ARG_TYPE_PURE_TOKEN,-1,"WITHSCORES",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
|
|
@ -9929,7 +9992,9 @@ struct COMMAND_ARG ZUNION_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZUNIONSTORE history */
|
||||
#define ZUNIONSTORE_History NULL
|
||||
commandHistory ZUNIONSTORE_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9949,6 +10014,7 @@ struct COMMAND_ARG ZUNIONSTORE_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZUNIONSTORE argument table */
|
||||
|
|
@ -9957,7 +10023,7 @@ struct COMMAND_ARG ZUNIONSTORE_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZUNIONSTORE_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZUNIONSTORE_aggregate_Subargs},
|
||||
};
|
||||
|
||||
/********** XACK ********************/
|
||||
|
|
@ -11082,34 +11148,6 @@ struct COMMAND_ARG DIGEST_Args[] = {
|
|||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** GCRA ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* GCRA history */
|
||||
#define GCRA_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* GCRA tips */
|
||||
#define GCRA_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* GCRA key specs */
|
||||
keySpec GCRA_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* GCRA argument table */
|
||||
struct COMMAND_ARG GCRA_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max-burst",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("tokens-per-period",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("period",ARG_TYPE_DOUBLE,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,"TOKENS",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
/********** GET ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
|
|
@ -11917,6 +11955,9 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("subscribe","Listens for messages published to channels.","O(N) where N is the number of channels to subscribe to.","2.0.0",CMD_DOC_NONE,NULL,NULL,"pubsub",COMMAND_GROUP_PUBSUB,SUBSCRIBE_History,0,SUBSCRIBE_Tips,0,subscribeCommand,-2,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,SUBSCRIBE_Keyspecs,0,NULL,1),.args=SUBSCRIBE_Args},
|
||||
{MAKE_CMD("sunsubscribe","Stops listening to messages posted to shard channels.","O(N) where N is the number of shard channels to unsubscribe.","7.0.0",CMD_DOC_NONE,NULL,NULL,"pubsub",COMMAND_GROUP_PUBSUB,SUNSUBSCRIBE_History,0,SUNSUBSCRIBE_Tips,0,sunsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,0,SUNSUBSCRIBE_Keyspecs,1,NULL,1),.args=SUNSUBSCRIBE_Args},
|
||||
{MAKE_CMD("unsubscribe","Stops listening to messages posted to channels.","O(N) where N is the number of channels to unsubscribe.","2.0.0",CMD_DOC_NONE,NULL,NULL,"pubsub",COMMAND_GROUP_PUBSUB,UNSUBSCRIBE_History,0,UNSUBSCRIBE_Tips,0,unsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,UNSUBSCRIBE_Keyspecs,0,NULL,1),.args=UNSUBSCRIBE_Args},
|
||||
/* rate_limit */
|
||||
{MAKE_CMD("gcra","Rate limit via GCRA (Generic Cell Rate Algorithm).","O(1)","8.8.0",CMD_DOC_NONE,NULL,NULL,"rate_limit",COMMAND_GROUP_RATE_LIMIT,GCRA_History,0,GCRA_Tips,0,gcraCommand,-5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_RATE_LIMIT,GCRA_Keyspecs,1,NULL,5),.args=GCRA_Args},
|
||||
{MAKE_CMD("gcrasetvalue","An internal command for recording a GCRA TAT value during AOF rewrite and replication.","O(1)","8.8.0",CMD_DOC_NONE,NULL,NULL,"rate_limit",COMMAND_GROUP_RATE_LIMIT,GCRASETVALUE_History,0,GCRASETVALUE_Tips,0,gcraSetValueCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_RATE_LIMIT,GCRASETVALUE_Keyspecs,1,NULL,2),.args=GCRASETVALUE_Args},
|
||||
/* scripting */
|
||||
{MAKE_CMD("eval","Executes a server-side Lua script.","Depends on the script that is executed.","2.6.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,EVAL_History,0,EVAL_Tips,0,evalCommand,-3,CMD_NOSCRIPT|CMD_SKIP_MONITOR|CMD_MAY_REPLICATE|CMD_NO_MANDATORY_KEYS|CMD_STALE,ACL_CATEGORY_SCRIPTING,EVAL_Keyspecs,1,evalGetKeys,4),.args=EVAL_Args},
|
||||
{MAKE_CMD("evalsha","Executes a server-side Lua script by SHA1 digest.","Depends on the script that is executed.","2.6.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,EVALSHA_History,0,EVALSHA_Tips,0,evalShaCommand,-3,CMD_NOSCRIPT|CMD_SKIP_MONITOR|CMD_MAY_REPLICATE|CMD_NO_MANDATORY_KEYS|CMD_STALE,ACL_CATEGORY_SCRIPTING,EVALSHA_Keyspecs,1,evalGetKeys,4),.args=EVALSHA_Args},
|
||||
|
|
@ -11988,9 +12029,9 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("zdiff","Returns the difference between multiple sorted sets.","O(L + (N-K)log(N)) worst case where L is the total number of elements in all the sets, N is the size of the first set, and K is the size of the result set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZDIFF_History,0,ZDIFF_Tips,0,zdiffCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZDIFF_Keyspecs,1,zunionInterDiffGetKeys,3),.args=ZDIFF_Args},
|
||||
{MAKE_CMD("zdiffstore","Stores the difference of multiple sorted sets in a key.","O(L + (N-K)log(N)) worst case where L is the total number of elements in all the sets, N is the size of the first set, and K is the size of the result set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZDIFFSTORE_History,0,ZDIFFSTORE_Tips,0,zdiffstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZDIFFSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,3),.args=ZDIFFSTORE_Args},
|
||||
{MAKE_CMD("zincrby","Increments the score of a member in a sorted set.","O(log(N)) where N is the number of elements in the sorted set.","1.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINCRBY_History,0,ZINCRBY_Tips,0,zincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZINCRBY_Keyspecs,1,NULL,3),.args=ZINCRBY_Args},
|
||||
{MAKE_CMD("zinter","Returns the intersect of multiple sorted sets.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTER_History,0,ZINTER_Tips,0,zinterCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZINTER_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZINTER_Args},
|
||||
{MAKE_CMD("zinter","Returns the intersect of multiple sorted sets.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTER_History,1,ZINTER_Tips,0,zinterCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZINTER_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZINTER_Args},
|
||||
{MAKE_CMD("zintercard","Returns the number of members of the intersect of multiple sorted sets.","O(N*K) worst case with N being the smallest input sorted set, K being the number of input sorted sets.","7.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTERCARD_History,0,ZINTERCARD_Tips,0,zinterCardCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZINTERCARD_Keyspecs,1,zunionInterDiffGetKeys,3),.args=ZINTERCARD_Args},
|
||||
{MAKE_CMD("zinterstore","Stores the intersect of multiple sorted sets in a key.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTERSTORE_History,0,ZINTERSTORE_Tips,0,zinterstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZINTERSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZINTERSTORE_Args},
|
||||
{MAKE_CMD("zinterstore","Stores the intersect of multiple sorted sets in a key.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTERSTORE_History,1,ZINTERSTORE_Tips,0,zinterstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZINTERSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZINTERSTORE_Args},
|
||||
{MAKE_CMD("zlexcount","Returns the number of members in a sorted set within a lexicographical range.","O(log(N)) with N being the number of elements in the sorted set.","2.8.9",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZLEXCOUNT_History,0,ZLEXCOUNT_Tips,0,zlexcountCommand,4,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZLEXCOUNT_Keyspecs,1,NULL,3),.args=ZLEXCOUNT_Args},
|
||||
{MAKE_CMD("zmpop","Returns the highest- or lowest-scoring members from one or more sorted sets after removing them. Deletes the sorted set if the last member was popped.","O(K) + O(M*log(N)) where K is the number of provided keys, N being the number of elements in the sorted set, and M being the number of elements popped.","7.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZMPOP_History,0,ZMPOP_Tips,0,zmpopCommand,-4,CMD_WRITE,ACL_CATEGORY_SORTEDSET,ZMPOP_Keyspecs,1,zmpopGetKeys,4),.args=ZMPOP_Args},
|
||||
{MAKE_CMD("zmscore","Returns the score of one or more members in a sorted set.","O(N) where N is the number of members being requested.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZMSCORE_History,0,ZMSCORE_Tips,0,zmscoreCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZMSCORE_Keyspecs,1,NULL,2),.args=ZMSCORE_Args},
|
||||
|
|
@ -12012,8 +12053,8 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("zrevrank","Returns the index of a member in a sorted set ordered by descending scores.","O(log(N))","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZREVRANK_History,1,ZREVRANK_Tips,0,zrevrankCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZREVRANK_Keyspecs,1,NULL,3),.args=ZREVRANK_Args},
|
||||
{MAKE_CMD("zscan","Iterates over members and scores of a sorted set.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZSCAN_History,0,ZSCAN_Tips,1,zscanCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZSCAN_Keyspecs,1,NULL,4),.args=ZSCAN_Args},
|
||||
{MAKE_CMD("zscore","Returns the score of a member in a sorted set.","O(1)","1.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZSCORE_History,0,ZSCORE_Tips,0,zscoreCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZSCORE_Keyspecs,1,NULL,2),.args=ZSCORE_Args},
|
||||
{MAKE_CMD("zunion","Returns the union of multiple sorted sets.","O(N)+O(M*log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNION_History,0,ZUNION_Tips,0,zunionCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZUNION_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZUNION_Args},
|
||||
{MAKE_CMD("zunionstore","Stores the union of multiple sorted sets in a key.","O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNIONSTORE_History,0,ZUNIONSTORE_Tips,0,zunionstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZUNIONSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZUNIONSTORE_Args},
|
||||
{MAKE_CMD("zunion","Returns the union of multiple sorted sets.","O(N)+O(M*log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNION_History,1,ZUNION_Tips,0,zunionCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZUNION_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZUNION_Args},
|
||||
{MAKE_CMD("zunionstore","Stores the union of multiple sorted sets in a key.","O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNIONSTORE_History,1,ZUNIONSTORE_Tips,0,zunionstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZUNIONSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZUNIONSTORE_Args},
|
||||
/* stream */
|
||||
{MAKE_CMD("xack","Returns the number of messages that were successfully acknowledged by the consumer group member of a stream.","O(1) for each message ID processed.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XACK_History,0,XACK_Tips,0,xackCommand,-4,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XACK_Keyspecs,1,NULL,3),.args=XACK_Args},
|
||||
{MAKE_CMD("xackdel","Acknowledges and deletes one or multiple messages for a stream consumer group.","O(1) for each message ID processed.","8.2.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XACKDEL_History,0,XACKDEL_Tips,0,xackdelCommand,-6,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XACKDEL_Keyspecs,1,NULL,4),.args=XACKDEL_Args},
|
||||
|
|
@ -12041,7 +12082,6 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("decrby","Decrements a number from the integer value of a key. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DECRBY_History,0,DECRBY_Tips,0,decrbyCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,DECRBY_Keyspecs,1,NULL,2),.args=DECRBY_Args},
|
||||
{MAKE_CMD("delex","Conditionally removes the specified key based on value or digest comparison.","O(1) for IFEQ/IFNE, O(N) for IFDEQ/IFDNE where N is the length of the string value.","8.4.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DELEX_History,0,DELEX_Tips,0,delexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,DELEX_Keyspecs,1,delexGetKeys,2),.args=DELEX_Args},
|
||||
{MAKE_CMD("digest","Returns the XXH3 hash of a string value.","O(N) where N is the length of the string value.","8.4.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DIGEST_History,0,DIGEST_Tips,0,digestCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,DIGEST_Keyspecs,1,NULL,1),.args=DIGEST_Args},
|
||||
{MAKE_CMD("gcra","Rate limit via GCRA (Generic Cell Rate Algorithm).","O(1)","8.8.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GCRA_History,0,GCRA_Tips,0,gcraCommand,-5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,GCRA_Keyspecs,1,NULL,5),.args=GCRA_Args},
|
||||
{MAKE_CMD("get","Returns the string value of a key.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GET_History,0,GET_Tips,0,getCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,GET_Keyspecs,1,NULL,1),.args=GET_Args},
|
||||
{MAKE_CMD("getdel","Returns the string value of a key after deleting the key.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETDEL_History,0,GETDEL_Tips,0,getdelCommand,2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETDEL_Keyspecs,1,NULL,1),.args=GETDEL_Args},
|
||||
{MAKE_CMD("getex","Returns the string value of a key after setting its expiration time.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETEX_History,0,GETEX_Tips,0,getexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETEX_Keyspecs,1,NULL,2),.args=GETEX_Args},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"GCRA": {
|
||||
"summary": "Rate limit via GCRA (Generic Cell Rate Algorithm).",
|
||||
"complexity": "O(1)",
|
||||
"group": "string",
|
||||
"group": "rate_limit",
|
||||
"since": "8.8.0",
|
||||
"arity": -5,
|
||||
"function": "gcraCommand",
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"STRING"
|
||||
"RATE_LIMIT"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
|
|
|
|||
52
src/commands/gcrasetvalue.json
Normal file
52
src/commands/gcrasetvalue.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"GCRASETVALUE": {
|
||||
"summary": "An internal command for recording a GCRA TAT value during AOF rewrite and replication.",
|
||||
"complexity": "O(1)",
|
||||
"group": "rate_limit",
|
||||
"since": "8.8.0",
|
||||
"arity": 3,
|
||||
"function": "gcraSetValueCommand",
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"RATE_LIMIT"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"OW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"const": "OK"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "tat",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -3,
|
||||
"function": "zinterCommand",
|
||||
"get_keys_function": "zunionInterDiffGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"READONLY"
|
||||
],
|
||||
|
|
@ -101,6 +107,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -4,
|
||||
"function": "zinterstoreCommand",
|
||||
"get_keys_function": "zunionInterDiffStoreGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM"
|
||||
|
|
@ -100,6 +106,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -3,
|
||||
"function": "zunionCommand",
|
||||
"get_keys_function": "zunionInterDiffGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"READONLY"
|
||||
],
|
||||
|
|
@ -101,6 +107,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -4,
|
||||
"function": "zunionstoreCommand",
|
||||
"get_keys_function": "zunionInterDiffStoreGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM"
|
||||
|
|
@ -99,6 +105,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2917,7 +2917,7 @@ static int setConfigNotifyKeyspaceEventsOption(standardConfig *config, sds *argv
|
|||
}
|
||||
int flags = keyspaceEventsStringToFlags(argv[0]);
|
||||
if (flags == -1) {
|
||||
*err = "Invalid event class character. Use 'Ag$lshzxeKEtmdn'.";
|
||||
*err = "Invalid event class character. Use 'Ag$lshzxeKEtmdnocr'.";
|
||||
return 0;
|
||||
}
|
||||
server.notify_keyspace_events = flags;
|
||||
|
|
|
|||
8
src/db.c
8
src/db.c
|
|
@ -1083,7 +1083,7 @@ redisDb *initTempDb(void) {
|
|||
tempDb[i].expires = kvstoreCreate(&kvstoreBaseType, &dbExpiresDictType,
|
||||
slot_count_bits, flags);
|
||||
tempDb[i].subexpires = estoreCreate(&subexpiresBucketsType, slot_count_bits);
|
||||
tempDb[i].stream_idmp_keys = dictCreate(&objectKeyPointerValueDictType);
|
||||
tempDb[i].stream_idmp_keys = dictCreate(&objectKeyNoValueDictType);
|
||||
}
|
||||
|
||||
return tempDb;
|
||||
|
|
@ -1117,7 +1117,7 @@ void streamMoveIdmpKeys(dict *src, dict *dst, int slot) {
|
|||
while ((de = dictNext(di)) != NULL) {
|
||||
robj *key = dictGetKey(de);
|
||||
if (calculateKeySlot(key->ptr) == slot) {
|
||||
if (dictAdd(dst, key, dictGetVal(de)) == DICT_OK) {
|
||||
if (dictAddRaw(dst, key, NULL)) {
|
||||
incrRefCount(key);
|
||||
}
|
||||
dictDelete(src, key);
|
||||
|
|
@ -1756,7 +1756,8 @@ char *obj_type_name[OBJ_TYPE_MAX] = {
|
|||
"zset",
|
||||
"hash",
|
||||
NULL, /* module type is special */
|
||||
"stream"
|
||||
"stream",
|
||||
"gcra"
|
||||
};
|
||||
|
||||
/* Helper function to get type from a string in scan commands */
|
||||
|
|
@ -2438,6 +2439,7 @@ void copyCommand(client *c) {
|
|||
case OBJ_ZSET: newobj = zsetDup(o); break;
|
||||
case OBJ_HASH: newobj = hashTypeDup(o, &minHashExpire); break;
|
||||
case OBJ_STREAM: newobj = streamDup(o); break;
|
||||
case OBJ_GCRA: newobj = gcraDup(o); break;
|
||||
case OBJ_MODULE:
|
||||
newobj = moduleTypeDupOrReply(c, key, newkey, dst->id, o);
|
||||
if (!newobj) return;
|
||||
|
|
|
|||
16
src/debug.c
16
src/debug.c
|
|
@ -123,6 +123,14 @@ void mixStringObjectDigest(unsigned char *digest, robj *o) {
|
|||
decrRefCount(o);
|
||||
}
|
||||
|
||||
void mixGCRAObjectDigest(unsigned char *digest, robj *o) {
|
||||
char buf[LONG_STR_SIZE];
|
||||
long long val;
|
||||
getLongLongFromGCRAObject(o, &val);
|
||||
int len = ll2string(buf, sizeof(buf), val);
|
||||
mixDigest(digest,buf,len);
|
||||
}
|
||||
|
||||
/* This function computes the digest of a data structure stored in the
|
||||
* object 'o'. It is the core of the DEBUG DIGEST command: when taking the
|
||||
* digest of a whole dataset, we take the digest of the key and the value
|
||||
|
|
@ -255,6 +263,8 @@ void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o)
|
|||
}
|
||||
}
|
||||
streamIteratorStop(&si);
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
mixGCRAObjectDigest(digest, o);
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
RedisModuleDigest md = {{0},{0},keyobj,db->id};
|
||||
moduleValue *mv = o->ptr;
|
||||
|
|
@ -896,7 +906,7 @@ NULL
|
|||
addReplyError(c,"Wrong protocol type name. Please use one of the following: string|integer|double|bignum|null|array|set|map|attrib|push|verbatim|true|false");
|
||||
}
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"sleep") && c->argc == 3) {
|
||||
double dtime = fast_float_strtod(c->argv[2]->ptr,NULL);
|
||||
double dtime = fast_float_strtod(c->argv[2]->ptr,sdslen(c->argv[2]->ptr),NULL);
|
||||
long long utime = dtime*1000000;
|
||||
struct timespec tv;
|
||||
|
||||
|
|
@ -1302,6 +1312,10 @@ void serverLogObjectDebugInfo(const robj *o) {
|
|||
serverLog(LL_WARNING,"Skiplist level: %d", (int) ((const zset*)o->ptr)->zsl->level);
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
serverLog(LL_WARNING,"Stream size: %d", (int) streamLength(o));
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
#if UINTPTR_MAX == 0xffffffffffffffff
|
||||
serverLog(LL_WARNING, "GCRA object: %lld", (long long)o->ptr);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1211,6 +1211,13 @@ void defragKey(defragKeysCtx *ctx, dictEntry *de, dictEntryLink link) {
|
|||
}
|
||||
} else if (ob->type == OBJ_STREAM) {
|
||||
defragStream(ctx, ob);
|
||||
} else if (ob->type == OBJ_GCRA) {
|
||||
/* GCRA object is just an allocation to a long long value */
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
void *newptr, *ptr = ob->ptr;
|
||||
if ((newptr = activeDefragAlloc(ptr)))
|
||||
ob->ptr = newptr;
|
||||
#endif
|
||||
} else if (ob->type == OBJ_MODULE) {
|
||||
defragModule(ctx,db, ob);
|
||||
} else {
|
||||
|
|
|
|||
544
src/fast_float_strtod.c
Normal file
544
src/fast_float_strtod.c
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
/* fast_float_strtod.c - Fast string to double conversion
|
||||
*
|
||||
* This is a C conversion of a subset of the fast_float C++ library,
|
||||
* implementing only what Redis needs: parsing decimal floating-point strings.
|
||||
*
|
||||
* Original fast_float library:
|
||||
* https://github.com/fastfloat/fast_float
|
||||
* by Daniel Lemire and João Paulo Magalhaes
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021 The fast_float authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
|
||||
#include "fast_float_strtod.h"
|
||||
#include "config.h"
|
||||
#include "zmalloc.h"
|
||||
|
||||
/* Powers of 10 from 10^0 to 10^22 (exact in double precision).
|
||||
* These are the only powers of 10 that can be exactly represented as doubles. */
|
||||
static const double powers_of_ten[] = {
|
||||
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
|
||||
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22
|
||||
};
|
||||
|
||||
/* Maximum mantissa for fast path: 2^53 */
|
||||
#define MAX_MANTISSA_FAST_PATH 9007199254740992ULL /* 2^53 */
|
||||
|
||||
/* Exponent limits for fast path */
|
||||
#define MIN_EXPONENT_FAST_PATH -22
|
||||
#define MAX_EXPONENT_FAST_PATH 22
|
||||
|
||||
/* Maximum number of significant digits we track before overflow */
|
||||
#define MAX_DIGITS 19
|
||||
|
||||
/* Case-insensitive match against known lowercase literals using `| 0x20`.
|
||||
* Only valid when the target characters are ASCII letters (a-z). */
|
||||
static inline int strcasecmp_3(const char *s, char c0, char c1, char c2) {
|
||||
return ((s[0] | 0x20) == c0) & ((s[1] | 0x20) == c1) & ((s[2] | 0x20) == c2);
|
||||
}
|
||||
|
||||
/* Case-insensitive comparison for first n characters.
|
||||
* Only valid when the target characters are ASCII letters (a-z). */
|
||||
static int strncasecmp_local(const char *s1, const char *s2, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
int diff = (s1[i] | 0x20) - s2[i];
|
||||
if (diff) return diff;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse inf/nan special values.
|
||||
* Returns 1 if parsed successfully, 0 otherwise.
|
||||
* On success, *endptr points past the parsed value. */
|
||||
static inline int parse_infnan(const char *p, const char *pend, double *result, const char **endptr) {
|
||||
int negative = (*p == '-');
|
||||
if (*p == '-' || *p == '+') p++;
|
||||
size_t remaining = pend - p;
|
||||
|
||||
if (remaining >= 3) {
|
||||
if (strcasecmp_3(p, 'n', 'a', 'n')) {
|
||||
*result = negative ? -NAN : NAN;
|
||||
p += 3;
|
||||
/* Check for optional nan(n-char-seq) */
|
||||
if (p < pend && *p == '(') {
|
||||
const char *start = p;
|
||||
p++;
|
||||
while (p < pend) {
|
||||
char c = *p;
|
||||
if (c == ')') {
|
||||
p++;
|
||||
break;
|
||||
}
|
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_')) {
|
||||
/* Invalid character, revert to position after "nan" */
|
||||
p = start;
|
||||
break;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
/* If we didn't find closing ')', revert */
|
||||
if (p[-1] != ')') {
|
||||
p = start;
|
||||
}
|
||||
}
|
||||
if (endptr) *endptr = (char *)p;
|
||||
return 1;
|
||||
}
|
||||
if (strcasecmp_3(p, 'i', 'n', 'f')) {
|
||||
*result = negative ? -INFINITY : INFINITY;
|
||||
p += 3;
|
||||
/* Check for optional "inity" suffix */
|
||||
if (remaining == 8 && strncasecmp_local(p, "inity", 5) == 0) {
|
||||
p += 5;
|
||||
}
|
||||
if (endptr) *endptr = (char *)p;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* SWAR (SIMD Within A Register) helpers for batch digit parsing. */
|
||||
|
||||
static inline uint64_t read8_to_u64(const char *p) {
|
||||
uint64_t val;
|
||||
memcpy(&val, p, sizeof(uint64_t));
|
||||
#if BYTE_ORDER == BIG_ENDIAN
|
||||
/* SWAR digit parsing assumes first char in LSB (little-endian layout). */
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
val = __builtin_bswap64(val);
|
||||
#else
|
||||
val = ((val & 0x00000000FFFFFFFFULL) << 32) | ((val & 0xFFFFFFFF00000000ULL) >> 32);
|
||||
val = ((val & 0x0000FFFF0000FFFFULL) << 16) | ((val & 0xFFFF0000FFFF0000ULL) >> 16);
|
||||
val = ((val & 0x00FF00FF00FF00FFULL) << 8) | ((val & 0xFF00FF00FF00FF00ULL) >> 8);
|
||||
#endif
|
||||
#endif
|
||||
return val;
|
||||
}
|
||||
|
||||
static inline int is_made_of_eight_digits(uint64_t val) {
|
||||
return !((((val + 0x4646464646464646ULL) | (val - 0x3030303030303030ULL)) &
|
||||
0x8080808080808080ULL));
|
||||
}
|
||||
|
||||
static inline uint32_t parse_eight_digits_swar(uint64_t val) {
|
||||
uint64_t const mask = 0x000000FF000000FFULL;
|
||||
uint64_t const mul1 = 0x000F424000000064ULL; /* 100 + (1000000ULL << 32) */
|
||||
uint64_t const mul2 = 0x0000271000000001ULL; /* 1 + (10000ULL << 32) */
|
||||
val -= 0x3030303030303030ULL;
|
||||
val = (val * 10) + (val >> 8);
|
||||
val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32;
|
||||
return (uint32_t)val;
|
||||
}
|
||||
|
||||
/* Parse a decimal number string into components.
|
||||
* This follows the fast_float algorithm closely. */
|
||||
static inline int parse_number_string(const char *p, const char *pend, double *result, const char **endptr) {
|
||||
uint64_t mantissa = 0; /* Mantissa digits as uint64 */
|
||||
int64_t exponent = 0; /* Decimal exponent (adjusted for decimal point) */
|
||||
int negative = 0; /* Sign flag */
|
||||
*endptr = p;
|
||||
|
||||
if (p == pend) return 0;
|
||||
|
||||
/* Parse sign */
|
||||
negative = (*p == '-');
|
||||
if (*p == '-' || *p == '+') {
|
||||
p++;
|
||||
if (p == pend) return 0;
|
||||
}
|
||||
|
||||
const char *start_digits = p;
|
||||
|
||||
/* Parse integer part */
|
||||
mantissa = 0;
|
||||
while (pend - p >= 8) {
|
||||
uint64_t val = read8_to_u64(p);
|
||||
if (!is_made_of_eight_digits(val)) break;
|
||||
mantissa = mantissa * 100000000 + parse_eight_digits_swar(val);
|
||||
p += 8;
|
||||
}
|
||||
while (p != pend && *p >= '0' && *p <= '9') {
|
||||
mantissa = mantissa * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
|
||||
int64_t digit_count = p - start_digits;
|
||||
|
||||
/* Parse decimal point and fractional part */
|
||||
exponent = 0;
|
||||
int has_decimal = (p != pend && *p == '.');
|
||||
|
||||
if (has_decimal) {
|
||||
p++;
|
||||
const char *before = p;
|
||||
while (pend - p >= 8) {
|
||||
uint64_t val = read8_to_u64(p);
|
||||
if (!is_made_of_eight_digits(val)) break;
|
||||
mantissa = mantissa * 100000000 + parse_eight_digits_swar(val);
|
||||
p += 8;
|
||||
}
|
||||
while (p != pend && *p >= '0' && *p <= '9') {
|
||||
mantissa = mantissa * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
exponent = before - p; /* Negative: number of fractional digits */
|
||||
digit_count += (p - before);
|
||||
}
|
||||
|
||||
/* Must have at least one digit */
|
||||
if (digit_count == 0) return 0;
|
||||
|
||||
/* Parse exponent */
|
||||
int64_t exp_number = 0;
|
||||
if (p != pend && (*p == 'e' || *p == 'E')) {
|
||||
const char *exp_start = p;
|
||||
p++;
|
||||
|
||||
int neg_exp = 0;
|
||||
if (p != pend && *p == '-') {
|
||||
neg_exp = 1;
|
||||
p++;
|
||||
} else if (p != pend && *p == '+') {
|
||||
p++;
|
||||
}
|
||||
|
||||
if (p == pend || *p < '0' || *p > '9') {
|
||||
/* No digits after e/E, revert to position before 'e' */
|
||||
p = exp_start;
|
||||
} else {
|
||||
while (p != pend && *p >= '0' && *p <= '9') {
|
||||
if (exp_number < 0x10000000) {
|
||||
exp_number = exp_number * 10 + (*p - '0');
|
||||
}
|
||||
p++;
|
||||
}
|
||||
if (neg_exp) exp_number = -exp_number;
|
||||
exponent += exp_number;
|
||||
}
|
||||
}
|
||||
|
||||
*endptr = p;
|
||||
|
||||
/* Handle overflow in mantissa: if we have too many digits,
|
||||
* we need to reparse more carefully */
|
||||
if (digit_count > MAX_DIGITS) {
|
||||
/* Skip leading zeros to get actual digit count */
|
||||
const char *s = start_digits;
|
||||
while (s != pend && (*s == '0' || *s == '.')) {
|
||||
if (*s == '0') digit_count--;
|
||||
s++;
|
||||
}
|
||||
|
||||
if (digit_count > MAX_DIGITS) return 0;
|
||||
}
|
||||
|
||||
/* Check if we're within fast path bounds */
|
||||
if (exponent < MIN_EXPONENT_FAST_PATH) return 0;
|
||||
if (exponent > MAX_EXPONENT_FAST_PATH) return 0;
|
||||
if (mantissa > MAX_MANTISSA_FAST_PATH) return 0;
|
||||
|
||||
/* Fast path: direct conversion */
|
||||
double value = (double)mantissa;
|
||||
|
||||
if (exponent < 0) {
|
||||
value = value / powers_of_ten[-exponent];
|
||||
} else if (exponent > 0) {
|
||||
value = value * powers_of_ten[exponent];
|
||||
}
|
||||
|
||||
if (negative) {
|
||||
value = -value;
|
||||
}
|
||||
|
||||
*result = value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Main conversion function.
|
||||
*
|
||||
* This function behaves similarly to the standard strtod function, converting
|
||||
* the initial portion of the string pointed to by `nptr` to a `double` value.
|
||||
* If the conversion fails, errno is set to EINVAL error code.
|
||||
*
|
||||
* @param nptr A pointer to the null-terminated byte string to be interpreted.
|
||||
* @param endptr A pointer to a pointer to character. If `endptr` is not NULL,
|
||||
* it will point to the character after the last character used
|
||||
* in the conversion.
|
||||
* @return The converted value as a double. If no valid conversion could
|
||||
* be performed, returns 0.0.
|
||||
*/
|
||||
static inline int fast_float_try_fast(const char *nptr, const char *pend, double *result, const char **endptr) {
|
||||
if (nptr == pend) {
|
||||
errno = EINVAL;
|
||||
if (endptr) *endptr = (char *)nptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse the number string */
|
||||
if (parse_number_string(nptr, pend, result, endptr)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Not a valid decimal number, try inf/nan special values */
|
||||
if (parse_infnan(nptr, pend, result, endptr)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double fast_float_strtod_fallback(const char *nptr, size_t len, char **endptr) {
|
||||
/* Since the input may not be null-terminated, we must copy it into a temporary buffer. */
|
||||
char static_buf[128];
|
||||
char *buf = static_buf;
|
||||
if (len >= sizeof(static_buf))
|
||||
buf = zmalloc(len + 1);
|
||||
memcpy(buf, nptr, len);
|
||||
buf[len] = '\0';
|
||||
|
||||
char *fallback_end;
|
||||
double result = strtod(buf, &fallback_end);
|
||||
if (endptr) *endptr = (char *)nptr + (fallback_end - buf);
|
||||
|
||||
/* If strtod failed to parse, set errno */
|
||||
if (fallback_end == buf) {
|
||||
errno = EINVAL;
|
||||
}
|
||||
|
||||
if (buf != static_buf) zfree(buf);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Convert string to double, with explicit length (string need NOT be null-terminated).
|
||||
* Falls back to strtod by copying to a temporary null-terminated buffer. */
|
||||
double fast_float_strtod(const char *nptr, size_t len, char **endptr) {
|
||||
double result = 0.0;
|
||||
const char *pend = nptr + len;
|
||||
const char *eptr;
|
||||
|
||||
/* Use fast path for non-null-terminated strings */
|
||||
if (likely(fast_float_try_fast(nptr, pend, &result, &eptr) && eptr == pend)) {
|
||||
if (endptr) *endptr = (char *)eptr;
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
/* On 32-bit x86 with x87 FPU, the fast-path fdiv/fmul result lives in
|
||||
* an 80-bit extended-precision register. With optimisation the compiler
|
||||
* may return that value in st(0) without ever storing it to a 64-bit
|
||||
* memory slot, so the caller would receive an 80-bit value that differs
|
||||
* from the correctly-rounded 64-bit double. Writing through a volatile
|
||||
* forces a real fstpl (store + pop to 64-bit memory) followed by fldl
|
||||
* (reload into st(0) from that 64-bit slot), ensuring the return value
|
||||
* is truncated to double precision before it reaches the caller. */
|
||||
volatile double ret = result;
|
||||
return ret;
|
||||
#else
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Fall back to strtod for complex cases:
|
||||
* - Very large or very small exponents
|
||||
* - Too many digits (need precise rounding)
|
||||
* This ensures we get correctly-rounded results for edge cases. */
|
||||
return fast_float_strtod_fallback(nptr, len, endptr);
|
||||
}
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
#include <stdio.h>
|
||||
#include "testhelp.h"
|
||||
|
||||
#define UNUSED(x) (void)(x)
|
||||
#define COUNTOF(arr) (int)(sizeof(arr) / sizeof((arr)[0]))
|
||||
|
||||
typedef struct {
|
||||
const char *input;
|
||||
double expected;
|
||||
} ff_testcase;
|
||||
|
||||
static int ff_eq(double a, double b) {
|
||||
if (isnan(a)) return isnan(b);
|
||||
if (isinf(a)) return isinf(b) && (a > 0) == (b > 0);
|
||||
return a == b;
|
||||
}
|
||||
|
||||
static void run_ff_tests(ff_testcase *cases, int n, int expect_failed) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char *s = cases[i].input;
|
||||
size_t len = strlen(s);
|
||||
char *eptr;
|
||||
|
||||
errno = 0;
|
||||
double d = fast_float_strtod(s, len, &eptr);
|
||||
int failed = ((size_t)(eptr - s) != len) || errno == EINVAL ||
|
||||
(errno == ERANGE && (d == HUGE_VAL || d == -HUGE_VAL || fpclassify(d) == FP_ZERO));
|
||||
int ok = (expect_failed == failed) && ff_eq(d, cases[i].expected);
|
||||
char descr[128];
|
||||
if (ok)
|
||||
snprintf(descr, sizeof(descr), "\"%s\" -> expect %s(%.20g)",
|
||||
s, expect_failed ? "fail" : "ok", cases[i].expected);
|
||||
else
|
||||
snprintf(descr, sizeof(descr), "\"%s\" -> expect %s(%.20g) but got %s(%.20g)",
|
||||
s, expect_failed ? "fail" : "ok", cases[i].expected, failed ? "fail" : "ok", d);
|
||||
test_cond(descr, ok);
|
||||
}
|
||||
}
|
||||
|
||||
int fastFloatTest(int argc, char **argv, int flags) {
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
/* Finite decimals: fast path, exponent ±22 edges, mantissa 2^53, strtod fallback. */
|
||||
ff_testcase decimal_ok[] = {
|
||||
{"0", 0.0},
|
||||
{"+0", 0.0},
|
||||
{"-0", -0.0},
|
||||
{"42", 42.0},
|
||||
{"+42", 42.0},
|
||||
{"-42", -42.0},
|
||||
{"00007", 7.0},
|
||||
{"00.25", 0.25},
|
||||
{"3.14", 3.14},
|
||||
{".5", 0.5},
|
||||
{"+.5", 0.5},
|
||||
{"1.", 1.0},
|
||||
{"0.", 0.0},
|
||||
{".0", 0.0},
|
||||
{"-1.5e2", -150.0},
|
||||
{"1e5", 1e5},
|
||||
{"1E5", 1e5},
|
||||
{"2E3", 2000.0},
|
||||
{"3e+5", 3e5},
|
||||
{"1e-10", 1e-10},
|
||||
{"1e-22", 1e-22},
|
||||
{"1e+22", 1e22},
|
||||
{"1e-23", 1e-23},
|
||||
{"1e+100", 1e100},
|
||||
{"1e-100", 1e-100},
|
||||
{"9007199254740992", 9007199254740992.0},
|
||||
{"9007199254740993", 9007199254740992.0},
|
||||
{"12345678901234567890", 1.2345678901234567e19},
|
||||
{"2.2250738585072012e-308", 2.2250738585072012e-308}, /* Near DBL_MIN boundary */
|
||||
{"0x10", 16.0},
|
||||
};
|
||||
run_ff_tests(decimal_ok, COUNTOF(decimal_ok), 0);
|
||||
|
||||
/* No valid prefix for full buffer, or trailing junk. */
|
||||
ff_testcase decimal_bad[] = {
|
||||
{"1abc", 1.0},
|
||||
{"1e", 1.0},
|
||||
{"1e+", 1.0},
|
||||
{"1e-", 1.0},
|
||||
{"1e+z", 1.0},
|
||||
{"12.34.56", 12.34},
|
||||
{"..1", 0.0},
|
||||
{"e10", 0.0},
|
||||
{"E10", 0.0},
|
||||
{"+", 0.0},
|
||||
{"-", 0.0},
|
||||
{"foo", 0.0},
|
||||
{"1 ", 1.0},
|
||||
{"3.14!", 3.14},
|
||||
};
|
||||
run_ff_tests(decimal_bad, COUNTOF(decimal_bad), 1);
|
||||
|
||||
ff_testcase inf_valid[] = {
|
||||
{"inf", INFINITY},
|
||||
{"INF", INFINITY},
|
||||
{"Inf", INFINITY},
|
||||
{"infinity", INFINITY},
|
||||
{"INFINITY", INFINITY},
|
||||
{"Infinity", INFINITY},
|
||||
{"+inf", INFINITY},
|
||||
{"-inf", -INFINITY},
|
||||
{"+infinity", INFINITY},
|
||||
{"-INFINITY", -INFINITY},
|
||||
};
|
||||
run_ff_tests(inf_valid, COUNTOF(inf_valid), 0);
|
||||
|
||||
ff_testcase inf_invalid[] = {
|
||||
{"in", 0},
|
||||
{"infin", INFINITY},
|
||||
{"infini1", INFINITY},
|
||||
{"infinitx", INFINITY},
|
||||
{"infinityy", INFINITY},
|
||||
{"info", INFINITY},
|
||||
{"ina", 0},
|
||||
{"INFI", INFINITY},
|
||||
{"iNf0", INFINITY},
|
||||
};
|
||||
run_ff_tests(inf_invalid, COUNTOF(inf_invalid), 1);
|
||||
|
||||
ff_testcase nan_valid[] = {
|
||||
{"nan", NAN},
|
||||
{"NAN", NAN},
|
||||
{"Nan", NAN},
|
||||
{"nan(123)", NAN},
|
||||
{"nan(abc)", NAN},
|
||||
{"nan(123abc)", NAN},
|
||||
};
|
||||
run_ff_tests(nan_valid, COUNTOF(nan_valid), 0);
|
||||
|
||||
ff_testcase nan_invalid[] = {
|
||||
{"na", 0},
|
||||
{"nan(", NAN}, /* unclosed paren */
|
||||
{"nan(abc", NAN}, /* missing closing paren */
|
||||
{"nan(ab!c)", NAN}, /* invalid char in paren */
|
||||
{"nan(ab c)", NAN}, /* space in paren */
|
||||
{"nanx", NAN}, /* trailing garbage */
|
||||
};
|
||||
run_ff_tests(nan_invalid, COUNTOF(nan_invalid), 1);
|
||||
|
||||
/* Large input that exceeds static_buf (128 bytes), exercising the zmalloc fallback path. */
|
||||
{
|
||||
/* Build a string "000...00042.0" with total length > 128. */
|
||||
char big[256];
|
||||
memset(big, '0', sizeof(big));
|
||||
big[sizeof(big) - 4] = '2';
|
||||
big[sizeof(big) - 3] = '.';
|
||||
big[sizeof(big) - 2] = '0';
|
||||
big[sizeof(big) - 1] = '\0';
|
||||
char *eptr;
|
||||
double d = fast_float_strtod(big, strlen(big), &eptr);
|
||||
test_cond("large input (>128 bytes) zmalloc fallback path",
|
||||
(size_t)(eptr - big) == strlen(big) && ff_eq(d, 2.0));
|
||||
|
||||
/* Large input that is completely invalid. */
|
||||
memset(big, 'x', sizeof(big) - 1);
|
||||
big[sizeof(big) - 1] = '\0';
|
||||
d = fast_float_strtod(big, strlen(big), &eptr);
|
||||
test_cond("invalid large input (>128 bytes) zmalloc fallback path",
|
||||
eptr == big && ff_eq(d, 0.0));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
13
src/fast_float_strtod.h
Normal file
13
src/fast_float_strtod.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
#ifndef __FAST_FLOAT_STRTOD_H__
|
||||
#define __FAST_FLOAT_STRTOD_H__
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
double fast_float_strtod(const char *nptr, size_t len, char **endptr);
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
int fastFloatTest(int argc, char **argv, int flags);
|
||||
#endif
|
||||
|
||||
#endif /* __FAST_FLOAT_STRTOD_H__ */
|
||||
77
src/gcra.c
77
src/gcra.c
|
|
@ -129,23 +129,11 @@ void gcraCommand(client *c) {
|
|||
long long tat_us, new_tat_us;
|
||||
dictEntryLink link;
|
||||
kvobj *kv = lookupKeyWriteWithLink(c->db, key, &link);
|
||||
if (checkType(c, kv, OBJ_STRING)) {
|
||||
if (checkType(c, kv, OBJ_GCRA)) {
|
||||
return;
|
||||
}
|
||||
if (kv != NULL) {
|
||||
/* Note the value of the key may have been overwritten outside of the
|
||||
* GCRA command (f.e by calling SET). We don't try to catch such errors
|
||||
* as this would be possible only with a dedicated structures for GCRA,
|
||||
* while using STRING gives us all the benefits of a redis key -
|
||||
* replication, setting expiration, etc. */
|
||||
if (getLongLongFromObject(kv, &tat_us) != C_OK) {
|
||||
addReplyError(c, "Invalid GCRA key");
|
||||
return;
|
||||
}
|
||||
if (tat_us <= 0) {
|
||||
addReplyError(c, "Negative time is invalid value for GCRA");
|
||||
return;
|
||||
}
|
||||
getLongLongFromGCRAObject(kv, &tat_us);
|
||||
} else {
|
||||
tat_us = now;
|
||||
}
|
||||
|
|
@ -208,10 +196,18 @@ void gcraCommand(client *c) {
|
|||
} else {
|
||||
limited = 0;
|
||||
ttl_us = new_tat_us - now;
|
||||
robj *tatobj = createStringObjectFromLongLong(new_tat_us);
|
||||
robj *tatobj = createGCRAObject(new_tat_us);
|
||||
setKeyByLink(c, c->db, key, &tatobj, kv ? SETKEY_ALREADY_EXIST : SETKEY_DOESNT_EXIST, &link);
|
||||
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
|
||||
notifyKeyspaceEvent(NOTIFY_RATE_LIMIT,"gcra",key,c->db->id);
|
||||
|
||||
/* The key implicitly sets its own expiry time (which is basically the
|
||||
* TaT after which time the value is no longer of any use). That way even
|
||||
* if only one GCRA command is called on a key it will automatically
|
||||
* expire after reaching its TaT without user needing to explicitly call
|
||||
* DEL on it.
|
||||
* These keys are expected to be numerous and short lived thus the
|
||||
* decision to keep the implicit expiraty.
|
||||
* NOTE: idea is same as in redis-cell. */
|
||||
long long when = new_tat_us / 1000;
|
||||
kv = setExpireByLink(c, c->db, key->ptr, when, link);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
|
||||
|
|
@ -219,9 +215,11 @@ void gcraCommand(client *c) {
|
|||
/* Replicating the command directly would mess up TaT as we use
|
||||
* commandTimeSnapshot. We instead rewrite the command as SET with the
|
||||
* appropriate expire time. */
|
||||
robj *pexat_obj = createStringObjectFromLongLong(when);
|
||||
rewriteClientCommandVector(c, 5, shared.set, key, kv, shared.pxat, pexat_obj);
|
||||
decrRefCount(pexat_obj);
|
||||
robj *gcrasetvalue = createStringObject("GCRASETVALUE", 12);
|
||||
robj *newtatstr = createStringObjectFromLongLong(new_tat_us);
|
||||
rewriteClientCommandVector(c, 3, gcrasetvalue, key, newtatstr);
|
||||
decrRefCount(gcrasetvalue);
|
||||
decrRefCount(newtatstr);
|
||||
|
||||
server.dirty++;
|
||||
}
|
||||
|
|
@ -239,3 +237,44 @@ void gcraCommand(client *c) {
|
|||
addReplyLongLong(c, retry_after_s);
|
||||
addReplyLongLong(c, reset_after_s);
|
||||
}
|
||||
|
||||
/* GCRASETVALUE key tat
|
||||
*
|
||||
* Internal command used during AOF rewrite to record a GCRA TAT value. The GCRA
|
||||
* command is also rewritten as GCRASETVALUE for replication since GCRA uses
|
||||
* commandTimeSnapshot. */
|
||||
void gcraSetValueCommand(client *c) {
|
||||
robj *key = c->argv[1];
|
||||
robj *tat = c->argv[2];
|
||||
long long when;
|
||||
|
||||
dictEntryLink link;
|
||||
kvobj *kv = lookupKeyWriteWithLink(c->db, key, &link);
|
||||
if (checkType(c, kv, OBJ_GCRA)) return;
|
||||
|
||||
if (getLongLongFromObjectOrReply(c, tat, &when, "Invalid TaT value") == C_ERR) {
|
||||
return;
|
||||
}
|
||||
if (when < 0) {
|
||||
addReplyError(c, "Invalid negative TaT value");
|
||||
return;
|
||||
}
|
||||
|
||||
robj *tatobj = createGCRAObject(when);
|
||||
setKeyByLink(c, c->db, key, &tatobj, kv ? SETKEY_ALREADY_EXIST : SETKEY_DOESNT_EXIST, &link);
|
||||
notifyKeyspaceEvent(NOTIFY_RATE_LIMIT,"gcra",key,c->db->id);
|
||||
|
||||
/* Just like the base GCRA command we set the expire time of the key implicitly. */
|
||||
long long when_ms = when / 1000;
|
||||
kv = setExpireByLink(c, c->db, key->ptr, when_ms, link);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
|
||||
server.dirty++;
|
||||
|
||||
addReply(c, shared.ok);
|
||||
}
|
||||
|
||||
robj *gcraDup(robj *o) {
|
||||
long long val;
|
||||
getLongLongFromGCRAObject(o, &val);
|
||||
return createGCRAObject(val);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ void emptyDbAsync(redisDb *db) {
|
|||
db->keys = kvstoreCreate(&kvstoreExType, &dbDictType, slot_count_bits, flags);
|
||||
db->expires = kvstoreCreate(&kvstoreBaseType, &dbExpiresDictType, slot_count_bits, flags);
|
||||
db->subexpires = estoreCreate(&subexpiresBucketsType, slot_count_bits);
|
||||
db->stream_idmp_keys = dictCreate(&objectKeyPointerValueDictType);
|
||||
db->stream_idmp_keys = dictCreate(&objectKeyNoValueDictType);
|
||||
protectClientReplyObjects(); /* Protect client reply objects before async free. */
|
||||
emptyDbDataAsync(oldkeys, oldexpires, oldsubexpires, old_stream_idmp_keys, NULL);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4225,6 +4225,7 @@ int RM_KeyType(RedisModuleKey *key) {
|
|||
case OBJ_HASH: return REDISMODULE_KEYTYPE_HASH;
|
||||
case OBJ_MODULE: return REDISMODULE_KEYTYPE_MODULE;
|
||||
case OBJ_STREAM: return REDISMODULE_KEYTYPE_STREAM;
|
||||
case OBJ_GCRA: return REDISMODULE_KEYTYPE_GCRA;
|
||||
default: return REDISMODULE_KEYTYPE_EMPTY;
|
||||
}
|
||||
}
|
||||
|
|
@ -9202,10 +9203,11 @@ void moduleReleaseGIL(void) {
|
|||
* - REDISMODULE_NOTIFY_OVERWRITTEN: Overwritten events
|
||||
* - REDISMODULE_NOTIFY_TYPE_CHANGED: Type-changed events
|
||||
* - REDISMODULE_NOTIFY_KEY_TRIMMED: Key trimmed events after a slot migration operation
|
||||
* - REDISMODULE_NOTIFY_RATE_LIMIT: Rate limit event
|
||||
* - REDISMODULE_NOTIFY_ALL: All events (Excluding REDISMODULE_NOTIFY_KEYMISS,
|
||||
* REDISMODULE_NOTIFY_NEW, REDISMODULE_NOTIFY_OVERWRITTEN,
|
||||
* REDISMODULE_NOTIFY_TYPE_CHANGED
|
||||
* and REDISMODULE_NOTIFY_KEY_TRIMMED)
|
||||
* REDISMODULE_NOTIFY_TYPE_CHANGED, REDISMODULE_NOTIFY_KEY_TRIMMED
|
||||
* and REDISMODULE_NOTIFY_RATE_LIMIT)
|
||||
* - REDISMODULE_NOTIFY_LOADED: A special notification available only for modules,
|
||||
* indicates that the key was loaded from persistence.
|
||||
* Notice, when this event fires, the given key
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ int keyspaceEventsStringToFlags(char *classes) {
|
|||
case 'n': flags |= NOTIFY_NEW; break;
|
||||
case 'o': flags |= NOTIFY_OVERWRITTEN; break;
|
||||
case 'c': flags |= NOTIFY_TYPE_CHANGED; break;
|
||||
case 'r': flags |= NOTIFY_RATE_LIMIT; break;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +71,7 @@ sds keyspaceEventsFlagsToString(int flags) {
|
|||
if (flags & NOTIFY_NEW) res = sdscatlen(res,"n",1);
|
||||
if (flags & NOTIFY_OVERWRITTEN) res = sdscatlen(res,"o",1);
|
||||
if (flags & NOTIFY_TYPE_CHANGED) res = sdscatlen(res,"c",1);
|
||||
if (flags & NOTIFY_RATE_LIMIT) res = sdscatlen(res,"r",1);
|
||||
}
|
||||
if (flags & NOTIFY_KEYSPACE) res = sdscatlen(res,"K",1);
|
||||
if (flags & NOTIFY_KEYEVENT) res = sdscatlen(res,"E",1);
|
||||
|
|
|
|||
83
src/object.c
83
src/object.c
|
|
@ -514,6 +514,23 @@ robj *createStreamObject(void) {
|
|||
return o;
|
||||
}
|
||||
|
||||
robj *createGCRAObject(long long value) {
|
||||
/* NOTE: for 32-bit systems we can't use integer encoding (as OBJ_STRING does)
|
||||
* as the GCRA object is a unixtime value in microseconds, which as of the
|
||||
* time of writing is already much more than 32-bit's LONG_MAX. */
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
long long *v = zmalloc(sizeof(long long));
|
||||
*v = value;
|
||||
robj *o = createObject(OBJ_GCRA,v);
|
||||
#else
|
||||
robj *o = createObject(OBJ_GCRA,NULL);
|
||||
o->ptr = (void*)value;
|
||||
#endif
|
||||
|
||||
o->encoding = OBJ_ENCODING_INT;
|
||||
return o;
|
||||
}
|
||||
|
||||
robj *createModuleObject(moduleType *mt, void *value) {
|
||||
moduleValue *mv = zmalloc(sizeof(*mv));
|
||||
mv->type = mt;
|
||||
|
|
@ -586,6 +603,14 @@ void freeStreamObject(robj *o) {
|
|||
freeStream(o->ptr);
|
||||
}
|
||||
|
||||
void freeGCRAObject(robj *o) {
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
zfree(o->ptr);
|
||||
#else
|
||||
(void)o;
|
||||
#endif
|
||||
}
|
||||
|
||||
void incrRefCount(robj *o) {
|
||||
if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT - 1) {
|
||||
o->refcount++;
|
||||
|
|
@ -629,6 +654,7 @@ void decrRefCount(robj *o) {
|
|||
case OBJ_HASH: freeHashObject(o); break;
|
||||
case OBJ_MODULE: freeModuleObject(o); break;
|
||||
case OBJ_STREAM: freeStreamObject(o); break;
|
||||
case OBJ_GCRA: freeGCRAObject(o); break;
|
||||
default: serverPanic("Unknown object type"); break;
|
||||
}
|
||||
}
|
||||
|
|
@ -691,8 +717,7 @@ void dismissSetObject(robj *o, size_t size_hint) {
|
|||
}
|
||||
|
||||
/* Dismiss hash table memory. */
|
||||
dismissMemory(set->ht_table[0], DICTHT_SIZE(set->ht_size_exp[0])*sizeof(dictEntry*));
|
||||
dismissMemory(set->ht_table[1], DICTHT_SIZE(set->ht_size_exp[1])*sizeof(dictEntry*));
|
||||
dismissDictBucketsMemory(set);
|
||||
} else if (o->encoding == OBJ_ENCODING_INTSET) {
|
||||
dismissMemory(o->ptr, intsetBlobLen((intset*)o->ptr));
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
|
|
@ -720,9 +745,7 @@ void dismissZsetObject(robj *o, size_t size_hint) {
|
|||
}
|
||||
|
||||
/* Dismiss hash table memory. */
|
||||
dict *d = zs->dict;
|
||||
dismissMemory(d->ht_table[0], DICTHT_SIZE(d->ht_size_exp[0])*sizeof(dictEntry*));
|
||||
dismissMemory(d->ht_table[1], DICTHT_SIZE(d->ht_size_exp[1])*sizeof(dictEntry*));
|
||||
dismissDictBucketsMemory(zs->dict);
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
dismissMemory(o->ptr, lpBytes((unsigned char*)o->ptr));
|
||||
} else {
|
||||
|
|
@ -748,8 +771,7 @@ void dismissHashObject(robj *o, size_t size_hint) {
|
|||
}
|
||||
|
||||
/* Dismiss hash table memory. */
|
||||
dismissMemory(d->ht_table[0], DICTHT_SIZE(d->ht_size_exp[0])*sizeof(dictEntry*));
|
||||
dismissMemory(d->ht_table[1], DICTHT_SIZE(d->ht_size_exp[1])*sizeof(dictEntry*));
|
||||
dismissDictBucketsMemory(d);
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
dismissMemory(o->ptr, lpBytes((unsigned char*)o->ptr));
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
|
|
@ -780,6 +802,13 @@ void dismissStreamObject(robj *o, size_t size_hint) {
|
|||
}
|
||||
}
|
||||
|
||||
void dismissGCRAObject(robj *o, size_t size_hint) {
|
||||
/* GCRA is a single allocation of a long long thus way smaller than a
|
||||
* page-size. The dismiss mechanism is not needed for it - hence NOOP.*/
|
||||
(void)o;
|
||||
(void)size_hint;
|
||||
}
|
||||
|
||||
/* When creating a snapshot in a fork child process, the main process and child
|
||||
* process share the same physical memory pages, and if / when the parent
|
||||
* modifies any keys due to write traffic, it'll cause CoW which consume
|
||||
|
|
@ -808,6 +837,7 @@ void dismissObject(robj *o, size_t size_hint) {
|
|||
case OBJ_ZSET: dismissZsetObject(o, size_hint); break;
|
||||
case OBJ_HASH: dismissHashObject(o, size_hint); break;
|
||||
case OBJ_STREAM: dismissStreamObject(o, size_hint); break;
|
||||
case OBJ_GCRA: dismissGCRAObject(o, size_hint); break;
|
||||
default: break;
|
||||
}
|
||||
#else
|
||||
|
|
@ -929,6 +959,7 @@ size_t getObjectLength(robj *o) {
|
|||
case OBJ_ZSET: return zsetLength(o);
|
||||
case OBJ_HASH: return hashTypeLength(o, 0);
|
||||
case OBJ_STREAM: return streamLength(o);
|
||||
case OBJ_GCRA: return gcraObjectLength(o);
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1137,6 +1168,22 @@ int getLongLongFromObject(robj *o, long long *target) {
|
|||
return C_OK;
|
||||
}
|
||||
|
||||
int getLongLongFromGCRAObject(robj *o, long long *target) {
|
||||
long long res;
|
||||
serverAssertWithInfo(NULL, o, o->type == OBJ_GCRA);
|
||||
serverAssert(o->encoding == OBJ_ENCODING_INT);
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
res = *((long long*)o->ptr);
|
||||
#else
|
||||
res = (long long)o->ptr;
|
||||
#endif
|
||||
if (unlikely(res < 0)) {
|
||||
serverPanic("Invalid negative GCRA value");
|
||||
}
|
||||
*target = res;
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
int getLongLongFromObjectOrReply(client *c, robj *o, long long *target, const char *msg) {
|
||||
long long value;
|
||||
if (getLongLongFromObject(o, &value) != C_OK) {
|
||||
|
|
@ -1227,7 +1274,8 @@ size_t kvobjComputeSize(robj *key, kvobj *o, size_t sample_size, int dbid) {
|
|||
o->type == OBJ_SET ||
|
||||
o->type == OBJ_ZSET ||
|
||||
o->type == OBJ_HASH ||
|
||||
o->type == OBJ_STREAM)
|
||||
o->type == OBJ_STREAM ||
|
||||
o->type == OBJ_GCRA)
|
||||
{
|
||||
return kvobjAllocSize(o);
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
|
|
@ -1253,12 +1301,31 @@ size_t kvobjAllocSize(kvobj *o) {
|
|||
} else if (o->type == OBJ_STREAM) {
|
||||
stream *s = o->ptr;
|
||||
asize += s->alloc_size;
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
asize += gcraTypeAllocSize(o);
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
/* TODO: Provide moduleGetAllocSize() module API for O(1) allocation size retrieval */
|
||||
}
|
||||
return asize;
|
||||
}
|
||||
|
||||
size_t gcraTypeAllocSize(robj *o) {
|
||||
(void)o;
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
return sizeof(long long);
|
||||
#else
|
||||
/* Same as string with int encoding there is no allocation as the value is
|
||||
* cast to void* and stored in o->ptr */
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The gcra object is a single long long value */
|
||||
size_t gcraObjectLength(robj *o) {
|
||||
(void)o;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Release data obtained with getMemoryOverheadData(). */
|
||||
void freeMemoryOverheadData(struct redisMemOverhead *mh) {
|
||||
zfree(mh->db);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* values of different logical types (strings, lists, sets, hashes, sorted sets,
|
||||
* streams, modules, ...). It contains:
|
||||
* - type: one of OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH, OBJ_STREAM,
|
||||
* OBJ_MODULE, ...
|
||||
* OBJ_GCRA, OBJ_MODULE, ...
|
||||
* - encoding: an implementation detail of how the value is represented in
|
||||
* memory for the given type (see OBJ_ENCODING_* below). For example,
|
||||
* strings may be RAW/EMBSTR/INT, sets may be INTSET or HT, etc.
|
||||
|
|
@ -161,6 +161,7 @@ robj *createHashObject(void);
|
|||
robj *createZsetObject(void);
|
||||
robj *createZsetListpackObject(void);
|
||||
robj *createStreamObject(void);
|
||||
robj *createGCRAObject(long long value);
|
||||
robj *createModuleObject(struct RedisModuleType *mt, void *value);
|
||||
int getLongFromObjectOrReply(struct client *c, robj *o, long *target, const char *msg);
|
||||
int getPositiveLongFromObjectOrReply(struct client *c, robj *o, long *target, const char *msg);
|
||||
|
|
@ -170,6 +171,7 @@ int getLongLongFromObjectOrReply(struct client *c, robj *o, long long *target, c
|
|||
int getDoubleFromObjectOrReply(struct client *c, robj *o, double *target, const char *msg);
|
||||
int getDoubleFromObject(const robj *o, double *target);
|
||||
int getLongLongFromObject(robj *o, long long *target);
|
||||
int getLongLongFromGCRAObject(robj *o, long long *target);
|
||||
int getLongDoubleFromObject(robj *o, long double *target);
|
||||
int getLongDoubleFromObjectOrReply(struct client *c, robj *o, long double *target, const char *msg);
|
||||
int getIntFromObjectOrReply(struct client *c, robj *o, int *target, const char *msg);
|
||||
|
|
@ -179,6 +181,8 @@ int collateStringObjects(const robj *a, const robj *b);
|
|||
int equalStringObjects(robj *a, robj *b);
|
||||
void trimStringObjectIfNeeded(robj *o, int trim_small_values);
|
||||
size_t kvobjAllocSize(kvobj *o);
|
||||
size_t gcraTypeAllocSize(robj *o);
|
||||
size_t gcraObjectLength(robj *o);
|
||||
|
||||
int objectSetLRUOrLFU(robj *val, long long lfu_freq, long long lru_idle,
|
||||
long long lru_clock, int lru_multiplier);
|
||||
|
|
|
|||
25
src/rdb.c
25
src/rdb.c
|
|
@ -713,6 +713,8 @@ int rdbSaveObjectType(rio *rdb, robj *o) {
|
|||
serverPanic("Unknown hash encoding");
|
||||
case OBJ_STREAM:
|
||||
return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_5);
|
||||
case OBJ_GCRA:
|
||||
return rdbSaveType(rdb,RDB_TYPE_GCRA);
|
||||
case OBJ_MODULE:
|
||||
return rdbSaveType(rdb,RDB_TYPE_MODULE_2);
|
||||
default:
|
||||
|
|
@ -1399,6 +1401,11 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid) {
|
|||
/* Save the all-time count of duplicate IIDs detected. */
|
||||
if ((n = rdbSaveLen(rdb,s->iids_duplicates)) == -1) return -1;
|
||||
nwritten += n;
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
long long t;
|
||||
getLongLongFromGCRAObject(o, &t);
|
||||
if ((n = rdbSaveLen(rdb,t)) == -1) return -1;
|
||||
nwritten += n;
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
/* Save a module-specific value. */
|
||||
RedisModuleIO io;
|
||||
|
|
@ -1680,6 +1687,10 @@ ssize_t rdbSaveDb(rio *rdb, int dbid, int rdbflags, long *key_counter, unsigned
|
|||
written += res;
|
||||
if ((res = rdbSaveLen(rdb, kvstoreDictSize(db->expires, curr_slot))) < 0) goto werr2;
|
||||
written += res;
|
||||
/* Dismiss bucket arrays of the previous slot to reduce CoW.
|
||||
* The final slot is not dismissed since the child exits shortly after. */
|
||||
if (server.in_fork_child && last_slot != -1)
|
||||
dismissDictBucketsMemory(kvstoreGetDict(db->keys, last_slot));
|
||||
last_slot = curr_slot;
|
||||
}
|
||||
kvobj *kv = dictGetKV(de);
|
||||
|
|
@ -1707,7 +1718,8 @@ ssize_t rdbSaveDb(rio *rdb, int dbid, int rdbflags, long *key_counter, unsigned
|
|||
* OS and possibly avoid or decrease COW. We give the dismiss
|
||||
* mechanism a hint about an estimated size of the object we stored. */
|
||||
size_t dump_size = rdb->processed_bytes - rdb_bytes_before_key;
|
||||
if (server.in_fork_child) dismissObject(kv, dump_size);
|
||||
if (server.in_fork_child && dump_size > server.page_size/2)
|
||||
dismissObject(kv, dump_size);
|
||||
|
||||
/* Update child info every 1 second (approximately).
|
||||
* in order to avoid calling mstime() on each iteration, we will
|
||||
|
|
@ -1758,6 +1770,10 @@ int rdbSaveRio(int req, rio *rdb, int *error, int rdbflags, rdbSaveInfo *rsi) {
|
|||
if (!(req & SLAVE_REQ_RDB_EXCLUDE_DATA)) {
|
||||
for (j = 0; j < server.dbnum; j++) {
|
||||
if (rdbSaveDb(rdb, j, rdbflags, &key_counter, &skipped) == -1) goto werr;
|
||||
/* In standalone mode, dismiss bucket arrays of the saved DB's
|
||||
* kvstore to reduce CoW. In cluster mode this is done per-slot. */
|
||||
if (server.in_fork_child && !server.cluster_enabled)
|
||||
dismissKvstoreBucketsMemory(server.db[j].keys);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3595,6 +3611,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
return NULL;
|
||||
}
|
||||
o = createModuleObject(mt, ptr);
|
||||
} else if (rdbtype == RDB_TYPE_GCRA) {
|
||||
uint64_t time = rdbLoadLen(rdb, NULL);
|
||||
if (time == RDB_LENERR || time > LLONG_MAX) {
|
||||
rdbReportReadError("Failed loading GCRA TaT value");
|
||||
return NULL;
|
||||
}
|
||||
o = createGCRAObject((long long)time);
|
||||
} else {
|
||||
rdbReportReadError("Unknown RDB encoding type %d",rdbtype);
|
||||
return NULL;
|
||||
|
|
|
|||
|
|
@ -80,10 +80,11 @@
|
|||
#define RDB_TYPE_HASH_LISTPACK_EX 25 /* Hash LP with HFEs. Attach min TTL at start */
|
||||
#define RDB_TYPE_STREAM_LISTPACKS_4 26 /* Stream with IDMP support */
|
||||
#define RDB_TYPE_STREAM_LISTPACKS_5 27 /* Stream with XNACK support (NACKed entries) */
|
||||
#define RDB_TYPE_GCRA 28 /* GCRA object */
|
||||
/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType(), and rdb_type_string[] */
|
||||
|
||||
/* Test if a type is an object type. */
|
||||
#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 27))
|
||||
#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 28))
|
||||
|
||||
/* Special RDB opcodes (saved/loaded with rdbSaveType/rdbLoadType). */
|
||||
#define RDB_OPCODE_KEY_META 243 /* Key metadata (module metadata classes). */
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ char *rdb_type_string[] = {
|
|||
"hash-listpack-md",
|
||||
"stream-v4",
|
||||
"stream-v5",
|
||||
"gcra",
|
||||
};
|
||||
|
||||
/* Show a few stats collected into 'rdbstate' */
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ typedef long long ustime_t;
|
|||
#define REDISMODULE_KEYTYPE_ZSET 5
|
||||
#define REDISMODULE_KEYTYPE_MODULE 6
|
||||
#define REDISMODULE_KEYTYPE_STREAM 7
|
||||
#define REDISMODULE_KEYTYPE_GCRA 8
|
||||
|
||||
/* Reply types. */
|
||||
#define REDISMODULE_REPLY_UNKNOWN -1
|
||||
|
|
@ -247,11 +248,12 @@ This flag should not be used directly by the module.
|
|||
#define REDISMODULE_NOTIFY_OVERWRITTEN (1<<15) /* o, key overwrite notification */
|
||||
#define REDISMODULE_NOTIFY_TYPE_CHANGED (1<<16) /* c, key type changed notification */
|
||||
#define REDISMODULE_NOTIFY_KEY_TRIMMED (1<<17) /* module only key space notification, indicates a key trimmed during slot migration */
|
||||
#define REDISMODULE_NOTIFY_RATE_LIMIT (1<<18) /* r, rate limit event */
|
||||
|
||||
/* Next notification flag, must be updated when adding new flags above!
|
||||
This flag should not be used directly by the module.
|
||||
* Use RedisModule_GetKeyspaceNotificationFlagsAll instead. */
|
||||
#define _REDISMODULE_NOTIFY_NEXT (1<<18)
|
||||
#define _REDISMODULE_NOTIFY_NEXT (1<<19)
|
||||
|
||||
#define REDISMODULE_NOTIFY_ALL (REDISMODULE_NOTIFY_GENERIC | REDISMODULE_NOTIFY_STRING | REDISMODULE_NOTIFY_LIST | REDISMODULE_NOTIFY_SET | REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_ZSET | REDISMODULE_NOTIFY_EXPIRED | REDISMODULE_NOTIFY_EVICTED | REDISMODULE_NOTIFY_STREAM | REDISMODULE_NOTIFY_MODULE) /* A */
|
||||
|
||||
|
|
|
|||
|
|
@ -376,23 +376,6 @@ int prepareReplicasToWrite(void) {
|
|||
return prepared;
|
||||
}
|
||||
|
||||
/* Wrapper for feedReplicationBuffer() that takes Redis string objects
|
||||
* as input. */
|
||||
void feedReplicationBufferWithObject(robj *o) {
|
||||
char llstr[LONG_STR_SIZE];
|
||||
void *p;
|
||||
size_t len;
|
||||
|
||||
if (o->encoding == OBJ_ENCODING_INT) {
|
||||
len = ll2string(llstr,sizeof(llstr),(long)o->ptr);
|
||||
p = llstr;
|
||||
} else {
|
||||
len = sdslen(o->ptr);
|
||||
p = o->ptr;
|
||||
}
|
||||
feedReplicationBuffer(p,len);
|
||||
}
|
||||
|
||||
/* Generally, we only have one replication buffer block to trim when replication
|
||||
* backlog size exceeds our setting and no replica reference it. But if replica
|
||||
* clients disconnect, we need to free many replication buffer blocks that are
|
||||
|
|
@ -468,115 +451,175 @@ void freeReplicaReferencedReplBuffer(client *replica) {
|
|||
replica->ref_block_pos = 0;
|
||||
}
|
||||
|
||||
/* Append bytes into the global replication buffer list, replication backlog and
|
||||
* all replica clients use replication buffers collectively, this function replace
|
||||
* 'addReply*', 'feedReplicationBacklog' for replicas and replication backlog,
|
||||
* First we add buffer into global replication buffer block list, and then
|
||||
* update replica / replication-backlog referenced node and block position. */
|
||||
void feedReplicationBuffer(char *s, size_t len) {
|
||||
/* Batched write API for the global replication backlog, optimized for minimal
|
||||
* overhead per append: data writes are just memcpys into the tail block.
|
||||
* All bookkeeping is deferred to replBufWriterEnd(). */
|
||||
typedef struct replBufWriter {
|
||||
listNode *start_node; /* First repl buffer block written to. */
|
||||
size_t start_pos; /* Byte offset within start_node where writing began. */
|
||||
size_t total_len; /* Total bytes written across all writes. */
|
||||
int new_blocks; /* Number of new blocks allocated during this stream. */
|
||||
replBufBlock *tail; /* Current tail block. */
|
||||
} replBufWriter;
|
||||
|
||||
/* Initialize the writer, cache the current tail position. */
|
||||
static void replBufWriterBegin(replBufWriter *wr) {
|
||||
listNode *ln = listLast(server.repl_buffer_blocks);
|
||||
replBufBlock *tail = ln ? listNodeValue(ln) : NULL;
|
||||
|
||||
if (tail && tail->used < tail->size) {
|
||||
wr->start_node = ln;
|
||||
wr->start_pos = tail->used;
|
||||
} else {
|
||||
wr->start_node = NULL;
|
||||
wr->start_pos = 0;
|
||||
}
|
||||
|
||||
wr->total_len = 0;
|
||||
wr->new_blocks = 0;
|
||||
wr->tail = tail;
|
||||
}
|
||||
|
||||
/* Allocate a new replication backlog block. Called when current block is full. */
|
||||
static void replBufWriterAllocBlock(replBufWriter *wr, size_t hint) {
|
||||
static long long repl_block_id = 0;
|
||||
size_t usable_size;
|
||||
/* Avoid creating nodes smaller than PROTO_REPLY_CHUNK_BYTES, so that we can append more data into them,
|
||||
* and also avoid creating nodes bigger than repl_backlog_size / 16, so that we won't have huge nodes that can't
|
||||
* trim when we only still need to hold a small portion from them. */
|
||||
size_t limit = max((size_t)server.repl_backlog_size / 16, (size_t)PROTO_REPLY_CHUNK_BYTES);
|
||||
size_t bsize = min(max(hint, (size_t)PROTO_REPLY_CHUNK_BYTES), limit);
|
||||
replBufBlock *tail = zmalloc_usable(bsize + sizeof(replBufBlock), &usable_size);
|
||||
/* Take over the allocation's internal fragmentation */
|
||||
tail->size = usable_size - sizeof(replBufBlock);
|
||||
tail->used = 0;
|
||||
tail->refcount = 0;
|
||||
tail->repl_offset = server.master_repl_offset + wr->total_len + 1;
|
||||
tail->id = repl_block_id++;
|
||||
listAddNodeTail(server.repl_buffer_blocks, tail);
|
||||
server.repl_buffer_mem += (usable_size + sizeof(listNode));
|
||||
createReplicationBacklogIndex(listLast(server.repl_buffer_blocks));
|
||||
|
||||
if (server.repl_backlog == NULL) return;
|
||||
/* Update stream state. */
|
||||
wr->tail = tail;
|
||||
wr->new_blocks++;
|
||||
if (wr->start_node == NULL) {
|
||||
wr->start_node = listLast(server.repl_buffer_blocks);
|
||||
wr->start_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
clusterSlotStatsIncrNetworkBytesOutForReplication(len);
|
||||
/* Slow path: fill remainder of current block + allocate as needed. */
|
||||
static void replBufWriterAppendSlow(replBufWriter *wr, const char *buf, size_t len) {
|
||||
while (len > 0) {
|
||||
size_t avail = wr->tail ? wr->tail->size - wr->tail->used : 0;
|
||||
if (avail > 0) {
|
||||
size_t copy = (avail >= len) ? len : avail;
|
||||
memcpy(wr->tail->buf + wr->tail->used, buf, copy);
|
||||
wr->tail->used += copy;
|
||||
wr->total_len += copy;
|
||||
buf += copy;
|
||||
len -= copy;
|
||||
}
|
||||
|
||||
if (len > 0)
|
||||
replBufWriterAllocBlock(wr, len);
|
||||
}
|
||||
}
|
||||
|
||||
/* Write data into the replication buffer. The slow path is split out to give
|
||||
* the compiler a chance to inline the common case where the write fits entirely
|
||||
* in the current block. */
|
||||
static inline void replBufWriterAppend(replBufWriter *wr, const char *buf, size_t len) {
|
||||
size_t avail = wr->tail ? wr->tail->size - wr->tail->used : 0;
|
||||
if (len > 0 && avail >= len) {
|
||||
memcpy(wr->tail->buf + wr->tail->used, buf, len);
|
||||
wr->tail->used += len;
|
||||
wr->total_len += len;
|
||||
return;
|
||||
}
|
||||
replBufWriterAppendSlow(wr, buf, len);
|
||||
}
|
||||
|
||||
/* Write a RESP header prefix<value>\r\n (e.g. "$12\r\n" or "*3\r\n").
|
||||
* Uses pre-built shared objects for small values, formats manually otherwise. */
|
||||
static inline void replBufWriterAppendBulkLen(replBufWriter *wr, char prefix, long long value) {
|
||||
serverAssert(prefix == '$' || prefix == '*');
|
||||
if (value >= 0 && value < OBJ_SHARED_BULKHDR_LEN) {
|
||||
robj **tbl = (prefix == '$') ? shared.bulkhdr : shared.mbulkhdr;
|
||||
replBufWriterAppend(wr, tbl[value]->ptr, OBJ_SHARED_HDR_STRLEN(value));
|
||||
return;
|
||||
}
|
||||
char buf[LONG_STR_SIZE+3];
|
||||
buf[0] = prefix;
|
||||
int len = ll2string(buf+1, sizeof(buf)-1, value);
|
||||
buf[len+1] = '\r';
|
||||
buf[len+2] = '\n';
|
||||
replBufWriterAppend(wr, buf, len+3);
|
||||
}
|
||||
|
||||
|
||||
/* Finalize the replication buffer write: update global offsets, set up replica
|
||||
* references for new data, check output buffer limits, and trim the
|
||||
* backlog if new blocks were allocated. */
|
||||
static void replBufWriterEnd(replBufWriter *wr) {
|
||||
if (wr->total_len == 0) return;
|
||||
|
||||
serverAssert(wr->start_node != NULL);
|
||||
clusterSlotStatsIncrNetworkBytesOutForReplication(wr->total_len);
|
||||
|
||||
/* Update the current cmd's keys with the commands replication bytes*/
|
||||
hotkeyMetrics metrics = {0, len};
|
||||
hotkeyMetrics metrics = {0, wr->total_len};
|
||||
hotkeyStatsUpdateCurrentCmd(server.hotkeys, metrics);
|
||||
|
||||
while(len > 0) {
|
||||
size_t start_pos = 0; /* The position of referenced block to start sending. */
|
||||
listNode *start_node = NULL; /* Replica/backlog starts referenced node. */
|
||||
int add_new_block = 0; /* Create new block if current block is total used. */
|
||||
listNode *ln = listLast(server.repl_buffer_blocks);
|
||||
replBufBlock *tail = ln ? listNodeValue(ln) : NULL;
|
||||
server.master_repl_offset += wr->total_len;
|
||||
server.repl_backlog->histlen += wr->total_len;
|
||||
|
||||
/* Append to tail string when possible. */
|
||||
if (tail && tail->size > tail->used) {
|
||||
start_node = listLast(server.repl_buffer_blocks);
|
||||
start_pos = tail->used;
|
||||
/* Copy the part we can fit into the tail, and leave the rest for a
|
||||
* new node */
|
||||
size_t avail = tail->size - tail->used;
|
||||
size_t copy = (avail >= len) ? len : avail;
|
||||
memcpy(tail->buf + tail->used, s, copy);
|
||||
tail->used += copy;
|
||||
s += copy;
|
||||
len -= copy;
|
||||
server.master_repl_offset += copy;
|
||||
server.repl_backlog->histlen += copy;
|
||||
}
|
||||
if (len) {
|
||||
/* Create a new node, make sure it is allocated to at
|
||||
* least PROTO_REPLY_CHUNK_BYTES */
|
||||
size_t usable_size;
|
||||
/* Avoid creating nodes smaller than PROTO_REPLY_CHUNK_BYTES, so that we can append more data into them,
|
||||
* and also avoid creating nodes bigger than repl_backlog_size / 16, so that we won't have huge nodes that can't
|
||||
* trim when we only still need to hold a small portion from them. */
|
||||
size_t limit = max((size_t)server.repl_backlog_size / 16, (size_t)PROTO_REPLY_CHUNK_BYTES);
|
||||
size_t size = min(max(len, (size_t)PROTO_REPLY_CHUNK_BYTES), limit);
|
||||
tail = zmalloc_usable(size + sizeof(replBufBlock), &usable_size);
|
||||
/* Take over the allocation's internal fragmentation */
|
||||
tail->size = usable_size - sizeof(replBufBlock);
|
||||
size_t copy = (tail->size >= len) ? len : tail->size;
|
||||
tail->used = copy;
|
||||
tail->refcount = 0;
|
||||
tail->repl_offset = server.master_repl_offset + 1;
|
||||
tail->id = repl_block_id++;
|
||||
memcpy(tail->buf, s, copy);
|
||||
listAddNodeTail(server.repl_buffer_blocks, tail);
|
||||
/* We also count the list node memory into replication buffer memory. */
|
||||
server.repl_buffer_mem += (usable_size + sizeof(listNode));
|
||||
add_new_block = 1;
|
||||
if (start_node == NULL) {
|
||||
start_node = listLast(server.repl_buffer_blocks);
|
||||
start_pos = 0;
|
||||
}
|
||||
s += copy;
|
||||
len -= copy;
|
||||
server.master_repl_offset += copy;
|
||||
server.repl_backlog->histlen += copy;
|
||||
}
|
||||
/* For output buffer of replicas. */
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(server.slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *slave = ln->value;
|
||||
if (!canFeedReplicaReplBuffer(slave)) continue;
|
||||
|
||||
/* For output buffer of replicas. */
|
||||
listIter li;
|
||||
listRewind(server.slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *slave = ln->value;
|
||||
if (!canFeedReplicaReplBuffer(slave)) continue;
|
||||
|
||||
/* Update shared replication buffer start position. */
|
||||
if (slave->ref_repl_buf_node == NULL) {
|
||||
slave->ref_repl_buf_node = start_node;
|
||||
slave->ref_block_pos = start_pos;
|
||||
/* Only increase the start block reference count. */
|
||||
((replBufBlock *)listNodeValue(start_node))->refcount++;
|
||||
}
|
||||
|
||||
/* Check output buffer limit only when add new block. */
|
||||
if (add_new_block) closeClientOnOutputBufferLimitReached(slave, 1);
|
||||
}
|
||||
|
||||
/* For replication backlog */
|
||||
if (server.repl_backlog->ref_repl_buf_node == NULL) {
|
||||
server.repl_backlog->ref_repl_buf_node = start_node;
|
||||
/* Update shared replication buffer start position. */
|
||||
if (slave->ref_repl_buf_node == NULL) {
|
||||
slave->ref_repl_buf_node = wr->start_node;
|
||||
slave->ref_block_pos = wr->start_pos;
|
||||
/* Only increase the start block reference count. */
|
||||
((replBufBlock *)listNodeValue(start_node))->refcount++;
|
||||
|
||||
/* Replication buffer must be empty before adding replication stream
|
||||
* into replication backlog. */
|
||||
serverAssert(add_new_block == 1 && start_pos == 0);
|
||||
((replBufBlock *)listNodeValue(wr->start_node))->refcount++;
|
||||
}
|
||||
if (add_new_block) {
|
||||
createReplicationBacklogIndex(listLast(server.repl_buffer_blocks));
|
||||
|
||||
/* It is important to trim after adding replication data to keep the backlog size close to
|
||||
* repl_backlog_size in the common case. We wait until we add a new block to avoid repeated
|
||||
* unnecessary trimming attempts when small amounts of data are added. See comments in
|
||||
* freeMemoryGetNotCountedMemory() for details on replication backlog memory tracking. */
|
||||
incrementalTrimReplicationBacklog(REPL_BACKLOG_TRIM_BLOCKS_PER_CALL);
|
||||
}
|
||||
/* Check output buffer limit only when new blocks were added. */
|
||||
if (wr->new_blocks) closeClientOnOutputBufferLimitReached(slave, 1);
|
||||
}
|
||||
|
||||
/* For replication backlog */
|
||||
if (server.repl_backlog->ref_repl_buf_node == NULL) {
|
||||
server.repl_backlog->ref_repl_buf_node = wr->start_node;
|
||||
/* Only increase the start block reference count. */
|
||||
((replBufBlock *)listNodeValue(wr->start_node))->refcount++;
|
||||
|
||||
/* Replication buffer must be empty before adding replication stream
|
||||
* into replication backlog. */
|
||||
serverAssert(wr->new_blocks > 0 && wr->start_pos == 0);
|
||||
}
|
||||
if (wr->new_blocks) {
|
||||
/* It is important to trim after adding replication data to keep the backlog size close to
|
||||
* repl_backlog_size in the common case. We wait until we add a new block to avoid repeated
|
||||
* unnecessary trimming attempts when small amounts of data are added. See comments in
|
||||
* freeMemoryGetNotCountedMemory() for details on replication backlog memory tracking. */
|
||||
incrementalTrimReplicationBacklog(REPL_BACKLOG_TRIM_BLOCKS_PER_CALL);
|
||||
}
|
||||
}
|
||||
|
||||
/* Append bytes into the global replication buffer. */
|
||||
static void feedReplicationBuffer(const char *buf, size_t len) {
|
||||
replBufWriter wr;
|
||||
replBufWriterBegin(&wr);
|
||||
replBufWriterAppend(&wr, buf, len);
|
||||
replBufWriterEnd(&wr);
|
||||
}
|
||||
|
||||
/* Propagate write commands to replication stream.
|
||||
|
|
@ -642,7 +685,7 @@ void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
|
|||
dictid_len, llstr));
|
||||
}
|
||||
|
||||
feedReplicationBufferWithObject(selectcmd);
|
||||
feedReplicationBuffer(selectcmd->ptr, sdslen(selectcmd->ptr));
|
||||
|
||||
/* Although the SELECT command is not associated with any slot,
|
||||
* its per-slot network-bytes-out accumulation is made by the above function call.
|
||||
|
|
@ -657,28 +700,28 @@ void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
|
|||
|
||||
/* Write the command to the replication buffer if any. */
|
||||
char aux[LONG_STR_SIZE+3];
|
||||
replBufWriter wr;
|
||||
replBufWriterBegin(&wr);
|
||||
|
||||
/* Add the multi bulk reply length. */
|
||||
aux[0] = '*';
|
||||
len = ll2string(aux+1,sizeof(aux)-1,argc);
|
||||
aux[len+1] = '\r';
|
||||
aux[len+2] = '\n';
|
||||
feedReplicationBuffer(aux,len+3);
|
||||
/* Write the multi bulk count */
|
||||
replBufWriterAppendBulkLen(&wr, '*', argc);
|
||||
|
||||
for (j = 0; j < argc; j++) {
|
||||
/* Write the bulk count */
|
||||
long objlen = stringObjectLen(argv[j]);
|
||||
replBufWriterAppendBulkLen(&wr, '$', objlen);
|
||||
|
||||
/* We need to feed the buffer with the object as a bulk reply
|
||||
* not just as a plain string, so create the $..CRLF payload len
|
||||
* and add the final CRLF */
|
||||
aux[0] = '$';
|
||||
len = ll2string(aux+1,sizeof(aux)-1,objlen);
|
||||
aux[len+1] = '\r';
|
||||
aux[len+2] = '\n';
|
||||
feedReplicationBuffer(aux,len+3);
|
||||
feedReplicationBufferWithObject(argv[j]);
|
||||
feedReplicationBuffer(aux+len+1,2);
|
||||
/* Write the bulk data */
|
||||
if (argv[j]->encoding == OBJ_ENCODING_INT) {
|
||||
len = ll2string(aux, sizeof(aux), (long)argv[j]->ptr);
|
||||
replBufWriterAppend(&wr, aux, len);
|
||||
} else {
|
||||
replBufWriterAppend(&wr, argv[j]->ptr, objlen);
|
||||
}
|
||||
replBufWriterAppend(&wr, "\r\n", 2);
|
||||
}
|
||||
|
||||
replBufWriterEnd(&wr);
|
||||
}
|
||||
|
||||
/* This is a debugging function that gets called when we detect something
|
||||
|
|
|
|||
|
|
@ -128,13 +128,10 @@ static int parseDouble(ReplyParser *parser, void *p_ctx) {
|
|||
const char *proto = parser->curr_location;
|
||||
char *p = strchr(proto+1,'\r');
|
||||
parser->curr_location = p + 2; /* for \r\n */
|
||||
char buf[MAX_LONG_DOUBLE_CHARS+1];
|
||||
size_t len = p-proto-1;
|
||||
double d;
|
||||
if (len <= MAX_LONG_DOUBLE_CHARS) {
|
||||
memcpy(buf,proto+1,len);
|
||||
buf[len] = '\0';
|
||||
d = fast_float_strtod(buf,NULL); /* We expect a valid representation. */
|
||||
d = fast_float_strtod(proto+1,len,NULL); /* We expect a valid representation. */
|
||||
} else {
|
||||
d = 0;
|
||||
}
|
||||
|
|
|
|||
41
src/server.c
41
src/server.c
|
|
@ -32,6 +32,7 @@
|
|||
#include "fwtree.h"
|
||||
#include "estore.h"
|
||||
#include "chk.h"
|
||||
#include "fast_float_strtod.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
|
|
@ -581,6 +582,19 @@ dictType objectKeyPointerValueDictType = {
|
|||
NULL /* allow to expand */
|
||||
};
|
||||
|
||||
/* Dict type with robj pointer keys and no values. */
|
||||
dictType objectKeyNoValueDictType = {
|
||||
dictEncObjHash, /* hash function */
|
||||
NULL, /* key dup */
|
||||
NULL, /* val dup */
|
||||
dictEncObjKeyCompare, /* key compare */
|
||||
dictObjectDestructor, /* key destructor */
|
||||
NULL, /* val destructor */
|
||||
NULL, /* allow to expand */
|
||||
.no_value = 1, /* no values in this dict */
|
||||
.keys_are_odd = 0, /* robj pointers are not odd */
|
||||
};
|
||||
|
||||
/* Like objectKeyPointerValueDictType(), but values can be destroyed, if
|
||||
* not NULL, calling zfree(). */
|
||||
dictType objectKeyHeapPointerValueDictType = {
|
||||
|
|
@ -2996,7 +3010,7 @@ void initServer(void) {
|
|||
server.db[j].blocking_keys = dictCreate(&keylistDictType);
|
||||
server.db[j].blocking_keys_unblock_on_nokey = dictCreate(&objectKeyPointerValueDictType);
|
||||
server.db[j].stream_claim_pending_keys = dictCreate(&objectKeyPointerValueDictType);
|
||||
server.db[j].stream_idmp_keys = dictCreate(&objectKeyPointerValueDictType);
|
||||
server.db[j].stream_idmp_keys = dictCreate(&objectKeyNoValueDictType);
|
||||
server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType);
|
||||
server.db[j].watched_keys = dictCreate(&keylistDictType);
|
||||
server.db[j].id = j;
|
||||
|
|
@ -7483,6 +7497,20 @@ void dismissClientMemory(client *c) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Dismiss the hash table bucket arrays of a dict. */
|
||||
void dismissDictBucketsMemory(dict *d) {
|
||||
if (!d) return;
|
||||
dismissMemory(d->ht_table[0], DICTHT_SIZE(d->ht_size_exp[0]) * sizeof(dictEntry*));
|
||||
dismissMemory(d->ht_table[1], DICTHT_SIZE(d->ht_size_exp[1]) * sizeof(dictEntry*));
|
||||
}
|
||||
|
||||
/* Dismiss the hash table bucket arrays for all dicts in the given kvstore. */
|
||||
void dismissKvstoreBucketsMemory(kvstore *kvs) {
|
||||
for (int didx = 0; didx < kvstoreNumDicts(kvs); didx++) {
|
||||
dismissDictBucketsMemory(kvstoreGetDict(kvs, didx));
|
||||
}
|
||||
}
|
||||
|
||||
/* In the child process, we don't need some buffers anymore, and these are
|
||||
* likely to change in the parent when there's heavy write traffic.
|
||||
* We dismiss them right away, to avoid CoW.
|
||||
|
|
@ -7521,6 +7549,14 @@ void dismissMemoryInChild(void) {
|
|||
client *c = listNodeValue(ln);
|
||||
dismissClientMemory(c);
|
||||
}
|
||||
|
||||
/* Dismiss expires kvstore bucket arrays since the child process never
|
||||
* accesses them, expire times are embedded in key objects. */
|
||||
if (server.in_fork_child == CHILD_TYPE_RDB || server.in_fork_child == CHILD_TYPE_AOF) {
|
||||
for (int dbid = 0; dbid < server.dbnum; dbid++) {
|
||||
dismissKvstoreBucketsMemory(server.db[dbid].expires);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -7784,6 +7820,7 @@ int __test_num = 0;
|
|||
typedef int redisTestProc(int argc, char **argv, int flags);
|
||||
int bitopsTest(int argc, char **argv, int flags);
|
||||
int zsetTest(int argc, char **argv, int flags);
|
||||
int vectorTest(int argc, char **argv, int flags);
|
||||
struct redisTest {
|
||||
char *name;
|
||||
redisTestProc *proc;
|
||||
|
|
@ -7807,11 +7844,13 @@ struct redisTest {
|
|||
{"fwtree", fwtreeTest},
|
||||
{"estore", estoreTest},
|
||||
{"ebuckets", ebucketsTest},
|
||||
{"vector", vectorTest},
|
||||
{"bitmap", bitopsTest},
|
||||
{"rax", raxTest},
|
||||
{"flax", flaxTest},
|
||||
{"zset", zsetTest},
|
||||
{"topk", chkTopKTest},
|
||||
{"fastfloat", fastFloatTest},
|
||||
};
|
||||
redisTestProc *getTestProcByName(const char *name) {
|
||||
int numtests = sizeof(redisTests)/sizeof(struct redisTest);
|
||||
|
|
|
|||
23
src/server.h
23
src/server.h
|
|
@ -288,6 +288,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
|
|||
#define ACL_CATEGORY_CONNECTION (1ULL<<18)
|
||||
#define ACL_CATEGORY_TRANSACTION (1ULL<<19)
|
||||
#define ACL_CATEGORY_SCRIPTING (1ULL<<20)
|
||||
#define ACL_CATEGORY_RATE_LIMIT (1ULL<<21)
|
||||
|
||||
/* Key-spec flags *
|
||||
* -------------- */
|
||||
|
|
@ -796,6 +797,7 @@ typedef enum {
|
|||
#define NOTIFY_OVERWRITTEN (1<<15) /* o, key overwrite notification (Note: excluded from NOTIFY_ALL) */
|
||||
#define NOTIFY_TYPE_CHANGED (1<<16) /* c, key type changed notification (Note: excluded from NOTIFY_ALL) */
|
||||
#define NOTIFY_KEY_TRIMMED (1<<17) /* module only key space notification, indicates a key trimmed during slot migration */
|
||||
#define NOTIFY_RATE_LIMIT (1<<18) /* r, notify rate limit event (Note: excluded from NOTIFY_ALL)*/
|
||||
#define NOTIFY_ALL (NOTIFY_GENERIC | NOTIFY_STRING | NOTIFY_LIST | NOTIFY_SET | NOTIFY_HASH | NOTIFY_ZSET | NOTIFY_EXPIRED | NOTIFY_EVICTED | NOTIFY_STREAM | NOTIFY_MODULE) /* A flag */
|
||||
|
||||
/* Using the following macro you can run code inside serverCron() with the
|
||||
|
|
@ -860,7 +862,17 @@ typedef enum {
|
|||
* encoding version. */
|
||||
#define OBJ_MODULE 5 /* Module object. */
|
||||
#define OBJ_STREAM 6 /* Stream object. */
|
||||
#define OBJ_TYPE_MAX 7 /* Maximum number of object types */
|
||||
#define OBJ_GCRA 7 /* GCRA object. */
|
||||
#define OBJ_TYPE_MAX 8 /* Maximum number of object types */
|
||||
|
||||
/* NOTE: adding a new object requires changes in the following places:
|
||||
* - rdb.c - save/load (also bump RDB_VERSION if needed)
|
||||
* - aof.c - rewrite
|
||||
* - db.c - obj_type_name, copyCommand
|
||||
* - debug.c - xorObjectDigest, serverLogObjectDebugInfo
|
||||
* - defrag.c - defragKey
|
||||
* - module.c - RM_KeyType (and add the new keytype to redismodule.h)
|
||||
* - object.c - object(create/free/dismiss/allocSize/Length) */
|
||||
|
||||
/* Extract encver / signature from a module type ID. */
|
||||
#define REDISMODULE_TYPE_ENCVER_BITS 10
|
||||
|
|
@ -2772,6 +2784,7 @@ typedef enum {
|
|||
COMMAND_GROUP_STREAM,
|
||||
COMMAND_GROUP_BITMAP,
|
||||
COMMAND_GROUP_MODULE,
|
||||
COMMAND_GROUP_RATE_LIMIT,
|
||||
} redisCommandGroup;
|
||||
|
||||
typedef void redisCommandProc(client *c);
|
||||
|
|
@ -3005,6 +3018,7 @@ typedef struct {
|
|||
extern struct redisServer server;
|
||||
extern struct sharedObjectsStruct shared;
|
||||
extern dictType objectKeyPointerValueDictType;
|
||||
extern dictType objectKeyNoValueDictType;
|
||||
extern dictType objectKeyHeapPointerValueDictType;
|
||||
extern dictType setDictType;
|
||||
extern dictType BenchmarkDictType;
|
||||
|
|
@ -3360,7 +3374,6 @@ ssize_t syncReadLine(int fd, char *ptr, ssize_t size, long long timeout);
|
|||
void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc);
|
||||
void replicationFeedStreamFromMasterStream(char *buf, size_t buflen);
|
||||
void resetReplicationBuffer(void);
|
||||
void feedReplicationBuffer(char *buf, size_t len);
|
||||
void freeReplicaReferencedReplBuffer(client *replica);
|
||||
void replicationFeedMonitors(client *c, list *monitors, int dictid, robj **argv, int argc);
|
||||
void updateSlavesWaitingBgsave(int bgsaveerr, int type);
|
||||
|
|
@ -3598,6 +3611,9 @@ int zzlLexValueLteMax(unsigned char *p, zlexrangespec *spec);
|
|||
int zslLexValueGteMin(sds value, zlexrangespec *spec);
|
||||
int zslLexValueLteMax(sds value, zlexrangespec *spec);
|
||||
|
||||
/* gcra related */
|
||||
robj *gcraDup(robj *o);
|
||||
|
||||
/* Core functions */
|
||||
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level);
|
||||
void updatePeakMemory(void);
|
||||
|
|
@ -3682,6 +3698,8 @@ void activeDefragFreeRaw(void *ptr);
|
|||
robj *activeDefragStringOb(robj* ob);
|
||||
void dismissSds(sds s);
|
||||
void dismissMemory(void* ptr, size_t size_hint);
|
||||
void dismissDictBucketsMemory(dict *d);
|
||||
void dismissKvstoreBucketsMemory(kvstore *kvs);
|
||||
void dismissMemoryInChild(void);
|
||||
int clientsCronRunClient(client *c);
|
||||
|
||||
|
|
@ -4468,6 +4486,7 @@ void resetCommand(client *c);
|
|||
void failoverCommand(client *c);
|
||||
void digestCommand(client *c);
|
||||
void gcraCommand(client *c);
|
||||
void gcraSetValueCommand(client *c);
|
||||
|
||||
#if defined(__GNUC__)
|
||||
void *calloc(size_t count, size_t size) __attribute__ ((deprecated));
|
||||
|
|
|
|||
|
|
@ -518,12 +518,7 @@ void sortCommandGeneric(client *c, int readonly) {
|
|||
if (sortby) vector[j].u.cmpobj = getDecodedObject(byval);
|
||||
} else {
|
||||
if (sdsEncodedObject(byval)) {
|
||||
char *eptr;
|
||||
|
||||
vector[j].u.score = fast_float_strtod(byval->ptr,&eptr);
|
||||
if (eptr[0] != '\0' || errno == ERANGE ||
|
||||
isnan(vector[j].u.score))
|
||||
{
|
||||
if (string2d(byval->ptr,sdslen(byval->ptr),&vector[j].u.score) == 0) {
|
||||
int_conversion_error = 1;
|
||||
}
|
||||
} else if (byval->encoding == OBJ_ENCODING_INT) {
|
||||
|
|
|
|||
|
|
@ -1022,7 +1022,6 @@ int hashTypeSet(redisDb *db, kvobj *o, sds field, sds value, int flags) {
|
|||
if (newExpireAt != EB_EXPIRE_TIME_INVALID) {
|
||||
dict *d = o->ptr;
|
||||
htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
|
||||
serverAssert(dictExpireMeta->expireMeta.trash == 0);
|
||||
ebAdd(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, newEntry, newExpireAt);
|
||||
}
|
||||
|
||||
|
|
@ -3478,9 +3477,6 @@ static void hfieldPersist(robj *hashObj, Entry *entry) {
|
|||
dict *d = hashObj->ptr;
|
||||
htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
|
||||
|
||||
/* If field has valid expiry then dict must have valid metadata as well */
|
||||
serverAssert(dictExpireMeta->expireMeta.trash == 0);
|
||||
|
||||
/* Remove field from private HFE DS */
|
||||
ebRemove(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, entry);
|
||||
|
||||
|
|
@ -3608,15 +3604,16 @@ static int parseHashCommandArgs(client *c, HashCommandArgs *args,
|
|||
&numFields, "Parameter `numFields` should be greater than 0") != C_OK)
|
||||
return C_ERR;
|
||||
|
||||
args->fieldCount = (int)numFields;
|
||||
args->firstFieldPos = i + 2;
|
||||
|
||||
/* Check bounds - we must have exactly the right number of fields */
|
||||
if (args->firstFieldPos + args->fieldCount > c->argc) {
|
||||
if (numFields > c->argc - args->firstFieldPos) {
|
||||
addReplyError(c, "wrong number of arguments");
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
args->fieldCount = (int)numFields;
|
||||
|
||||
/* Skip over the field arguments */
|
||||
i = args->firstFieldPos + args->fieldCount - 1;
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -2500,11 +2500,14 @@ void streamPropagateGroupID(client *c, robj *key, streamCG *group, robj *groupna
|
|||
decrRefCount(argv[6]);
|
||||
}
|
||||
|
||||
/* We need this when we want to propagate creation of consumer that was created
|
||||
* by XREADGROUP with the NOACK option. In that case, the only way to create
|
||||
* the consumer at the replica is by using XGROUP CREATECONSUMER (see issue #7140)
|
||||
/* Propagate creation of a consumer that was implicitly created by XREADGROUP.
|
||||
* Called only when no XCLAIM commands were propagated for this consumer,
|
||||
* since XCLAIM implicitly creates the consumer on the replica. This covers
|
||||
* two cases:
|
||||
* (1) NOACK, where the PEL/XCLAIM path is skipped entirely.
|
||||
* (2) no messages were available to deliver (see #7140).
|
||||
*
|
||||
* XGROUP CREATECONSUMER <key> <groupname> <consumername>
|
||||
* XGROUP CREATECONSUMER <key> <groupname> <consumername>
|
||||
*/
|
||||
void streamPropagateConsumerCreation(client *c, robj *key, robj *groupname, sds consumername) {
|
||||
robj *argv[5];
|
||||
|
|
@ -3456,6 +3459,7 @@ void xreadCommand(client *c) {
|
|||
int serve_claimed = 0;
|
||||
int serve_synchronously = 0;
|
||||
int serve_history = 0; /* True for XREADGROUP with ID != ">". */
|
||||
int consumer_created = 0;
|
||||
streamConsumer *consumer = NULL; /* Unused if XREAD */
|
||||
streamPropInfo spi = {c->argv[streams_arg+i],groupname}; /* Unused if XREAD */
|
||||
|
||||
|
|
@ -3516,10 +3520,7 @@ void xreadCommand(client *c) {
|
|||
c->db->id,SCC_DEFAULT);
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db,getKeySlot(c->argv[streams_arg+i]->ptr),o,old_alloc,kvobjAllocSize(o));
|
||||
if (noack)
|
||||
streamPropagateConsumerCreation(c,spi.keyname,
|
||||
spi.groupname,
|
||||
consumer->name);
|
||||
consumer_created = 1;
|
||||
}
|
||||
consumer->seen_time = commandTimeSnapshot();
|
||||
keyModified(c,c->db,c->argv[streams_arg+i],o,0); /* only update LRM */
|
||||
|
|
@ -3545,6 +3546,7 @@ void xreadCommand(client *c) {
|
|||
flags |= STREAM_RWR_CLAIMED;
|
||||
}
|
||||
|
||||
unsigned long propCount = 0;
|
||||
if (serve_synchronously) {
|
||||
arraylen++;
|
||||
if (arraylen == 1) arraylen_ptr = addReplyDeferredLen(c);
|
||||
|
|
@ -3559,7 +3561,6 @@ void xreadCommand(client *c) {
|
|||
if (c->resp == 2) addReplyArrayLen(c,2);
|
||||
addReplyBulk(c,c->argv[streams_arg+i]);
|
||||
|
||||
unsigned long propCount = 0;
|
||||
if (noack) flags |= STREAM_RWR_NOACK;
|
||||
if (serve_history) flags |= STREAM_RWR_HISTORY;
|
||||
if (server.memory_tracking_enabled)
|
||||
|
|
@ -3574,6 +3575,14 @@ void xreadCommand(client *c) {
|
|||
keyModified(c,c->db,c->argv[streams_arg+i],o,0); /* only update LRM */
|
||||
}
|
||||
}
|
||||
|
||||
/* Propagate consumer creation only when no XCLAIM was generated,
|
||||
* since XCLAIM implicitly creates the consumer on the replica.
|
||||
* With NOACK the PEL/XCLAIM path is skipped entirely, so we
|
||||
* always need explicit propagation regardless of propCount. */
|
||||
if (consumer_created && (noack || propCount == 0)) {
|
||||
streamPropagateConsumerCreation(c,spi.keyname, spi.groupname, consumer->name);
|
||||
}
|
||||
}
|
||||
|
||||
/* We replied synchronously! Set the top array len and return to caller. */
|
||||
|
|
@ -5546,7 +5555,7 @@ void xtrimCommand(client *c) {
|
|||
|
||||
/* Helper function for xinfoCommand.
|
||||
* Handles the variants of XINFO STREAM */
|
||||
void xinfoReplyWithStreamInfo(client *c, kvobj *kv) {
|
||||
void xinfoReplyWithStreamInfo(client *c, robj *key, kvobj *kv) {
|
||||
stream *s = kv->ptr;
|
||||
int full = 1;
|
||||
long long count = 10; /* Default COUNT is 10 so we don't block the server */
|
||||
|
|
@ -5779,7 +5788,7 @@ void xinfoReplyWithStreamInfo(client *c, kvobj *kv) {
|
|||
}
|
||||
}
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db,getKeySlot(c->argv[1]->ptr),kv,old_alloc,kvobjAllocSize(kv));
|
||||
updateSlotAllocSize(c->db,getKeySlot(key->ptr),kv,old_alloc,kvobjAllocSize(kv));
|
||||
}
|
||||
|
||||
/* XINFO CONSUMERS <key> <group>
|
||||
|
|
@ -5883,7 +5892,7 @@ NULL
|
|||
raxStop(&ri);
|
||||
} else if (!strcasecmp(opt,"STREAM")) {
|
||||
/* XINFO STREAM <key> [FULL [COUNT <count>]]. */
|
||||
xinfoReplyWithStreamInfo(c,kv);
|
||||
xinfoReplyWithStreamInfo(c,key,kv);
|
||||
} else {
|
||||
addReplySubcommandSyntaxError(c);
|
||||
}
|
||||
|
|
@ -6487,7 +6496,7 @@ void streamKeyLoaded(redisDb *db, robj *key, robj *val) {
|
|||
}
|
||||
}
|
||||
|
||||
/* To be used when a steam key was removed from ram, un-redigster from stream_idmp_keys if needed */
|
||||
/* To be used when a stream key was removed from ram, un-register from stream_idmp_keys if needed */
|
||||
void streamKeyRemoved(redisDb *db, robj *key, robj *val) {
|
||||
UNUSED(val);
|
||||
dictDelete(db->stream_idmp_keys, key);
|
||||
|
|
|
|||
43
src/t_zset.c
43
src/t_zset.c
|
|
@ -721,24 +721,26 @@ static int zslParseRange(robj *min, robj *max, zrangespec *spec) {
|
|||
if (min->encoding == OBJ_ENCODING_INT) {
|
||||
spec->min = (long)min->ptr;
|
||||
} else {
|
||||
size_t len = sdslen(min->ptr);
|
||||
if (((char*)min->ptr)[0] == '(') {
|
||||
spec->min = fast_float_strtod((char*)min->ptr+1,&eptr);
|
||||
spec->min = fast_float_strtod((char*)min->ptr+1,len-1,&eptr);
|
||||
if (eptr[0] != '\0' || isnan(spec->min)) return C_ERR;
|
||||
spec->minex = 1;
|
||||
} else {
|
||||
spec->min = fast_float_strtod((char*)min->ptr,&eptr);
|
||||
spec->min = fast_float_strtod((char*)min->ptr,len,&eptr);
|
||||
if (eptr[0] != '\0' || isnan(spec->min)) return C_ERR;
|
||||
}
|
||||
}
|
||||
if (max->encoding == OBJ_ENCODING_INT) {
|
||||
spec->max = (long)max->ptr;
|
||||
} else {
|
||||
size_t len = sdslen(max->ptr);
|
||||
if (((char*)max->ptr)[0] == '(') {
|
||||
spec->max = fast_float_strtod((char*)max->ptr+1,&eptr);
|
||||
spec->max = fast_float_strtod((char*)max->ptr+1,len-1,&eptr);
|
||||
if (eptr[0] != '\0' || isnan(spec->max)) return C_ERR;
|
||||
spec->maxex = 1;
|
||||
} else {
|
||||
spec->max = fast_float_strtod((char*)max->ptr,&eptr);
|
||||
spec->max = fast_float_strtod((char*)max->ptr,len,&eptr);
|
||||
if (eptr[0] != '\0' || isnan(spec->max)) return C_ERR;
|
||||
}
|
||||
}
|
||||
|
|
@ -945,13 +947,8 @@ zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n, un
|
|||
*----------------------------------------------------------------------------*/
|
||||
|
||||
static double zzlStrtod(unsigned char *vstr, unsigned int vlen) {
|
||||
char buf[128];
|
||||
if (vlen > sizeof(buf) - 1)
|
||||
vlen = sizeof(buf) - 1;
|
||||
memcpy(buf,vstr,vlen);
|
||||
buf[vlen] = '\0';
|
||||
return fast_float_strtod(buf,NULL);
|
||||
}
|
||||
return fast_float_strtod((char*)vstr, vlen, NULL);
|
||||
}
|
||||
|
||||
double zzlGetScore(unsigned char *sptr) {
|
||||
unsigned char *vstr;
|
||||
|
|
@ -2653,6 +2650,15 @@ static int zuiCompareByRevCardinality(const void *s1, const void *s2) {
|
|||
#define REDIS_AGGR_SUM 1
|
||||
#define REDIS_AGGR_MIN 2
|
||||
#define REDIS_AGGR_MAX 3
|
||||
#define REDIS_AGGR_COUNT 4
|
||||
|
||||
/* Return the weighted contribution of a single sorted set member.
|
||||
* For COUNT aggregation the actual score is irrelevant — each member
|
||||
* contributes its set's weight (i.e. "one occurrence worth <weight>").
|
||||
* For all other aggregation modes the contribution is weight * score. */
|
||||
inline static double zuiWeightedScore(double score, double weight, int aggregate) {
|
||||
return (aggregate == REDIS_AGGR_COUNT) ? weight : weight * score;
|
||||
}
|
||||
|
||||
inline static void zunionInterAggregate(double *target, double val, int aggregate) {
|
||||
if (aggregate == REDIS_AGGR_SUM) {
|
||||
|
|
@ -2661,6 +2667,11 @@ inline static void zunionInterAggregate(double *target, double val, int aggregat
|
|||
* is +inf and the other is -inf. When these numbers are added,
|
||||
* we maintain the convention of the result being 0.0. */
|
||||
if (isnan(*target)) *target = 0.0;
|
||||
} else if (aggregate == REDIS_AGGR_COUNT) {
|
||||
*target += val;
|
||||
/* The val is zuiWeightedScore(…) == weight, which can be +inf/-inf,
|
||||
* so the NaN guard applies here. */
|
||||
if (isnan(*target)) *target = 0.0;
|
||||
} else if (aggregate == REDIS_AGGR_MIN) {
|
||||
*target = val < *target ? val : *target;
|
||||
} else if (aggregate == REDIS_AGGR_MAX) {
|
||||
|
|
@ -2962,6 +2973,8 @@ void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, in
|
|||
aggregate = REDIS_AGGR_MIN;
|
||||
} else if (!strcasecmp(c->argv[j]->ptr,"max")) {
|
||||
aggregate = REDIS_AGGR_MAX;
|
||||
} else if (!strcasecmp(c->argv[j]->ptr,"count")) {
|
||||
aggregate = REDIS_AGGR_COUNT;
|
||||
} else {
|
||||
zfree(src);
|
||||
addReplyErrorObject(c,shared.syntaxerr);
|
||||
|
|
@ -3018,17 +3031,17 @@ void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, in
|
|||
while (zuiNext(&src[0],&zval)) {
|
||||
double score, value;
|
||||
|
||||
score = src[0].weight * zval.score;
|
||||
score = zuiWeightedScore(zval.score, src[0].weight, aggregate);
|
||||
if (isnan(score)) score = 0;
|
||||
|
||||
for (j = 1; j < setnum; j++) {
|
||||
/* It is not safe to access the zset we are
|
||||
* iterating, so explicitly check for equal object. */
|
||||
if (src[j].subject == src[0].subject) {
|
||||
value = zval.score*src[j].weight;
|
||||
value = zuiWeightedScore(zval.score, src[j].weight, aggregate);
|
||||
zunionInterAggregate(&score,value,aggregate);
|
||||
} else if (zuiFind(&src[j],&zval,&value)) {
|
||||
value *= src[j].weight;
|
||||
value = zuiWeightedScore(value, src[j].weight, aggregate);
|
||||
zunionInterAggregate(&score,value,aggregate);
|
||||
} else {
|
||||
break;
|
||||
|
|
@ -3075,7 +3088,7 @@ void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, in
|
|||
zuiInitIterator(&src[i]);
|
||||
while (zuiNext(&src[i],&zval)) {
|
||||
/* Initialize value */
|
||||
score = src[i].weight * zval.score;
|
||||
score = zuiWeightedScore(zval.score, src[i].weight, aggregate);
|
||||
if (isnan(score)) score = 0;
|
||||
|
||||
/* Search for this element in the dict (which stores node pointers). */
|
||||
|
|
|
|||
|
|
@ -664,13 +664,10 @@ int string2d(const char *s, size_t slen, double *dp) {
|
|||
if (unlikely(slen == 0 ||
|
||||
isspace(((const char*)s)[0])))
|
||||
return 0;
|
||||
*dp = fast_float_strtod(s, &eptr);
|
||||
/* If `fast_float_strtod` didn't consume full input, try `strtod`
|
||||
* Given fast_float does not support hexadecimal strings representation */
|
||||
*dp = fast_float_strtod(s, slen, &eptr);
|
||||
/* Reject if not all characters were consumed by the parser. */
|
||||
if (unlikely((size_t)(eptr - (char*)s) != slen)) {
|
||||
char *fallback_eptr;
|
||||
*dp = strtod(s, &fallback_eptr);
|
||||
if ((size_t)(fallback_eptr - (char*)s) != slen) return 0;
|
||||
return 0;
|
||||
}
|
||||
if (unlikely(errno == EINVAL ||
|
||||
(errno == ERANGE &&
|
||||
|
|
|
|||
173
src/vector.c
Normal file
173
src/vector.c
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/* vector.c - Simple append-only vector implementation
|
||||
*
|
||||
* Copyright (c) 2026-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "vector.h"
|
||||
#include "redisassert.h"
|
||||
#include "zmalloc.h"
|
||||
|
||||
#define VEC_DEFAULT_INITCAP 8
|
||||
|
||||
/*
|
||||
* Vector initialization.
|
||||
*
|
||||
* Modes:
|
||||
* - stack != NULL: use caller-provided storage for the first initcap items.
|
||||
* - stack == NULL && initcap > 0: start heap-backed with an initial 'initcap' capacity.
|
||||
* - stack == NULL && initcap == 0: start heap-backed with no initial storage.
|
||||
*/
|
||||
void vecInit(vec *v, void **stack, size_t initcap) {
|
||||
/* If stack is provided, initcap must be > 0 and at the size of the stack */
|
||||
assert(initcap > 0 || stack == NULL);
|
||||
|
||||
v->size = 0;
|
||||
v->cap = initcap;
|
||||
v->stack = stack; /* stack is NULL if not used */
|
||||
|
||||
/* now init data either stack, heap or NULL */
|
||||
v->data = (stack) ? stack : ((initcap > 0) ? zmalloc(initcap * sizeof(void *)) : NULL);
|
||||
}
|
||||
|
||||
/* Free only heap storage if any */
|
||||
void vecRelease(vec *v) {
|
||||
/* if data is not stack-allocated and is not NULL, free it */
|
||||
if (v->data && v->data != v->stack)
|
||||
zfree(v->data);
|
||||
v->size = 0;
|
||||
v->cap = 0;
|
||||
v->data = NULL;
|
||||
v->stack = NULL;
|
||||
}
|
||||
|
||||
/* Reset the logical length to zero while preserving allocated storage. */
|
||||
void vecClear(vec *v) {
|
||||
v->size = 0;
|
||||
}
|
||||
|
||||
/* Return the number of elements in the vector. */
|
||||
size_t vecSize(const vec *v) {
|
||||
return v->size;
|
||||
}
|
||||
|
||||
/* Get element at index. index must be < vecSize(v). */
|
||||
void *vecGet(const vec *v, size_t index) {
|
||||
assert(index < v->size);
|
||||
return v->data[index];
|
||||
}
|
||||
|
||||
/* Return the contiguous backing array. */
|
||||
void **vecData(vec *v) {
|
||||
return v->data;
|
||||
}
|
||||
|
||||
/* Ensure capacity is at least mincap. */
|
||||
void vecReserve(vec *v, size_t mincap) {
|
||||
void **newdata;
|
||||
|
||||
if (mincap <= v->cap) return;
|
||||
|
||||
/* If no heap storage is used yet, allocate and copy from stack if needed. */
|
||||
if (v->data == v->stack) {
|
||||
newdata = zmalloc(mincap * sizeof(void *));
|
||||
if (v->size) memcpy(newdata, v->data, v->size * sizeof(void *));
|
||||
} else {
|
||||
newdata = zrealloc(v->data, mincap * sizeof(void *));
|
||||
}
|
||||
|
||||
v->data = newdata;
|
||||
v->cap = mincap;
|
||||
}
|
||||
|
||||
/* Append one element, growing storage as needed. */
|
||||
void vecPush(vec *v, void *value) {
|
||||
if (v->size == v->cap) {
|
||||
size_t newcap = (v->cap > 0) ? v->cap * 2 : VEC_DEFAULT_INITCAP;
|
||||
vecReserve(v, newcap);
|
||||
}
|
||||
|
||||
v->data[v->size++] = value;
|
||||
}
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "testhelp.h"
|
||||
|
||||
#define UNUSED(x) (void)(x)
|
||||
|
||||
int vectorTest(int argc, char **argv, int flags)
|
||||
{
|
||||
UNUSED(argc);
|
||||
UNUSED(argv);
|
||||
UNUSED(flags);
|
||||
|
||||
vec v;
|
||||
void *vstack[2];
|
||||
int one = 1, two = 2, three = 3, four = 4, five = 5, six = 6;
|
||||
|
||||
vecInit(&v, vstack, 2);
|
||||
test_cond("vecInit() stack-backed size is 0", vecSize(&v) == 0);
|
||||
test_cond("vecInit() uses stack buffer", vecData(&v) == vstack);
|
||||
vecReserve(&v, 1);
|
||||
test_cond("vecReserve() no-ops when capacity is already sufficient",
|
||||
v.cap == 2 && vecData(&v) == vstack);
|
||||
vecPush(&v, &one);
|
||||
vecPush(&v, &two);
|
||||
test_cond("vecPush() appends into stack storage",
|
||||
vecSize(&v) == 2 && vecData(&v) == vstack &&
|
||||
vecGet(&v, 0) == &one && vecGet(&v, 1) == &two);
|
||||
vecReserve(&v, 4);
|
||||
test_cond("vecReserve() spills from stack to heap preserving values",
|
||||
v.cap == 4 && vecData(&v) != vstack &&
|
||||
vecGet(&v, 0) == &one && vecGet(&v, 1) == &two);
|
||||
vecPush(&v, &three);
|
||||
test_cond("vecPush() spills from stack to heap preserving values",
|
||||
vecSize(&v) == 3 &&
|
||||
vecData(&v) != vstack && vecGet(&v, 0) == &one &&
|
||||
vecGet(&v, 1) == &two && vecGet(&v, 2) == &three);
|
||||
|
||||
void **heap_data = vecData(&v);
|
||||
vecClear(&v);
|
||||
test_cond("vecClear() resets size but preserves storage",
|
||||
vecSize(&v) == 0 && vecData(&v) == heap_data);
|
||||
vecRelease(&v);
|
||||
test_cond("vecRelease() resets vector state",
|
||||
vecSize(&v) == 0 && vecData(&v) == NULL && v.cap == 0);
|
||||
|
||||
vecInit(&v, NULL, 4);
|
||||
test_cond("vecInit() heap-backed hint allocates storage",
|
||||
vecSize(&v) == 0 && vecData(&v) != NULL && v.cap == 4);
|
||||
vecPush(&v, &four);
|
||||
test_cond("vecPush() works in heap-backed mode",
|
||||
vecGet(&v, 0) == &four);
|
||||
vecReserve(&v, 8);
|
||||
test_cond("vecReserve() grows heap-backed storage preserving values",
|
||||
v.cap == 8 && vecGet(&v, 0) == &four);
|
||||
vecRelease(&v);
|
||||
|
||||
vecInit(&v, NULL, 0);
|
||||
vecReserve(&v, 6);
|
||||
test_cond("vecReserve() allocates heap storage from empty vector",
|
||||
v.cap == 6 && vecData(&v) != NULL);
|
||||
vecPush(&v, &five);
|
||||
vecPush(&v, &six);
|
||||
test_cond("vecPush() works after vecReserve() on empty vector",
|
||||
vecSize(&v) == 2 &&
|
||||
vecGet(&v, 0) == &five && vecGet(&v, 1) == &six);
|
||||
vecRelease(&v);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
92
src/vector.h
Normal file
92
src/vector.h
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#ifndef REDIS_VECTOR_H
|
||||
#define REDIS_VECTOR_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/*
|
||||
* Simple append-only vector (dynamic array) of void * elements.
|
||||
*
|
||||
* Design:
|
||||
* --------
|
||||
* - Stores elements in a contiguous array (void **).
|
||||
* - Supports append (vecPush) and read access.
|
||||
* - Optionally uses caller-provided stack buffer to avoid heap allocations.
|
||||
* - See also comment in vector.c of vecInit() for more details.
|
||||
*
|
||||
* Memory:
|
||||
* -------
|
||||
* - vecRelease() frees heap memory if used.
|
||||
* - Stack buffer is never freed.
|
||||
* - Stored elements are never freed.
|
||||
*
|
||||
* Modes:
|
||||
* -------
|
||||
* 1. Start On Stack (grow to heap): vec v;
|
||||
* void *vstack[8];
|
||||
* ...
|
||||
* vecInit(&v, vstack, 8);
|
||||
*
|
||||
* Start Embedded (grow to heap): typedef struct {
|
||||
* vec v;
|
||||
* void *vembedded[8];
|
||||
* } obj;
|
||||
* ...
|
||||
* vecInit(&obj->v, obj->vembedded, 8);
|
||||
*
|
||||
* 2. Heap only, init capacity 8: vec v;
|
||||
* ...
|
||||
* vecInit(&v, NULL, 8);
|
||||
*
|
||||
* Heap only, init capacity 0: vec v;
|
||||
* ...
|
||||
* vecInit(&v, NULL, 0);
|
||||
*
|
||||
* 3. Depends on var size: vec v;
|
||||
* void *vstack[8];
|
||||
* vecInit(&v, vstack, 8);
|
||||
* vecReserve(&v, varsize); // varsize <= 8 ? stack : heap
|
||||
*
|
||||
* Notes:
|
||||
* ------
|
||||
* - Not thread-safe.
|
||||
* - If stack == NULL and initcap > 0, initcap is treated as an initial
|
||||
* heap-capacity hint.
|
||||
* - When used in Redis core, the implementation should use the Redis allocator
|
||||
* wrappers (zmalloc / zrealloc / zfree) rather than libc allocation APIs.
|
||||
*/
|
||||
|
||||
typedef struct vec {
|
||||
size_t size; /* Number of elements in the vector. */
|
||||
size_t cap; /* Capacity of the vector. */
|
||||
void **data; /* Heap-allocated storage or refers to stack. */
|
||||
void **stack; /* Optional stack buffer. */
|
||||
} vec;
|
||||
|
||||
/* Initialize a vector */
|
||||
void vecInit(vec *v, void **stack, size_t initcap);
|
||||
|
||||
/* Free only heap storage if any */
|
||||
void vecRelease(vec *v);
|
||||
|
||||
/* Reset the logical length to zero while preserving allocated storage. */
|
||||
void vecClear(vec *v);
|
||||
|
||||
size_t vecSize(const vec *v);
|
||||
|
||||
/* Requires index < vecSize(v). */
|
||||
void *vecGet(const vec *v, size_t index);
|
||||
|
||||
/* Return the contiguous backing array. */
|
||||
void **vecData(vec *v);
|
||||
|
||||
/* Ensure capacity is at least mincap. */
|
||||
void vecReserve(vec *v, size_t mincap);
|
||||
|
||||
/* Append one element, growing storage as needed. */
|
||||
void vecPush(vec *v, void *value);
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
int vectorTest(int argc, char **argv, int flags);
|
||||
#endif
|
||||
|
||||
#endif /* REDIS_VECTOR_H */
|
||||
|
|
@ -41,5 +41,5 @@ test "errorstats: rejected call due to MOVED Redirection" {
|
|||
}
|
||||
assert_match {} [errorstat $pok MOVED]
|
||||
assert_match {*count=1*} [errorstat $perr MOVED]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0} [cmdstat $perr set]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0*} [cmdstat $perr set]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ set ::tlsdir "tests/tls"
|
|||
|
||||
# Continuously sends SET commands to the server. If key is omitted, a random key
|
||||
# is used for every SET command. The value is always random.
|
||||
proc gen_write_load {host port seconds tls {key ""} {size 0} {sleep 0}} {
|
||||
# ignore_error_reply (default 0): when non-zero, MOVED/ASK replies are tolerated
|
||||
# while draining pipelined responses (periodic 500-reply batches and final drain).
|
||||
proc gen_write_load {host port seconds tls {key ""} {size 0} {sleep 0} {ignore_error_reply 0}} {
|
||||
set start_time [clock seconds]
|
||||
set r [redis $host $port 1 $tls]
|
||||
$r client setname LOAD_HANDLER
|
||||
|
|
@ -44,12 +46,19 @@ proc gen_write_load {host port seconds tls {key ""} {size 0} {sleep 0}} {
|
|||
} else {
|
||||
$r set $key $value
|
||||
}
|
||||
|
||||
|
||||
incr count
|
||||
if {$count % 500 == 0} {
|
||||
for {set i 0} {$i < 500} {incr i} {
|
||||
$r read
|
||||
# Capture opts to preserve original errorInfo/errorCode on re-raise.
|
||||
if {[catch {$r read} err opts]} {
|
||||
if {$ignore_error_reply && ([string match {MOVED*} $err] || [string match {ASK*} $err])} {
|
||||
continue
|
||||
}
|
||||
return -options $opts $err
|
||||
}
|
||||
}
|
||||
set count 0
|
||||
}
|
||||
|
||||
if {[clock seconds]-$start_time > $seconds} {
|
||||
|
|
@ -59,12 +68,17 @@ proc gen_write_load {host port seconds tls {key ""} {size 0} {sleep 0}} {
|
|||
after $sleep
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Read remaining replies
|
||||
for {set i 0} {$i < $count} {incr i} {
|
||||
$r read
|
||||
if {[catch {$r read} err opts]} {
|
||||
if {$ignore_error_reply && ([string match {MOVED*} $err] || [string match {ASK*} $err])} {
|
||||
continue
|
||||
}
|
||||
return -options $opts $err
|
||||
}
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
gen_write_load [lindex $argv 0] [lindex $argv 1] [lindex $argv 2] [lindex $argv 3] [lindex $argv 4] [lindex $argv 5] [lindex $argv 6]
|
||||
gen_write_load [lindex $argv 0] [lindex $argv 1] [lindex $argv 2] [lindex $argv 3] [lindex $argv 4] [lindex $argv 5] [lindex $argv 6] [lindex $argv 7]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# Actually, we may not have many asserts in the test, since we just check for
|
||||
# crashes and the dump file inconsistencies.
|
||||
|
||||
start_server {tags {"dismiss external:skip"}} {
|
||||
start_server {tags {"dismiss external:skip needs:debug"}} {
|
||||
# In other tests, although we test child process dumping RDB file, but
|
||||
# memory allocations of key/values are usually small, they couldn't cover
|
||||
# the "dismiss" object methods, in this test, we create big size key/values
|
||||
|
|
@ -47,12 +47,15 @@ start_server {tags {"dismiss external:skip"}} {
|
|||
r xadd bigstream * entry1 $bigstr entry2 $bigstr
|
||||
|
||||
set digest [debug_digest]
|
||||
r config set aof-use-rdb-preamble no
|
||||
r bgrewriteaof
|
||||
waitForBgrewriteaof r
|
||||
r debug loadaof
|
||||
set newdigest [debug_digest]
|
||||
assert {$digest eq $newdigest}
|
||||
# Test both RDB (yes) and AOF (no) rewrite paths.
|
||||
foreach preamble {yes no} {
|
||||
r config set aof-use-rdb-preamble $preamble
|
||||
r bgrewriteaof
|
||||
waitForBgrewriteaof r
|
||||
r debug loadaof
|
||||
set newdigest [debug_digest]
|
||||
assert {$digest eq $newdigest}
|
||||
}
|
||||
}
|
||||
|
||||
test {dismiss client output buffer} {
|
||||
|
|
@ -99,4 +102,48 @@ start_server {tags {"dismiss external:skip"}} {
|
|||
waitForBgsave $master
|
||||
}
|
||||
}
|
||||
|
||||
test {dismiss multi-db kvstore bucket memory in standalone mode} {
|
||||
r flushall
|
||||
regexp {db=(\d+)} [r client info] -> curdb
|
||||
# Populate multiple DBs to verify each DB's bucket arrays can be dismissed.
|
||||
foreach db {0 1 2 3} {
|
||||
r select $db
|
||||
populate 2000 "db${db}key:" 3 0 false 3600
|
||||
}
|
||||
set digest [debug_digest]
|
||||
|
||||
# Test both RDB (yes) and AOF (no) rewrite paths.
|
||||
foreach preamble {yes no} {
|
||||
r config set aof-use-rdb-preamble $preamble
|
||||
r bgrewriteaof
|
||||
waitForBgrewriteaof r
|
||||
r debug loadaof
|
||||
set newdigest [debug_digest]
|
||||
assert {$digest eq $newdigest}
|
||||
}
|
||||
r select $curdb
|
||||
}
|
||||
}
|
||||
|
||||
start_cluster 1 0 {tags {dismiss external:skip cluster needs:debug}} {
|
||||
test {dismiss slot dict bucket memory in cluster mode} {
|
||||
# Concentrate keys into a few slots using hash tags so each slot's
|
||||
# bucket array is large enough to be dismissed.
|
||||
# {06S} -> slot 0, {Qi} -> slot 1, {5L5} -> slot 2
|
||||
foreach tag {{06S} {Qi} {5L5}} {
|
||||
populate 2000 "${tag}key:" 3 0 false 3600
|
||||
}
|
||||
set digest [r debug digest]
|
||||
|
||||
# Test both RDB (yes) and AOF (no) rewrite paths.
|
||||
foreach preamble {yes no} {
|
||||
r config set aof-use-rdb-preamble $preamble
|
||||
r bgrewriteaof
|
||||
waitForBgrewriteaof r
|
||||
r debug loadaof
|
||||
set newdigest [r debug digest]
|
||||
assert {$digest eq $newdigest}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -604,9 +604,11 @@ proc find_valgrind_errors {stderr on_termination} {
|
|||
# Execute a background process writing random data for the specified number
|
||||
# of seconds to the specified Redis instance. If key is omitted, a random key
|
||||
# is used for every SET command.
|
||||
proc start_write_load {host port seconds {key ""} {size 0} {sleep 0}} {
|
||||
# ignore_error_reply (default 0): set non-zero in cluster slot-migration tests to tolerate
|
||||
# MOVED/ASK replies while draining pipelined writes in the load helper.
|
||||
proc start_write_load {host port seconds {key ""} {size 0} {sleep 0} {ignore_error_reply 0}} {
|
||||
set tclsh [info nameofexecutable]
|
||||
exec $tclsh tests/helpers/gen_write_load.tcl $host $port $seconds $::tls $key $size $sleep &
|
||||
exec $tclsh tests/helpers/gen_write_load.tcl $host $port $seconds $::tls $key $size $sleep $ignore_error_reply &
|
||||
}
|
||||
|
||||
# Stop a process generating write load executed with start_write_load.
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ start_server {tags {"acl external:skip"}} {
|
|||
assert_error {*NOPERM No permissions to access a key*} {$rd read}
|
||||
$rd ping
|
||||
$rd close
|
||||
assert_match {*calls=0,usec=0,*,rejected_calls=1,failed_calls=0} [cmdrstat blpop r]
|
||||
assert_match {*calls=0,usec=0,*,rejected_calls=1,failed_calls=0*} [cmdrstat blpop r]
|
||||
}
|
||||
|
||||
test {Users can be configured to authenticate with any password} {
|
||||
|
|
|
|||
|
|
@ -577,23 +577,16 @@ start_cluster 3 3 {tags {external:skip cluster} overrides {cluster-node-timeout
|
|||
R 1 debug asm-trim-method none
|
||||
populate_slot 10000 -idx 1 -slot 6000
|
||||
|
||||
# Start write traffic on node-0
|
||||
# Throws -MOVED error once asm is completed, catch block will ignore it.
|
||||
catch {
|
||||
# Start the slot 0 write load on the R 0
|
||||
set port [get_port 0]
|
||||
set key [slot_key 0 mykey]
|
||||
set load_handle0 [start_write_load "127.0.0.1" $port 100 $key 0 5]
|
||||
}
|
||||
# Start write traffic on node-0 (ignore_error_reply=1 tolerates MOVED/ASK
|
||||
# replies while slots are being migrated).
|
||||
set port [get_port 0]
|
||||
set key [slot_key 0 mykey]
|
||||
set load_handle0 [start_write_load "127.0.0.1" $port 100 $key 0 5 1]
|
||||
|
||||
# Start write traffic on node-1
|
||||
# Throws -MOVED error once asm is completed, catch block will ignore it.
|
||||
catch {
|
||||
# Start the slot 6000 write load on the R 1
|
||||
set port [get_port 1]
|
||||
set key [slot_key 6000 mykey]
|
||||
set load_handle1 [start_write_load "127.0.0.1" $port 100 $key 0 5]
|
||||
}
|
||||
# Start write traffic on node-1 (ignore_error_reply=1 for migration redirects).
|
||||
set port [get_port 1]
|
||||
set key [slot_key 6000 mykey]
|
||||
set load_handle1 [start_write_load "127.0.0.1" $port 100 $key 0 5 1]
|
||||
|
||||
# Migrate keys
|
||||
R 1 CLUSTER MIGRATION IMPORT 0 100
|
||||
|
|
@ -801,8 +794,9 @@ start_cluster 3 3 {tags {external:skip cluster} overrides {cluster-node-timeout
|
|||
# we set a delay to write incremental data
|
||||
R 1 config set rdb-key-save-delay 1000000
|
||||
|
||||
# Start the slot 0 write load on the R 1
|
||||
set load_handle [start_write_load "127.0.0.1" [get_port 1] 100 $slot0_key]
|
||||
# Start slot 0 write load on R1. ignore_error_reply=1 tolerates MOVED/ASK
|
||||
# replies that can appear while slot 0 is being migrated.
|
||||
set load_handle [start_write_load "127.0.0.1" [get_port 1] 100 $slot0_key 0 0 1]
|
||||
|
||||
# Clear all fail points
|
||||
assert_equal {OK} [R 0 debug asm-failpoint "" ""]
|
||||
|
|
|
|||
|
|
@ -229,6 +229,103 @@ start_server {tags {"gcra" "external:skip"}} {
|
|||
}
|
||||
}
|
||||
|
||||
start_server {tags {"gcra" "external:skip"}} {
|
||||
test {GCRA - RDB save and reload preserves value} {
|
||||
r del mykey
|
||||
r gcra mykey 5 1 60
|
||||
r gcra mykey 5 1 60
|
||||
|
||||
set dump_before [r dump mykey]
|
||||
|
||||
r debug reload
|
||||
|
||||
assert_equal [r type mykey] "gcra"
|
||||
set dump_after [r dump mykey]
|
||||
assert_equal $dump_before $dump_after
|
||||
} {} {needs:debug}
|
||||
|
||||
test {GCRA - RDB save and reload preserves TTL} {
|
||||
r del mykey
|
||||
r gcra mykey 5 1 60
|
||||
set ttl_before [r pexpiretime mykey]
|
||||
assert_morethan $ttl_before 0
|
||||
|
||||
r debug reload
|
||||
|
||||
set ttl_after [r pexpiretime mykey]
|
||||
assert_morethan $ttl_after 0
|
||||
assert_equal $ttl_after $ttl_before
|
||||
} {} {needs:debug}
|
||||
|
||||
test {GCRA - DUMP and RESTORE roundtrip} {
|
||||
r del mykey mykey2
|
||||
r gcra mykey 5 1 60
|
||||
r gcra mykey 5 1 60
|
||||
|
||||
set dump [r dump mykey]
|
||||
set ttl [r pttl mykey]
|
||||
r restore mykey2 $ttl $dump
|
||||
|
||||
assert_equal [r type mykey2] "gcra"
|
||||
|
||||
set result_orig [r gcra mykey 5 1 60]
|
||||
set result_restored [r gcra mykey2 5 1 60]
|
||||
assert_equal [lindex $result_orig 2] [lindex $result_restored 2]
|
||||
}
|
||||
|
||||
test {GCRA - AOF rewrite preserves value} {
|
||||
r del mykey
|
||||
r config set appendonly yes
|
||||
waitForBgrewriteaof r
|
||||
|
||||
r gcra mykey 5 1 60
|
||||
r gcra mykey 5 1 60
|
||||
|
||||
set dump_before [r dump mykey]
|
||||
|
||||
r BGREWRITEAOF
|
||||
waitForBgrewriteaof r
|
||||
r debug reload
|
||||
|
||||
assert_equal [r type mykey] "gcra"
|
||||
set dump_after [r dump mykey]
|
||||
assert_equal $dump_before $dump_after
|
||||
} {} {external:skip needs:debug}
|
||||
|
||||
test {GCRA - AOF rewrite preserves TTL} {
|
||||
r del mykey
|
||||
r config set appendonly yes
|
||||
waitForBgrewriteaof r
|
||||
|
||||
r gcra mykey 5 1 60
|
||||
|
||||
r BGREWRITEAOF
|
||||
waitForBgrewriteaof r
|
||||
|
||||
set ttl_before [r pttl mykey]
|
||||
assert {$ttl_before > 0}
|
||||
|
||||
r debug reload
|
||||
|
||||
set ttl_after [r pttl mykey]
|
||||
assert {$ttl_after > 0}
|
||||
assert {$ttl_after <= $ttl_before}
|
||||
} {} {external:skip needs:debug}
|
||||
|
||||
test {GCRA - DEBUG DIGEST consistent after RDB reload} {
|
||||
r del mykey
|
||||
r gcra mykey 5 1 60
|
||||
r gcra mykey 5 1 60
|
||||
|
||||
set digest_before [r debug digest]
|
||||
|
||||
r debug reload
|
||||
|
||||
set digest_after [r debug digest]
|
||||
assert_equal $digest_before $digest_after
|
||||
} {} {needs:debug}
|
||||
}
|
||||
|
||||
start_server {tags {"gcra repl" "external:skip"}} {
|
||||
set replica [srv 0 client]
|
||||
set replica_host [srv 0 host]
|
||||
|
|
@ -240,27 +337,26 @@ start_server {tags {"gcra repl" "external:skip"}} {
|
|||
set master_host [srv 0 host]
|
||||
set master_port [srv 0 port]
|
||||
|
||||
$master flushdb
|
||||
$replica flushdb
|
||||
test {GCRA - Replication works} {
|
||||
$master flushdb
|
||||
$replica flushdb
|
||||
|
||||
$replica replicaof $master_host $master_port
|
||||
wait_for_condition 100 100 {
|
||||
[s -1 master_link_status] eq "up"
|
||||
} else {
|
||||
fail "Master <-> Replica didn't finish sync"
|
||||
}
|
||||
$replica replicaof $master_host $master_port
|
||||
wait_for_condition 100 100 {
|
||||
[s -1 master_link_status] eq "up"
|
||||
} else {
|
||||
fail "Master <-> Replica didn't finish sync"
|
||||
}
|
||||
|
||||
set cmdinfo [$replica info commandstats]
|
||||
assert_equal [lsearch -glob $cmdinfo "cmdstat_gcra:*"] -1
|
||||
assert_equal [lsearch -glob $cmdinfo "cmdstat_set:*"] -1
|
||||
set cmdinfo [$replica info commandstats]
|
||||
assert_equal [lsearch -glob $cmdinfo "cmdstat_gcrasetvalue:*"] -1
|
||||
|
||||
$master del mykey
|
||||
$master gcra mykey 2 1 1000 TOKENS 2
|
||||
$master del mykey
|
||||
$master gcra mykey 2 1 1000 TOKENS 2
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
set cmdinfo [$replica info commandstats]
|
||||
assert_equal [lsearch -glob $cmdinfo "cmdstat_gcra:*"] -1
|
||||
assert_morethan_equal [lsearch -glob $cmdinfo "cmdstat_set:*"] 0
|
||||
set cmdinfo [$replica info commandstats]
|
||||
assert_morethan_equal [lsearch -glob $cmdinfo "cmdstat_gcrasetvalue:*"] 0
|
||||
} {} {external:skip}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ start_server {tags {"info" "external:skip"}} {
|
|||
catch {r auth k} e
|
||||
assert_match {ERR AUTH*} $e
|
||||
assert_match {*count=1*} [errorstat ERR]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
assert_equal [s total_error_replies] 1
|
||||
r config resetstat
|
||||
assert_match {} [errorstat ERR]
|
||||
|
|
@ -137,15 +137,15 @@ start_server {tags {"info" "external:skip"}} {
|
|||
catch {r exec} e
|
||||
assert_match {ERR AUTH*} $e
|
||||
assert_match {*count=1*} [errorstat ERR]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdstat set]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdstat exec]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0*} [cmdstat set]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0*} [cmdstat exec]
|
||||
assert_equal [s total_error_replies] 1
|
||||
|
||||
# MULTI/EXEC command errors should still be pinpointed to him
|
||||
catch {r exec} e
|
||||
assert_match {ERR EXEC without MULTI} $e
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat exec]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1*} [cmdstat exec]
|
||||
assert_match {*count=2*} [errorstat ERR]
|
||||
assert_equal [s total_error_replies] 2
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ start_server {tags {"info" "external:skip"}} {
|
|||
catch {r evalsha NotValidShaSUM 0} e
|
||||
assert_match {NOSCRIPT*} $e
|
||||
assert_match {*count=1*} [errorstat NOSCRIPT]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat evalsha]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat evalsha]
|
||||
assert_equal [s total_error_replies] 1
|
||||
r config resetstat
|
||||
assert_match {} [errorstat NOSCRIPT]
|
||||
|
|
@ -188,7 +188,7 @@ start_server {tags {"info" "external:skip"}} {
|
|||
catch {r XGROUP CREATECONSUMER mystream mygroup consumer} e
|
||||
assert_match {NOGROUP*} $e
|
||||
assert_match {*count=1*} [errorstat NOGROUP]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup\\|createconsumer]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat xgroup\\|createconsumer]
|
||||
r config resetstat
|
||||
assert_match {} [errorstat NOGROUP]
|
||||
}
|
||||
|
|
@ -217,9 +217,9 @@ start_server {tags {"info" "external:skip"}} {
|
|||
assert_match {*count=1*} [errorstat ERR]
|
||||
assert_match {*count=1*} [errorstat EXECABORT]
|
||||
assert_equal [s total_error_replies] 2
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0} [cmdstat set]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdstat multi]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat exec]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0*} [cmdstat set]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0*} [cmdstat multi]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat exec]
|
||||
assert_equal [s total_error_replies] 2
|
||||
r config resetstat
|
||||
assert_match {} [errorstat ERR]
|
||||
|
|
@ -232,11 +232,11 @@ start_server {tags {"info" "external:skip"}} {
|
|||
catch {r set k} e
|
||||
assert_match {ERR wrong number of arguments for 'set' command} $e
|
||||
assert_match {*count=1*} [errorstat ERR]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0} [cmdstat set]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0*} [cmdstat set]
|
||||
# ensure that after a rejected command, valid ones are counted properly
|
||||
r set k1 v1
|
||||
r set k2 v2
|
||||
assert_match {calls=2,*,rejected_calls=1,failed_calls=0} [cmdstat set]
|
||||
assert_match {calls=2,*,rejected_calls=1,failed_calls=0*} [cmdstat set]
|
||||
assert_equal [s total_error_replies] 1
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ start_server {tags {"info" "external:skip"}} {
|
|||
catch {r set a b} e
|
||||
assert_match {OOM*} $e
|
||||
assert_match {*count=1*} [errorstat OOM]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0} [cmdstat set]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0*} [cmdstat set]
|
||||
assert_equal [s total_error_replies] 1
|
||||
r config resetstat
|
||||
assert_match {} [errorstat OOM]
|
||||
|
|
@ -264,7 +264,7 @@ start_server {tags {"info" "external:skip"}} {
|
|||
catch {r set a b} e
|
||||
assert_match {NOPERM*} $e
|
||||
assert_match {*count=1*} [errorstat NOPERM]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0} [cmdstat set]
|
||||
assert_match {*calls=0,*,rejected_calls=1,failed_calls=0*} [cmdstat set]
|
||||
assert_equal [s total_error_replies] 1
|
||||
r config resetstat
|
||||
assert_match {} [errorstat NOPERM]
|
||||
|
|
@ -283,7 +283,7 @@ start_server {tags {"info" "external:skip"}} {
|
|||
r client unblock $rd_id error
|
||||
assert_error {UNBLOCKED*} {$rd read}
|
||||
assert_match {*count=1*} [errorstat UNBLOCKED]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat blpop]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat blpop]
|
||||
assert_equal [s total_error_replies] 1
|
||||
$rd close
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,16 +248,16 @@ foreach call_type {nested normal} {
|
|||
# RM_Call that propagates an error
|
||||
assert_error "WRONGTYPE*" {r do_rm_call hgetall x}
|
||||
assert_equal [errorrstat WRONGTYPE r] {count=1}
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdrstat hgetall r]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdrstat hgetall r]
|
||||
|
||||
# RM_Call from bg thread that propagates an error
|
||||
assert_error "WRONGTYPE*" {r do_bg_rm_call hgetall x}
|
||||
assert_equal [errorrstat WRONGTYPE r] {count=2}
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=2} [cmdrstat hgetall r]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=2*} [cmdrstat hgetall r]
|
||||
|
||||
assert_equal [s total_error_replies] 6
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=4} [cmdrstat do_rm_call r]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=2} [cmdrstat do_bg_rm_call r]
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=4*} [cmdrstat do_rm_call r]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=2*} [cmdrstat do_bg_rm_call r]
|
||||
}
|
||||
|
||||
set master [srv 0 client]
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@ start_server {tags {"modules external:skip"}} {
|
|||
r acl setuser foo >pwd on ~* &* +@all
|
||||
assert_equal {OK} [r AUTH foo allow]
|
||||
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny}
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
assert_error {*WRONGPASS*} {r AUTH foo nomatch}
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=2} [cmdstat auth]
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=2*} [cmdstat auth]
|
||||
assert_equal {OK} [r AUTH foo pwd]
|
||||
# Test for No Pass user
|
||||
r acl setuser foo on ~* &* +@all nopass
|
||||
assert_equal {OK} [r AUTH foo allow]
|
||||
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny}
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=3} [cmdstat auth]
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=3*} [cmdstat auth]
|
||||
assert_equal {OK} [r AUTH foo nomatch]
|
||||
|
||||
# Validate that the Module added an ACL Log entry.
|
||||
|
|
@ -67,13 +67,13 @@ start_server {tags {"modules external:skip"}} {
|
|||
assert_equal $hello3_response [r HELLO 3 AUTH foo allow]
|
||||
# Validate denying AUTH for the HELLO cmd
|
||||
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo deny}
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=1*} [cmdstat hello]
|
||||
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch}
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=2*} [cmdstat hello]
|
||||
assert_error {*Auth denied by Misc Module*} {r HELLO 3 AUTH foo deny}
|
||||
assert_match {*calls=7,*,rejected_calls=0,failed_calls=3} [cmdstat hello]
|
||||
assert_match {*calls=7,*,rejected_calls=0,failed_calls=3*} [cmdstat hello]
|
||||
assert_error {*WRONGPASS*} {r HELLO 3 AUTH foo nomatch}
|
||||
assert_match {*calls=8,*,rejected_calls=0,failed_calls=4} [cmdstat hello]
|
||||
assert_match {*calls=8,*,rejected_calls=0,failed_calls=4*} [cmdstat hello]
|
||||
|
||||
# Validate that the Module added an ACL Log entry.
|
||||
set entry [lindex [r ACL LOG] 1]
|
||||
|
|
@ -97,10 +97,10 @@ start_server {tags {"modules external:skip"}} {
|
|||
r client setname client0
|
||||
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo deny setname client1}
|
||||
assert {[r client getname] eq {client0}}
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=1*} [cmdstat hello]
|
||||
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch setname client2}
|
||||
assert {[r client getname] eq {client0}}
|
||||
assert_match {*calls=4,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
|
||||
assert_match {*calls=4,*,rejected_calls=0,failed_calls=2*} [cmdstat hello]
|
||||
}
|
||||
|
||||
test {test blocking module AUTH} {
|
||||
|
|
@ -109,15 +109,15 @@ start_server {tags {"modules external:skip"}} {
|
|||
r acl setuser foo >pwd on ~* &* +@all
|
||||
assert_equal {OK} [r AUTH foo block_allow]
|
||||
assert_error {*Auth denied by Misc Module*} {r AUTH foo block_deny}
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
assert_error {*WRONGPASS*} {r AUTH foo nomatch}
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=2} [cmdstat auth]
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=2*} [cmdstat auth]
|
||||
assert_equal {OK} [r AUTH foo pwd]
|
||||
# Test for No Pass user
|
||||
r acl setuser foo on ~* &* +@all nopass
|
||||
assert_equal {OK} [r AUTH foo block_allow]
|
||||
assert_error {*Auth denied by Misc Module*} {r AUTH foo block_deny}
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=3} [cmdstat auth]
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=3*} [cmdstat auth]
|
||||
assert_equal {OK} [r AUTH foo nomatch]
|
||||
# Validate that every Blocking AUTH command took at least 500000 usec.
|
||||
set stats [cmdstat auth]
|
||||
|
|
@ -144,13 +144,13 @@ start_server {tags {"modules external:skip"}} {
|
|||
assert_equal $hello3_response [r HELLO 3 AUTH foo block_allow]
|
||||
# validate denying AUTH for the HELLO cmd
|
||||
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo block_deny}
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=1*} [cmdstat hello]
|
||||
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch}
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
|
||||
assert_match {*calls=6,*,rejected_calls=0,failed_calls=2*} [cmdstat hello]
|
||||
assert_error {*Auth denied by Misc Module*} {r HELLO 3 AUTH foo block_deny}
|
||||
assert_match {*calls=7,*,rejected_calls=0,failed_calls=3} [cmdstat hello]
|
||||
assert_match {*calls=7,*,rejected_calls=0,failed_calls=3*} [cmdstat hello]
|
||||
assert_error {*WRONGPASS*} {r HELLO 3 AUTH foo nomatch}
|
||||
assert_match {*calls=8,*,rejected_calls=0,failed_calls=4} [cmdstat hello]
|
||||
assert_match {*calls=8,*,rejected_calls=0,failed_calls=4*} [cmdstat hello]
|
||||
# Validate that every HELLO AUTH command took at least 500000 usec.
|
||||
set stats [cmdstat hello]
|
||||
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
|
||||
|
|
@ -178,10 +178,10 @@ start_server {tags {"modules external:skip"}} {
|
|||
r client setname client0
|
||||
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo block_deny setname client1}
|
||||
assert {[r client getname] eq {client0}}
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
|
||||
assert_match {*calls=3,*,rejected_calls=0,failed_calls=1*} [cmdstat hello]
|
||||
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch setname client2}
|
||||
assert {[r client getname] eq {client0}}
|
||||
assert_match {*calls=4,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
|
||||
assert_match {*calls=4,*,rejected_calls=0,failed_calls=2*} [cmdstat hello]
|
||||
# Validate that every HELLO AUTH SETNAME command took at least 500000 usec.
|
||||
set stats [cmdstat hello]
|
||||
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
|
||||
|
|
@ -205,7 +205,7 @@ start_server {tags {"modules external:skip"}} {
|
|||
|
||||
# Case 2 - Non Blocking Deny
|
||||
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny}
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
|
||||
r config resetstat
|
||||
|
||||
|
|
@ -214,7 +214,7 @@ start_server {tags {"modules external:skip"}} {
|
|||
|
||||
# Case 4 - Blocking Deny
|
||||
assert_error {*Auth denied by Misc Module*} {r AUTH foo block_deny}
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
|
||||
# Validate that every Blocking AUTH command took at least 500000 usec.
|
||||
set stats [cmdstat auth]
|
||||
|
|
@ -228,13 +228,13 @@ start_server {tags {"modules external:skip"}} {
|
|||
|
||||
# Case 6 - Non Blocking Deny via the second module.
|
||||
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny_two}
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
|
||||
r config resetstat
|
||||
|
||||
# Case 7 - All four auth callbacks "Skip" by not explicitly allowing or denying.
|
||||
assert_error {*WRONGPASS*} {r AUTH foo nomatch}
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
assert_equal {OK} [r AUTH foo pwd]
|
||||
|
||||
# Because we had to attempt all 4 callbacks, validate that the AUTH command took at least
|
||||
|
|
@ -283,7 +283,7 @@ start_server {tags {"modules external:skip"}} {
|
|||
r multi
|
||||
r AUTH foo block_allow
|
||||
assert_error {*ERR Blocking module command called from transaction*} {r exec}
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
}
|
||||
|
||||
test {Disabling Redis User during blocking module auth} {
|
||||
|
|
@ -300,7 +300,7 @@ start_server {tags {"modules external:skip"}} {
|
|||
wait_for_blocked_clients_count 0 500 10
|
||||
$rd flush
|
||||
assert_error {*WRONGPASS*} { $rd read }
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdstat auth]
|
||||
}
|
||||
|
||||
test {Killing a client in the middle of blocking module auth} {
|
||||
|
|
@ -354,7 +354,7 @@ start_server {tags {"modules external:skip"}} {
|
|||
$rd flush
|
||||
assert_equal [$rd read] "OK"
|
||||
set stats [cmdstat auth]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} $stats
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0*} $stats
|
||||
|
||||
# Validate that even the new blocking module auth cb which was registered in the middle of
|
||||
# blocking module auth is attempted - making it take twice the duration (2x 500000 us).
|
||||
|
|
@ -387,7 +387,7 @@ start_server {tags {"modules external:skip"}} {
|
|||
wait_for_blocked_clients_count 0 500 10
|
||||
$rd flush
|
||||
assert_equal [$rd read] "OK"
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdstat auth]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0*} [cmdstat auth]
|
||||
|
||||
# Validate that unloading the moduleauthtwo module does not unregister module auth cbs of
|
||||
# of the testacl module. Module based auth should succeed.
|
||||
|
|
@ -400,6 +400,6 @@ start_server {tags {"modules external:skip"}} {
|
|||
assert_error {*WRONGPASS*} {r AUTH foo block_allow}
|
||||
assert_error {*WRONGPASS*} {r AUTH foo allow_two}
|
||||
assert_error {*WRONGPASS*} {r AUTH foo allow}
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=3} [cmdstat auth]
|
||||
assert_match {*calls=5,*,rejected_calls=0,failed_calls=3*} [cmdstat auth]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,6 +204,14 @@ foreach command {SORT SORT_RO} {
|
|||
assert_equal [lsort -real $floats] [r sort mylist]
|
||||
}
|
||||
|
||||
test "SORT BY with smallest normal double 2.2250738585072012e-308" {
|
||||
r flushdb
|
||||
r lpush mylist a b
|
||||
r set weight_a 2.2250738585072012e-308
|
||||
r set weight_b 1
|
||||
assert_equal {a b} [r sort mylist BY weight_*]
|
||||
} {} {cluster:skip}
|
||||
|
||||
test "SORT with STORE returns zero if result is empty (github issue 224)" {
|
||||
r flushdb
|
||||
r sort foo{t} store bar{t}
|
||||
|
|
|
|||
|
|
@ -1277,6 +1277,24 @@ start_server {tags {"external:skip needs:debug"}} {
|
|||
assert_range [r hpttl myhash FIELDS 1 f3] 4500 5000
|
||||
}
|
||||
|
||||
test "HSETEX EX - field appears twice in FIELDS list with EX is allowed ($type)" {
|
||||
# The EX condition passes, so all fields must be set, and the last value wins.
|
||||
r del myhash
|
||||
r hset myhash f1 v1
|
||||
r hsetex myhash EX 100 FIELDS 2 f1 new1 f1 new2
|
||||
# Last value wins (same as plain HSET behavior with duplicate fields)
|
||||
assert_equal "new2" [r hget myhash f1]
|
||||
assert_range [r httl myhash FIELDS 1 f1] 80 100
|
||||
}
|
||||
|
||||
test "HSETEX FNX - field appears twice in FIELDS list with EX is allowed ($type)" {
|
||||
# The FNX condition passes, so all fields must be set, and the last value wins.
|
||||
r del myhash
|
||||
r hsetex myhash FNX EX 100 FIELDS 2 f1 new1 f1 new2
|
||||
assert_equal "new2" [r hget myhash f1]
|
||||
assert_range [r httl myhash FIELDS 1 f1] 80 100
|
||||
}
|
||||
|
||||
test "HSETEX - Test 'EX' flag ($type)" {
|
||||
r del myhash
|
||||
r hset myhash f1 v1 f2 v2
|
||||
|
|
@ -2359,6 +2377,11 @@ start_server {tags {"hash"}} {
|
|||
assert_error {*Parameter*numFields*should be greater than 0*} {r HEXPIRE myhash 60 FIELDS -1 f1}
|
||||
assert_error {*invalid number of fields*} {r HSETEX myhash FIELDS 0 f1 v1 EX 60}
|
||||
assert_error {*invalid number of fields*} {r HGETEX myhash FIELDS 0 f1 EX 60}
|
||||
set future_sec [expr {[clock seconds] + 60}]
|
||||
set future_ms [expr {[clock milliseconds] + 60000}]
|
||||
foreach {cmd expire} [list HEXPIRE 60 HPEXPIRE 60000 HEXPIREAT $future_sec HPEXPIREAT $future_ms] {
|
||||
assert_error {*wrong number of arguments*} [list r $cmd myhash $expire FIELDS 2147483647 f1]
|
||||
}
|
||||
|
||||
# Test missing FIELDS keyword
|
||||
assert_error {*unknown argument*} {r HEXPIRE myhash 60 2 f1 f2}
|
||||
|
|
|
|||
|
|
@ -2366,7 +2366,7 @@ foreach {pop} {BLPOP BLMPOP_RIGHT} {
|
|||
r LPUSH mylist 1
|
||||
wait_for_blocked_clients_count 0
|
||||
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdrstat blpop r]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0*} [cmdrstat blpop r]
|
||||
|
||||
$rd close
|
||||
}
|
||||
|
|
@ -2390,7 +2390,7 @@ foreach {pop} {BLPOP BLMPOP_RIGHT} {
|
|||
# unblock the client on timeout
|
||||
r client unblock $id timeout
|
||||
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdrstat blpop r]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0*} [cmdrstat blpop r]
|
||||
|
||||
$rd close
|
||||
}
|
||||
|
|
|
|||
|
|
@ -690,7 +690,7 @@ start_server {
|
|||
|
||||
# verify command stats, error stats and error counter work on failed blocked command
|
||||
assert_match {*count=1*} [errorrstat NOGROUP r]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdrstat xreadgroup r]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1*} [cmdrstat xreadgroup r]
|
||||
assert_equal [s total_error_replies] 1
|
||||
}
|
||||
|
||||
|
|
@ -1905,6 +1905,79 @@ start_server {
|
|||
}
|
||||
}
|
||||
|
||||
start_server {tags {"repl external:skip" "stream"}} {
|
||||
# Verify that XREADGROUP propagates a newly created consumer to
|
||||
# the replica in cases where no XCLAIM is generated (XCLAIM
|
||||
# implicitly creates the consumer, so explicit propagation is
|
||||
# only needed when it is absent). Two cases are tested:
|
||||
# 1. Without NOACK and no messages to deliver — no XCLAIM at all.
|
||||
# 2. With NOACK and messages delivered — NOACK skips PEL/XCLAIM.
|
||||
test "XREADGROUP propagates new consumer to replica" {
|
||||
set master [srv 0 client]
|
||||
set master_host [srv 0 host]
|
||||
set master_port [srv 0 port]
|
||||
|
||||
start_server {tags {"stream"}} {
|
||||
set replica [srv 0 client]
|
||||
|
||||
$replica replicaof $master_host $master_port
|
||||
wait_for_sync $replica
|
||||
|
||||
$master DEL mystream
|
||||
$master XADD mystream 1-0 f v
|
||||
$master XGROUP CREATE mystream grp 0
|
||||
|
||||
# Consume the only message so the stream has no
|
||||
# new messages pending for delivery.
|
||||
$master XREADGROUP GROUP grp c1 STREAMS mystream >
|
||||
$master XACK mystream grp 1-0
|
||||
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
# Case 1: XREADGROUP without NOACK for a brand-new
|
||||
# consumer when there are NO messages to deliver.
|
||||
# No XCLAIM is generated, so the consumer must be
|
||||
# explicitly propagated.
|
||||
set reply [$master XREADGROUP GROUP grp c2 STREAMS mystream >]
|
||||
assert_equal $reply {}
|
||||
|
||||
set master_consumers [$master XINFO CONSUMERS mystream grp]
|
||||
set master_names [lmap c $master_consumers {dict get $c name}]
|
||||
assert {[lsearch $master_names "c2"] >= 0}
|
||||
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
set replica_consumers [$replica XINFO CONSUMERS mystream grp]
|
||||
set replica_names [lmap c $replica_consumers {dict get $c name}]
|
||||
if {[lsearch $replica_names "c2"] < 0} {
|
||||
fail "Consumer 'c2' not found on replica (have: $replica_names)"
|
||||
}
|
||||
|
||||
# Case 2: XREADGROUP with NOACK for a brand-new consumer
|
||||
# when a message IS available. NOACK skips PEL/XCLAIM
|
||||
# entirely, so the consumer must be explicitly propagated
|
||||
# even though messages were delivered.
|
||||
$master XADD mystream 2-0 f v
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
set reply [$master XREADGROUP GROUP grp c3 NOACK STREAMS mystream >]
|
||||
assert {$reply ne {}}
|
||||
|
||||
set master_consumers [$master XINFO CONSUMERS mystream grp]
|
||||
set master_names [lmap c $master_consumers {dict get $c name}]
|
||||
assert {[lsearch $master_names "c3"] >= 0}
|
||||
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
set replica_consumers [$replica XINFO CONSUMERS mystream grp]
|
||||
set replica_names [lmap c $replica_consumers {dict get $c name}]
|
||||
if {[lsearch $replica_names "c3"] < 0} {
|
||||
fail "Consumer 'c3' not found on replica (have: $replica_names)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
if {!$::force_resp3} {
|
||||
test "XREADGROUP CLAIM field types are correct" {
|
||||
|
|
|
|||
|
|
@ -971,6 +971,26 @@ start_server {tags {"zset"}} {
|
|||
assert_equal {b 2 c 3} [r zinter 2 zseta{t} zsetb{t} aggregate max withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE with AGGREGATE COUNT - $encoding" {
|
||||
assert_equal 4 [r zunionstore zsetc{t} 2 zseta{t} zsetb{t} aggregate count]
|
||||
assert_equal {a 1 d 1 b 2 c 2} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNION/ZINTER with AGGREGATE COUNT - $encoding" {
|
||||
assert_equal {a 1 d 1 b 2 c 2} [r zunion 2 zseta{t} zsetb{t} aggregate count withscores]
|
||||
assert_equal {b 2 c 2} [r zinter 2 zseta{t} zsetb{t} aggregate count withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE with AGGREGATE COUNT and WEIGHTS - $encoding" {
|
||||
assert_equal 4 [r zunionstore zsetc{t} 2 zseta{t} zsetb{t} weights 2 3 aggregate count]
|
||||
assert_equal {a 2 d 3 b 5 c 5} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNION/ZINTER with AGGREGATE COUNT and WEIGHTS - $encoding" {
|
||||
assert_equal {a 2 d 3 b 5 c 5} [r zunion 2 zseta{t} zsetb{t} weights 2 3 aggregate count withscores]
|
||||
assert_equal {b 5 c 5} [r zinter 2 zseta{t} zsetb{t} weights 2 3 aggregate count withscores]
|
||||
}
|
||||
|
||||
test "ZINTERSTORE basics - $encoding" {
|
||||
assert_equal 2 [r zinterstore zsetc{t} 2 zseta{t} zsetb{t}]
|
||||
assert_equal {b 3 c 5} [r zrange zsetc{t} 0 -1 withscores]
|
||||
|
|
@ -1030,6 +1050,39 @@ start_server {tags {"zset"}} {
|
|||
assert_equal {b 2 c 3} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZINTERSTORE with AGGREGATE COUNT - $encoding" {
|
||||
assert_equal 2 [r zinterstore zsetc{t} 2 zseta{t} zsetb{t} aggregate count]
|
||||
assert_equal {b 2 c 2} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZINTERSTORE with AGGREGATE COUNT and WEIGHTS - $encoding" {
|
||||
assert_equal 2 [r zinterstore zsetc{t} 2 zseta{t} zsetb{t} weights 2 3 aggregate count]
|
||||
assert_equal {b 5 c 5} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE/ZINTERSTORE with AGGREGATE COUNT - 3 sets - $encoding" {
|
||||
r del s1{t} s2{t} s3{t} t1{t}
|
||||
r zadd s1{t} 1 foo 1 bar
|
||||
r zadd s2{t} 2 foo 2 bar
|
||||
r zadd s3{t} 3 foo
|
||||
|
||||
assert_equal 1 [r zinterstore t1{t} 3 s1{t} s2{t} s3{t} aggregate count]
|
||||
assert_equal {foo 3} [r zrange t1{t} 0 -1 withscores]
|
||||
|
||||
assert_equal 2 [r zunionstore t1{t} 3 s1{t} s2{t} s3{t} aggregate count]
|
||||
assert_equal {bar 2 foo 3} [r zrange t1{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE/ZINTERSTORE with AGGREGATE COUNT and WEIGHTS - 3 sets - $encoding" {
|
||||
assert_equal 1 [r zinterstore t1{t} 3 s1{t} s2{t} s3{t} weights 10 5 3 aggregate count]
|
||||
assert_equal {foo 18} [r zrange t1{t} 0 -1 withscores]
|
||||
|
||||
assert_equal 2 [r zunionstore t1{t} 3 s1{t} s2{t} s3{t} weights 10 5 3 aggregate count]
|
||||
assert_equal {bar 15 foo 18} [r zrange t1{t} 0 -1 withscores]
|
||||
|
||||
r del s1{t} s2{t} s3{t} t1{t}
|
||||
}
|
||||
|
||||
foreach cmd {ZUNIONSTORE ZINTERSTORE} {
|
||||
test "$cmd with +inf/-inf scores - $encoding" {
|
||||
r del zsetinf1{t} zsetinf2{t}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ GROUPS = {
|
|||
"geo": "COMMAND_GROUP_GEO",
|
||||
"stream": "COMMAND_GROUP_STREAM",
|
||||
"bitmap": "COMMAND_GROUP_BITMAP",
|
||||
"rate_limit": "COMMAND_GROUP_RATE_LIMIT",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -602,7 +603,8 @@ const char *COMMAND_GROUP_STR[] = {
|
|||
"geo",
|
||||
"stream",
|
||||
"bitmap",
|
||||
"module"
|
||||
"module",
|
||||
"rate_limit"
|
||||
};
|
||||
|
||||
const char *commandGroupStr(int index) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue