mirror of
https://github.com/redis/redis.git
synced 2026-05-28 04:02:46 -04:00
Merge branch 'unstable' into improve-stream_idmp_keys-time-complexity
This commit is contained in:
commit
060ae69b90
89 changed files with 5932 additions and 4809 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.5.90
|
||||
MODULE_VERSION = v8.7.90
|
||||
MODULE_REPO = https://github.com/redisearch/redisearch
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/search-community/redisearch.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,15 @@ 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
|
||||
# S Subkeyspace events, published with __subkeyspace@<db>__:<key> prefix.
|
||||
# T Subkeyevent events, published with __subkeyevent@<db>__:<event> prefix.
|
||||
# I Subkeyspaceitem events, published per subkey with
|
||||
# __subkeyspaceitem@<db>__:<key>\n<subkey> prefix.
|
||||
# V Subkeyspaceevent events, published with
|
||||
# __subkeyspaceevent@<db>__:<event>|<key> prefix.
|
||||
# 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 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 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. */
|
||||
};
|
||||
|
||||
|
|
|
|||
106
src/aof.c
106
src/aof.c
|
|
@ -2197,6 +2197,35 @@ int rioWriteStreamPendingEntry(rio *r, robj *key, const char *groupname, size_t
|
|||
return 1;
|
||||
}
|
||||
|
||||
/* Helper for rewriteStreamObject(): emit a single XNACK FORCE command that
|
||||
* reconstructs one or more NACKed (unowned) PEL entries sharing the same
|
||||
* delivery_count. `ids` points to an array of `count` streamIDs (at most
|
||||
* AOF_REWRITE_ITEMS_PER_CMD). Returns 0 on error, 1 on success. */
|
||||
int rioWriteStreamNackedEntries(rio *r, robj *key, const char *groupname,
|
||||
size_t groupname_len, streamID *ids,
|
||||
int count, uint64_t delivery_count) {
|
||||
serverAssert(count > 0 && count <= AOF_REWRITE_ITEMS_PER_CMD);
|
||||
|
||||
/* XNACK <key> <group> FAIL IDS <n> <id..> RETRYCOUNT <cnt> FORCE
|
||||
* 6 fixed tokens before IDs + count IDs + 3 fixed tokens after. */
|
||||
if (rioWriteBulkCount(r,'*',6+count+3) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"XNACK",5) == 0) return 0;
|
||||
if (rioWriteBulkObject(r,key) == 0) return 0;
|
||||
if (rioWriteBulkString(r,groupname,groupname_len) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"FAIL",4) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"IDS",3) == 0) return 0;
|
||||
if (rioWriteBulkLongLong(r,count) == 0) return 0;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (rioWriteBulkStreamID(r,&ids[i]) == 0) return 0;
|
||||
}
|
||||
|
||||
if (rioWriteBulkString(r,"RETRYCOUNT",10) == 0) return 0;
|
||||
if (rioWriteBulkLongLong(r,delivery_count) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"FORCE",5) == 0) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Helper for rewriteStreamObject(): emit the XGROUP CREATECONSUMER is
|
||||
* needed in order to create consumers that do not have any pending entries.
|
||||
* All this in the context of the specified key and group. */
|
||||
|
|
@ -2354,6 +2383,43 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
|
|||
raxStop(&ri_pel);
|
||||
}
|
||||
raxStop(&ri_cons);
|
||||
|
||||
/* Emit XNACK FORCE for NACKed (unowned) entries from the
|
||||
* NACK zone of the PEL time-ordered list
|
||||
* (pel_time_head..pel_nack_tail). Consecutive entries with
|
||||
* the same delivery_count are batched into a single command.
|
||||
*
|
||||
* nack_stop is the first node outside the NACK zone (or NULL
|
||||
* when the zone extends to the end of the PEL). When
|
||||
* pel_nack_tail is NULL (no NACKed entries) the guard below
|
||||
* skips the whole block. */
|
||||
streamNACK *nack_end = group->pel_nack_tail;
|
||||
if (nack_end != NULL) {
|
||||
streamID batch_ids[AOF_REWRITE_ITEMS_PER_CMD];
|
||||
streamNACK *nack_stop = nack_end->pel_next;
|
||||
streamNACK *nack = group->pel_time_head;
|
||||
int batch_count = 0;
|
||||
uint64_t batch_dc = 0;
|
||||
while (nack && nack != nack_stop) {
|
||||
if (batch_count == 0) batch_dc = nack->delivery_count;
|
||||
batch_ids[batch_count++] = nack->id;
|
||||
streamNACK *next = nack->pel_next;
|
||||
if (batch_count >= AOF_REWRITE_ITEMS_PER_CMD ||
|
||||
!next || next == nack_stop ||
|
||||
next->delivery_count != batch_dc)
|
||||
{
|
||||
if (rioWriteStreamNackedEntries(r,key,(char*)ri.key,
|
||||
ri.key_len,batch_ids,
|
||||
batch_count,batch_dc) == 0)
|
||||
{
|
||||
raxStop(&ri);
|
||||
return 0;
|
||||
}
|
||||
batch_count = 0;
|
||||
}
|
||||
nack = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
raxStop(&ri);
|
||||
}
|
||||
|
|
@ -2401,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. */
|
||||
|
|
@ -2456,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 {
|
||||
|
|
@ -2504,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);
|
||||
|
|
@ -2516,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 */
|
||||
|
|
@ -2534,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
|
||||
|
|
@ -2552,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;
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ void restoreCommand(client *c) {
|
|||
objectSetLRUOrLFU(kv, lfu_freq, lru_idle, lru_clock, 1000);
|
||||
keyModified(c,c->db,key,NULL,1);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"restore",key,c->db->id);
|
||||
KSN_INVALIDATE_KVOBJ(kv);
|
||||
|
||||
/* If we deleted a key that means REPLACE parameter was passed and the
|
||||
* destination key existed. */
|
||||
|
|
@ -1740,7 +1741,7 @@ unsigned int clusterDelKeysInSlot(unsigned int hashslot, int by_command) {
|
|||
* just moved to another node. The modules needs to know that these
|
||||
* keys are no longer available locally, so just send the keyspace
|
||||
* notification to the modules, but not to clients. */
|
||||
moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, server.db[0].id);
|
||||
moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, server.db[0].id, NULL, 0);
|
||||
}
|
||||
exitExecutionUnit();
|
||||
postExecutionUnitOperations();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -3649,7 +3663,7 @@ void asmActiveTrimDeleteKey(redisDb *db, robj *keyobj, int migration_cleanup) {
|
|||
* to another node. The modules need to know that these keys are no longer
|
||||
* available locally, so just send the keyspace notification to the modules,
|
||||
* but not to clients. */
|
||||
moduleNotifyKeyspaceEvent(NOTIFY_KEY_TRIMMED, "key_trimmed", keyobj, db->id);
|
||||
moduleNotifyKeyspaceEvent(NOTIFY_KEY_TRIMMED, "key_trimmed", keyobj, db->id, NULL, 0);
|
||||
} else {
|
||||
/* Not a migration cleanup, the key is really deleted from the database,
|
||||
* need to notify the clients. */
|
||||
|
|
|
|||
170
src/commands.def
170
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 ********************/
|
||||
|
|
@ -10574,6 +10640,7 @@ commandHistory XINFO_STREAM_History[] = {
|
|||
{"7.0.0","Added the `max-deleted-entry-id`, `entries-added`, `recorded-first-entry-id`, `entries-read` and `lag` fields"},
|
||||
{"7.2.0","Added the `active-time` field, and changed the meaning of `seen-time`."},
|
||||
{"8.6.0","Added the `idmp-duration`, `idmp-maxsize`, `pids-tracked`, `iids-tracked`, `iids-added` and `iids-duplicates` fields for IDMP tracking."},
|
||||
{"8.8.0","Added the `nacked-count` field to consumer groups in `FULL` output."},
|
||||
};
|
||||
#endif
|
||||
|
||||
|
|
@ -10606,7 +10673,7 @@ struct COMMAND_STRUCT XINFO_Subcommands[] = {
|
|||
{MAKE_CMD("consumers","Returns a list of the consumers in a consumer group.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XINFO_CONSUMERS_History,1,XINFO_CONSUMERS_Tips,1,xinfoCommand,4,CMD_READONLY,ACL_CATEGORY_STREAM,XINFO_CONSUMERS_Keyspecs,1,NULL,2),.args=XINFO_CONSUMERS_Args},
|
||||
{MAKE_CMD("groups","Returns a list of the consumer groups of a stream.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XINFO_GROUPS_History,1,XINFO_GROUPS_Tips,0,xinfoCommand,3,CMD_READONLY,ACL_CATEGORY_STREAM,XINFO_GROUPS_Keyspecs,1,NULL,1),.args=XINFO_GROUPS_Args},
|
||||
{MAKE_CMD("help","Returns helpful text about the different subcommands.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XINFO_HELP_History,0,XINFO_HELP_Tips,0,xinfoCommand,2,CMD_LOADING|CMD_STALE,ACL_CATEGORY_STREAM,XINFO_HELP_Keyspecs,0,NULL,0)},
|
||||
{MAKE_CMD("stream","Returns information about a stream.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XINFO_STREAM_History,4,XINFO_STREAM_Tips,0,xinfoCommand,-3,CMD_READONLY,ACL_CATEGORY_STREAM,XINFO_STREAM_Keyspecs,1,NULL,2),.args=XINFO_STREAM_Args},
|
||||
{MAKE_CMD("stream","Returns information about a stream.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XINFO_STREAM_History,5,XINFO_STREAM_Tips,0,xinfoCommand,-3,CMD_READONLY,ACL_CATEGORY_STREAM,XINFO_STREAM_Keyspecs,1,NULL,2),.args=XINFO_STREAM_Args},
|
||||
{0}
|
||||
};
|
||||
|
||||
|
|
@ -10651,6 +10718,48 @@ struct COMMAND_ARG XLEN_Args[] = {
|
|||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** XNACK ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* XNACK history */
|
||||
#define XNACK_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* XNACK tips */
|
||||
#define XNACK_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* XNACK key specs */
|
||||
keySpec XNACK_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* XNACK mode argument table */
|
||||
struct COMMAND_ARG XNACK_mode_Subargs[] = {
|
||||
{MAKE_ARG("silent",ARG_TYPE_PURE_TOKEN,-1,"SILENT",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("fail",ARG_TYPE_PURE_TOKEN,-1,"FAIL",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("fatal",ARG_TYPE_PURE_TOKEN,-1,"FATAL",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XNACK ids argument table */
|
||||
struct COMMAND_ARG XNACK_ids_Subargs[] = {
|
||||
{MAKE_ARG("numids",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("id",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XNACK argument table */
|
||||
struct COMMAND_ARG XNACK_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("group",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("mode",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,3,NULL),.subargs=XNACK_mode_Subargs},
|
||||
{MAKE_ARG("ids",ARG_TYPE_BLOCK,-1,"IDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=XNACK_ids_Subargs},
|
||||
{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,"RETRYCOUNT",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
{MAKE_ARG("force",ARG_TYPE_PURE_TOKEN,-1,"FORCE",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
/********** XPENDING ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
|
|
@ -11039,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
|
||||
|
|
@ -11874,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},
|
||||
|
|
@ -11945,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},
|
||||
|
|
@ -11969,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},
|
||||
|
|
@ -11984,6 +12068,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("xidmprecord","An internal command for setting IDMP metadata on an existing stream message.","O(1)","8.6.2",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XIDMPRECORD_History,0,XIDMPRECORD_Tips,0,xidmprecordCommand,5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STREAM,XIDMPRECORD_Keyspecs,1,NULL,4),.args=XIDMPRECORD_Args},
|
||||
{MAKE_CMD("xinfo","A container for stream introspection commands.","Depends on subcommand.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XINFO_History,0,XINFO_Tips,0,NULL,-2,0,0,XINFO_Keyspecs,0,NULL,0),.subcommands=XINFO_Subcommands},
|
||||
{MAKE_CMD("xlen","Return the number of messages in a stream.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XLEN_History,0,XLEN_Tips,0,xlenCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STREAM,XLEN_Keyspecs,1,NULL,1),.args=XLEN_Args},
|
||||
{MAKE_CMD("xnack","Releases claimed messages back to the group's PEL without acknowledging them, making them available for re-delivery.","O(1) for each message ID processed.","8.8.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XNACK_History,0,XNACK_Tips,0,xnackCommand,-7,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XNACK_Keyspecs,1,NULL,6),.args=XNACK_Args},
|
||||
{MAKE_CMD("xpending","Returns the information and entries from a stream consumer group's pending entries list.","O(N) with N being the number of elements returned, so asking for a small fixed number of entries per call is O(1). O(M), where M is the total number of entries scanned when used with the IDLE filter. When the command returns just the summary and the list of consumers is small, it runs in O(1) time; otherwise, an additional O(N) time for iterating every consumer.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XPENDING_History,1,XPENDING_Tips,1,xpendingCommand,-3,CMD_READONLY,ACL_CATEGORY_STREAM,XPENDING_Keyspecs,1,NULL,3),.args=XPENDING_Args},
|
||||
{MAKE_CMD("xrange","Returns the messages from a stream within a range of IDs.","O(N) with N being the number of elements being returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1).","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XRANGE_History,1,XRANGE_Tips,0,xrangeCommand,-4,CMD_READONLY,ACL_CATEGORY_STREAM,XRANGE_Keyspecs,1,NULL,4),.args=XRANGE_Args},
|
||||
{MAKE_CMD("xread","Returns messages from multiple streams with IDs greater than the ones requested. Blocks until a message is available otherwise.",NULL,"5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XREAD_History,0,XREAD_Tips,0,xreadCommand,-4,CMD_BLOCKING|CMD_READONLY,ACL_CATEGORY_STREAM,XREAD_Keyspecs,1,xreadGetKeys,3),.args=XREAD_Args},
|
||||
|
|
@ -11997,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},
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@
|
|||
},
|
||||
{
|
||||
"const": "transactions"
|
||||
},
|
||||
{
|
||||
"const": "rate_limit"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,10 @@
|
|||
[
|
||||
"8.6.0",
|
||||
"Added the `idmp-duration`, `idmp-maxsize`, `pids-tracked`, `iids-tracked`, `iids-added` and `iids-duplicates` fields for IDMP tracking."
|
||||
],
|
||||
[
|
||||
"8.8.0",
|
||||
"Added the `nacked-count` field to consumer groups in `FULL` output."
|
||||
]
|
||||
],
|
||||
"function": "xinfoCommand",
|
||||
|
|
@ -298,6 +302,10 @@
|
|||
"description": "total number of unacknowledged entries",
|
||||
"type": "integer"
|
||||
},
|
||||
"nacked-count": {
|
||||
"description": "number of entries currently in the nacked zone",
|
||||
"type": "integer"
|
||||
},
|
||||
"pending": {
|
||||
"description": "data about all of the unacknowledged entries",
|
||||
"type": "array",
|
||||
|
|
|
|||
102
src/commands/xnack.json
Normal file
102
src/commands/xnack.json
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"XNACK": {
|
||||
"summary": "Releases claimed messages back to the group's PEL without acknowledging them, making them available for re-delivery.",
|
||||
"complexity": "O(1) for each message ID processed.",
|
||||
"group": "stream",
|
||||
"since": "8.8.0",
|
||||
"arity": -7,
|
||||
"function": "xnackCommand",
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"STREAM"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "group",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "mode",
|
||||
"type": "oneof",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "silent",
|
||||
"type": "pure-token",
|
||||
"token": "SILENT"
|
||||
},
|
||||
{
|
||||
"name": "fail",
|
||||
"type": "pure-token",
|
||||
"token": "FAIL"
|
||||
},
|
||||
{
|
||||
"name": "fatal",
|
||||
"type": "pure-token",
|
||||
"token": "FATAL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ids",
|
||||
"token": "IDS",
|
||||
"type": "block",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "numids",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"token": "RETRYCOUNT",
|
||||
"name": "count",
|
||||
"type": "integer",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "force",
|
||||
"token": "FORCE",
|
||||
"type": "pure-token",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "The number of messages successfully NACKed (released back to the group PEL).",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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$lshzxeKEtmdnocrSTIV'.";
|
||||
return 0;
|
||||
}
|
||||
server.notify_keyspace_events = flags;
|
||||
|
|
|
|||
24
src/db.c
24
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;
|
||||
|
|
@ -1525,6 +1525,7 @@ void delexCommand(client *c) {
|
|||
rewriteClientCommandVector(c, 2, shared.del, key);
|
||||
keyModified(c, c->db, key, NULL, 1);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, c->db->id);
|
||||
KSN_INVALIDATE_KVOBJ(o);
|
||||
server.dirty++;
|
||||
}
|
||||
|
||||
|
|
@ -1758,7 +1759,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 */
|
||||
|
|
@ -1913,7 +1915,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
|||
* COUNT, so if the hash table is in a pathological state (very
|
||||
* sparsely populated) we avoid to block too much time at the cost
|
||||
* of returning no or very few elements. */
|
||||
long maxiterations = count*10;
|
||||
long maxiterations = (count > LONG_MAX / 10) ? LONG_MAX : count * 10;
|
||||
|
||||
/* We pass scanData which have three pointers to the callback:
|
||||
* 1. data.keys: the list to which it will add new elements;
|
||||
|
|
@ -2251,10 +2253,9 @@ void renameGenericCommand(client *c, int nx) {
|
|||
|
||||
keyModified(c,c->db,c->argv[1],NULL,1);
|
||||
keyModified(c,c->db,c->argv[2],o,1);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from",
|
||||
c->argv[1],c->db->id);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_to",
|
||||
c->argv[2],c->db->id);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "rename_from", c->argv[1],c->db->id);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "rename_to", c->argv[2],c->db->id);
|
||||
KSN_INVALIDATE_KVOBJ(o);
|
||||
if (overwritten) {
|
||||
notifyKeyspaceEvent(NOTIFY_OVERWRITTEN, "overwritten", c->argv[2], c->db->id);
|
||||
if (desttype != srctype)
|
||||
|
|
@ -2349,10 +2350,9 @@ void moveCommand(client *c) {
|
|||
|
||||
keyModified(c,src,c->argv[1],NULL,1);
|
||||
keyModified(c,dst,c->argv[1],kv,1);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,
|
||||
"move_from",c->argv[1],src->id);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,
|
||||
"move_to",c->argv[1],dst->id);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "move_from", c->argv[1],src->id);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "move_to", c->argv[1],dst->id);
|
||||
KSN_INVALIDATE_KVOBJ(kv);
|
||||
|
||||
server.dirty++;
|
||||
addReply(c,shared.cone);
|
||||
|
|
@ -2442,6 +2442,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;
|
||||
|
|
@ -2474,6 +2475,7 @@ void copyCommand(client *c) {
|
|||
/* OK! key copied. Signal modification */
|
||||
keyModified(c,dst,c->argv[2],kvCopy,1);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"copy_to",c->argv[2],dst->id);
|
||||
KSN_INVALIDATE_KVOBJ(kvCopy);
|
||||
|
||||
/* `delete` implies the destination key was overwritten */
|
||||
if (delete) {
|
||||
|
|
|
|||
23
src/debug.c
23
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;
|
||||
|
|
@ -436,6 +446,8 @@ void debugCommand(client *c) {
|
|||
" Show low level info about `key` and associated value.",
|
||||
"DROP-CLUSTER-PACKET-FILTER <packet-type>",
|
||||
" Drop all packets that match the filtered type. Set to -1 allow all packets.",
|
||||
"ENABLE-KEYMETA-RUNTIME-REGISTRATION <0|1>",
|
||||
" Allow keymeta class registration outside server startup (for testing).",
|
||||
"OOM",
|
||||
" Crash the server simulating an out-of-memory error.",
|
||||
"PANIC",
|
||||
|
|
@ -894,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;
|
||||
|
||||
|
|
@ -927,6 +939,11 @@ NULL
|
|||
{
|
||||
server.skip_checksum_validation = atoi(c->argv[2]->ptr);
|
||||
addReply(c,shared.ok);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"enable-keymeta-runtime-registration") &&
|
||||
c->argc == 3)
|
||||
{
|
||||
server.allow_keymeta_registration = atoi(c->argv[2]->ptr);
|
||||
addReply(c,shared.ok);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"aof-flush-sleep") &&
|
||||
c->argc == 3)
|
||||
{
|
||||
|
|
@ -1295,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
|
||||
}
|
||||
|
|
|
|||
73
src/defrag.c
73
src/defrag.c
|
|
@ -855,51 +855,52 @@ void defragRadixTree(rax **raxref, int defrag_data, raxDefragFunction *element_c
|
|||
raxStop(&ri);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
streamCG *cg;
|
||||
streamConsumer *c;
|
||||
} PendingEntryContext;
|
||||
|
||||
void* defragStreamConsumerPendingEntry(raxIterator *ri, void *privdata) {
|
||||
PendingEntryContext *ctx = privdata;
|
||||
streamConsumer *c = privdata;
|
||||
streamNACK *nack = ri->data;
|
||||
/* NACKs are already defragged by the CG PEL walk (defragStreamCGPendingEntry).
|
||||
* cgroup_ref_node->value is also updated there for all NACKs (including
|
||||
* unowned NACK-zone entries that have no consumer PEL walk).
|
||||
* Here we only fix up the back-pointer to the possibly-relocated consumer. */
|
||||
nack->consumer = c;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* defragStreamCGPendingEntry(raxIterator *ri, void *privdata) {
|
||||
streamCG *cg = privdata;
|
||||
streamNACK *nack = ri->data, *newnack;
|
||||
nack->consumer = ctx->c; /* update nack pointer to consumer */
|
||||
nack->cgroup_ref_node->value = ctx->cg; /* Update the value of cgroups_ref node to the consumer group. */
|
||||
/* Update cgroup_ref_node to the possibly-relocated CG for every NACK.
|
||||
* Consumer-owned entries will get this overwritten again redundantly by
|
||||
* defragStreamConsumerPendingEntry; unowned (NACK zone) entries have no
|
||||
* consumer PEL walk, so this is their only chance. */
|
||||
nack->cgroup_ref_node->value = cg;
|
||||
newnack = activeDefragAlloc(nack);
|
||||
if (newnack) {
|
||||
/* Update consumer group pointer to the nack. */
|
||||
void *prev;
|
||||
raxInsert(ctx->cg->pel, ri->key, ri->key_len, newnack, &prev);
|
||||
serverAssert(prev==nack);
|
||||
|
||||
/* Update the doubly-linked list pointers in adjacent nacks.
|
||||
* When we move a nack to a new address, we need to update the
|
||||
* pel_prev->pel_next and pel_next->pel_prev pointers. */
|
||||
/* If this NACK is owned by a consumer, update the consumer's PEL. */
|
||||
if (newnack->consumer) {
|
||||
void *prev;
|
||||
raxInsert(newnack->consumer->pel, ri->key, ri->key_len, newnack, &prev);
|
||||
serverAssert(prev == nack);
|
||||
}
|
||||
if (newnack->pel_prev) {
|
||||
newnack->pel_prev->pel_next = newnack;
|
||||
} else {
|
||||
/* This is the head of the list */
|
||||
ctx->cg->pel_time_head = newnack;
|
||||
cg->pel_time_head = newnack;
|
||||
}
|
||||
if (newnack->pel_next) {
|
||||
newnack->pel_next->pel_prev = newnack;
|
||||
} else {
|
||||
/* This is the tail of the list */
|
||||
ctx->cg->pel_time_tail = newnack;
|
||||
cg->pel_time_tail = newnack;
|
||||
}
|
||||
if (cg->pel_nack_tail == nack) {
|
||||
cg->pel_nack_tail = newnack;
|
||||
}
|
||||
}
|
||||
return newnack;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
stream *s;
|
||||
streamCG *cg;
|
||||
} StreamConsumerContext;
|
||||
|
||||
void* defragStreamConsumer(raxIterator *ri, void *privdata) {
|
||||
StreamConsumerContext *ctx = privdata;
|
||||
stream *s = ctx->s;
|
||||
streamCG *cg = ctx->cg;
|
||||
stream *s = privdata;
|
||||
streamConsumer *c = ri->data;
|
||||
void *newc = activeDefragAlloc(c);
|
||||
if (newc) {
|
||||
|
|
@ -911,8 +912,7 @@ void* defragStreamConsumer(raxIterator *ri, void *privdata) {
|
|||
if (c->pel) {
|
||||
/* Update pel back-pointer to new stream */
|
||||
c->pel->alloc_size = &s->alloc_size;
|
||||
PendingEntryContext pel_ctx = {cg, c};
|
||||
defragRadixTree(&c->pel, 0, defragStreamConsumerPendingEntry, &pel_ctx);
|
||||
defragRadixTree(&c->pel, 0, defragStreamConsumerPendingEntry, c);
|
||||
}
|
||||
return newc; /* returns NULL if c was not defragged */
|
||||
}
|
||||
|
|
@ -925,14 +925,12 @@ void* defragStreamConsumerGroup(raxIterator *ri, void *privdata) {
|
|||
if (cg->pel) {
|
||||
/* Update pel back-pointer to new stream */
|
||||
cg->pel->alloc_size = &s->alloc_size;
|
||||
defragRadixTree(&cg->pel, 0, NULL, NULL);
|
||||
defragRadixTree(&cg->pel, 0, defragStreamCGPendingEntry, cg);
|
||||
}
|
||||
/* pel_time_head/tail are just pointers to NACKs in pel, no separate defrag needed */
|
||||
if (cg->consumers) {
|
||||
/* Update consumers back-pointer to new stream */
|
||||
cg->consumers->alloc_size = &s->alloc_size;
|
||||
StreamConsumerContext consumer_ctx = {s, cg};
|
||||
defragRadixTree(&cg->consumers, 0, defragStreamConsumer, &consumer_ctx);
|
||||
defragRadixTree(&cg->consumers, 0, defragStreamConsumer, s);
|
||||
}
|
||||
return cg;
|
||||
}
|
||||
|
|
@ -1165,6 +1163,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 {
|
||||
|
|
|
|||
|
|
@ -836,6 +836,7 @@ void expireGenericCommand(client *c, long long basetime, int unit) {
|
|||
|
||||
keyModified(c,c->db,key,kv,1);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
|
||||
KSN_INVALIDATE_KVOBJ(kv);
|
||||
server.dirty++;
|
||||
return;
|
||||
}
|
||||
|
|
@ -913,6 +914,7 @@ void persistCommand(client *c) {
|
|||
if (removeExpire(c->db,c->argv[1])) {
|
||||
keyModified(c,c->db,c->argv[1],kv,1);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"persist",c->argv[1],c->db->id);
|
||||
KSN_INVALIDATE_KVOBJ(kv);
|
||||
addReply(c,shared.cone);
|
||||
server.dirty++;
|
||||
} else {
|
||||
|
|
|
|||
620
src/fast_float_strtod.c
Normal file
620
src/fast_float_strtod.c
Normal file
|
|
@ -0,0 +1,620 @@
|
|||
/* 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;
|
||||
|
||||
double value;
|
||||
if (mantissa <= MAX_MANTISSA_FAST_PATH) {
|
||||
/* Clinger fast path: all operands exact in double precision,
|
||||
* single multiply/divide produces a correctly-rounded result. */
|
||||
value = (double)mantissa;
|
||||
if (exponent < 0) value = value / powers_of_ten[-exponent];
|
||||
else if (exponent > 0) value = value * powers_of_ten[exponent];
|
||||
} else {
|
||||
#ifdef __SIZEOF_INT128__
|
||||
/* Widened fast path for 17-19 significant-digit mantissas.
|
||||
*
|
||||
* (double)mantissa alone loses up to 11 bits when mantissa > 2^53,
|
||||
* so the existing Clinger path would yield up to 1 ULP vs strtod.
|
||||
* We recover full precision by doing the multiply/divide in 128-bit
|
||||
* integer arithmetic (correctly-rounded by construction). Cases
|
||||
* outside the supported exponent range fall through to strtod.
|
||||
*
|
||||
* Requires __uint128_t (GCC/Clang builtin, available on every 64-bit
|
||||
* target Redis supports). 32-bit builds take the strtod() fallback. */
|
||||
if (exponent < -19 || exponent > 19) return 0;
|
||||
|
||||
if (exponent >= 0) {
|
||||
/* (mantissa * 10^e) fits in 128 bits. Convert exactly: the
|
||||
* single (double) cast from __uint128_t rounds to nearest. */
|
||||
__uint128_t prod = (__uint128_t)mantissa * (uint64_t)powers_of_ten[exponent];
|
||||
uint64_t hi = (uint64_t)(prod >> 64);
|
||||
uint64_t lo = (uint64_t)prod;
|
||||
/* (double)hi * 2^64 has no rounding error (hi up to 2^64-1 rounds
|
||||
* once, then * 2^64 is exact). Adding lo rounds once. Total:
|
||||
* matches strtod on every tested case with e in [0,19]. */
|
||||
value = (double)hi * 18446744073709551616.0 + (double)lo;
|
||||
} else {
|
||||
/* mantissa / 10^|e|: scale numerator up by 2^64 before integer
|
||||
* division to preserve precision, then descale by multiplying by
|
||||
* 2^-64 (exact power-of-two scaling, does not round). The single
|
||||
* (double) cast of the integer quotient produces IEEE round-to-
|
||||
* nearest-even, matching strtod() bit-exactly for every tested
|
||||
* 16-19 significant digit case. */
|
||||
uint64_t divisor = (uint64_t)powers_of_ten[-exponent];
|
||||
__uint128_t scaled = (__uint128_t)mantissa << 64;
|
||||
__uint128_t q = scaled / divisor;
|
||||
uint64_t hi = (uint64_t)(q >> 64);
|
||||
uint64_t lo = (uint64_t)q;
|
||||
value = ((double)hi * 18446744073709551616.0 + (double)lo)
|
||||
* 5.421010862427522170037e-20; /* 2^-64 */
|
||||
}
|
||||
#else
|
||||
/* 32-bit target without __uint128_t: fall through to the strtod()
|
||||
* fallback. Correctness is preserved (it's the same path that shipped
|
||||
* in 8.8-M02); only the perf gain is 64-bit-target-specific. */
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
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},
|
||||
|
||||
/* Widened fast path: mantissa > 2^53 (==9007199254740992), |exp| in [1,19].
|
||||
* These cover the __uint128_t code path that avoids the strtod() fallback.
|
||||
* Each expected value is the IEEE-correct round-to-nearest double. */
|
||||
|
||||
/* 17-19 significant digit mantissas — negative exponent (scores in [0,1)) */
|
||||
{"0.49606648747577575", 0.49606648747577575}, /* 17 sig digits, ZADD hot case */
|
||||
{"0.8731899671198792", 0.8731899671198792}, /* 16 sig digits */
|
||||
{"0.34912978268081996", 0.34912978268081996}, /* 17 sig digits */
|
||||
{"0.0033318113277969186", 0.0033318113277969186}, /* 19 sig digits after leading-zero strip */
|
||||
{"0.9955843393406656", 0.9955843393406656},
|
||||
{"0.999999999999999", 0.999999999999999}, /* repunit-ish, ULP boundary */
|
||||
|
||||
/* Mantissa just above 2^53: triggers the widened path */
|
||||
{"9007199254740993.0", 9007199254740992.0}, /* rounds down */
|
||||
{"9007199254740995.0", 9007199254740996.0}, /* ties-to-even up */
|
||||
{"9007199254740996.0", 9007199254740996.0},
|
||||
{"10000000000000000", 1e16}, /* exact 10^16, mantissa = 10^16 */
|
||||
{"99999999999999999", 1e17}, /* one less than 10^17 */
|
||||
|
||||
/* 18-digit mantissa with various exponents */
|
||||
{"1234567890123456789", 1.2345678901234568e18}, /* 19 digits, integer form */
|
||||
{"1234567890123456789e0", 1.2345678901234568e18},
|
||||
{"1234567890123456789e-5", 12345678901234.568},
|
||||
{"1234567890123456789e-19", 0.12345678901234568},
|
||||
{"1234567890123456789e5", 1.2345678901234569e23}, /* 19-digit mantissa × 10^5 — widened path */
|
||||
|
||||
/* Boundary: exponent exactly ±19 (widened-path limit) */
|
||||
{"1234567890123.456789e-19", 1.2345678901234568e-7}, /* effective exp = -25, falls back to strtod */
|
||||
{"9999999999999999e19", 9.999999999999999e34},
|
||||
{"9999999999999999e-19", 9.999999999999999e-4},
|
||||
|
||||
/* Negative numbers exercising the widened path */
|
||||
{"-0.49606648747577575", -0.49606648747577575},
|
||||
{"-9007199254740993", -9007199254740992.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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ int extractLongLatOrReply(client *c, robj **argv, double *xy) {
|
|||
if (xy[0] < GEO_LONG_MIN || xy[0] > GEO_LONG_MAX ||
|
||||
xy[1] < GEO_LAT_MIN || xy[1] > GEO_LAT_MAX) {
|
||||
addReplyErrorFormat(c,
|
||||
"-ERR invalid longitude,latitude pair %f,%f\r\n",xy[0],xy[1]);
|
||||
"invalid longitude,latitude pair %f,%f",xy[0],xy[1]);
|
||||
return C_ERR;
|
||||
}
|
||||
return C_OK;
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ int rdbLoadSkipMetaIfAllowed(rio *rdb, char *cname, int flags) {
|
|||
*
|
||||
* Note: rdbLoadCheckModuleValue() reads opcodes until it finds RDB_MODULE_OPCODE_EOF,
|
||||
* so it consumes the EOF marker as well. We don't need to read it separately. */
|
||||
robj *dummy = rdbLoadCheckModuleValue(rdb, cname);
|
||||
robj *dummy = rdbLoadCheckModuleValue(rdb, cname, 1);
|
||||
if (dummy == NULL) {
|
||||
serverLog(LL_WARNING, "Corrupted metadata value for class '%s'", cname);
|
||||
return -1;
|
||||
|
|
@ -743,25 +743,25 @@ KeyMetaClassId keyMetaClassCreate(RedisModule *context, const char *name,
|
|||
|
||||
/* Check for name conflicts using 4-char name. Allow reuse of RELEASED; forbid if INUSE. */
|
||||
int alreayReleased;
|
||||
int slot = keyMetaClassLookupByName(name, &alreayReleased);
|
||||
int keyMetaId = keyMetaClassLookupByName(name, &alreayReleased);
|
||||
|
||||
if (alreayReleased) {
|
||||
/* If already released, then reuse the slot. */
|
||||
/* If already released, then reuse the keyMetaId. */
|
||||
} else {
|
||||
/* Assert class is registered for first time */
|
||||
serverAssert(slot == -1);
|
||||
serverAssert(keyMetaId == -1);
|
||||
|
||||
/* Find free slot */
|
||||
/* Find free keyMetaId */
|
||||
for (int i = KEY_META_ID_MODULE_FIRST; i <= KEY_META_ID_MODULE_LAST; i++) {
|
||||
if (keyMetaClass[i].state == CLASS_STATE_FREE) {
|
||||
slot = i;
|
||||
keyMetaId = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot == -1) return 0; /* no free slots */
|
||||
if (keyMetaId == -1) return 0; /* no free keyMetaId */
|
||||
}
|
||||
|
||||
KeyMetaClass *pKeyMetaClass = &keyMetaClass[slot];
|
||||
KeyMetaClass *pKeyMetaClass = &keyMetaClass[keyMetaId];
|
||||
|
||||
/* Store 4-char short name */
|
||||
memcpy(pKeyMetaClass->name, name, KM_NAME_LEN);
|
||||
|
|
@ -774,7 +774,7 @@ KeyMetaClassId keyMetaClassCreate(RedisModule *context, const char *name,
|
|||
pKeyMetaClass->state = CLASS_STATE_INUSE;
|
||||
pKeyMetaClass->classSpecEncoded = classSpecEncoded;
|
||||
KM_SET_CONST_CONF(pKeyMetaClass->conf) = *conf; /* Copy config as is. */
|
||||
return slot; /* Return handle (1..7). */
|
||||
return keyMetaId; /* Return handle (1..7). */
|
||||
}
|
||||
|
||||
/* Destroy (release) a class by its ID. Returns 1 on success, 0 on failure. */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
194
src/module.c
194
src/module.c
|
|
@ -303,6 +303,9 @@ static pthread_mutex_t moduleGIL = PTHREAD_MUTEX_INITIALIZER;
|
|||
/* Function pointer type for keyspace event notification subscriptions from modules. */
|
||||
typedef int (*RedisModuleNotificationFunc) (RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key);
|
||||
|
||||
/* Function pointer type for keyspace event notifications with subkeys from modules. */
|
||||
typedef void (*RedisModuleNotificationWithSubkeysFunc)(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key, RedisModuleString **subkeys, int count);
|
||||
|
||||
/* Function pointer type for post jobs */
|
||||
typedef void (*RedisModulePostNotificationJobFunc) (RedisModuleCtx *ctx, void *pd);
|
||||
|
||||
|
|
@ -313,8 +316,12 @@ typedef struct RedisModuleKeyspaceSubscriber {
|
|||
RedisModule *module;
|
||||
/* Notification callback in the module*/
|
||||
RedisModuleNotificationFunc notify_callback;
|
||||
/* Extended notification callback with subkeys */
|
||||
RedisModuleNotificationWithSubkeysFunc notify_callback_with_subkeys;
|
||||
/* A bit mask of the events the module is interested in */
|
||||
int event_mask;
|
||||
/* Delivery flags for subkey notifications, controlling when the callback is invoked. */
|
||||
int flags;
|
||||
/* Active flag set on entry, to avoid reentrant subscribers
|
||||
* calling themselves */
|
||||
int active;
|
||||
|
|
@ -332,6 +339,11 @@ typedef struct RedisModulePostExecUnitJob {
|
|||
/* The module keyspace notification subscribers list */
|
||||
static list *moduleKeyspaceSubscribers;
|
||||
|
||||
/* Cached event types that have at least one subscriber.
|
||||
* Updated on subscribe/unsubscribe to avoid traversing the list on every event. */
|
||||
static int moduleKeyspaceSubscribersTypes = 0;
|
||||
static int moduleKeyspaceSubscribersWithSubkeysTypes = 0;
|
||||
|
||||
/* The module post keyspace jobs list */
|
||||
static list *modulePostExecUnitJobs;
|
||||
|
||||
|
|
@ -783,6 +795,23 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Update the cached subscriber types by walking the subscriber list.
|
||||
* Called after subscribe/unsubscribe operations. */
|
||||
static void moduleUpdateKeyspaceSubscribersTypes(void) {
|
||||
int mask = 0, subkeys_mask = 0;
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(moduleKeyspaceSubscribers,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
RedisModuleKeyspaceSubscriber *sub = ln->value;
|
||||
mask |= sub->event_mask;
|
||||
if (sub->notify_callback_with_subkeys)
|
||||
subkeys_mask |= sub->event_mask;
|
||||
}
|
||||
moduleKeyspaceSubscribersTypes = mask;
|
||||
moduleKeyspaceSubscribersWithSubkeysTypes = subkeys_mask;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
* Service API exported to modules
|
||||
*
|
||||
|
|
@ -4225,6 +4254,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4480,9 +4510,10 @@ int RM_SetAbsExpire(RedisModuleKey *key, mstime_t expire) {
|
|||
*
|
||||
* Note: the metadata class name "AAAAAAAAA" is reserved and produces an error.
|
||||
*
|
||||
* If RM_CreateKeyMetaClass() is called outside of RedisModule_OnLoad() function,
|
||||
* there is already a metadata class registered with the same name,
|
||||
* or if the metadata class name or metaver is invalid, a negative value is returned.
|
||||
* If RM_CreateKeyMetaClass() is called outside of RedisModule_OnLoad() function
|
||||
* and outside of server startup, there is already a metadata class registered
|
||||
* with the same name, or if the metadata class name or metaver is invalid,
|
||||
* a negative value is returned.
|
||||
* Otherwise the new metadata class is registered into Redis, and a reference of
|
||||
* type RedisModuleKeyMetaClassId is returned: the caller of the function should store
|
||||
* this reference into a global variable to make future use of it in the
|
||||
|
|
@ -4503,8 +4534,11 @@ RedisModuleKeyMetaClassId RM_CreateKeyMetaClass(RedisModuleCtx *ctx,
|
|||
{
|
||||
RedisModuleKeyMetaClassId id;
|
||||
|
||||
/* Allow registration only OnLoad (and when debug commands disabled) */
|
||||
if ((!ctx->module->onload) && (server.enable_debug_cmd == PROTECTED_ACTION_ALLOWED_NO))
|
||||
/* Allow registration during OnLoad, server startup, or when debug flag is set */
|
||||
int ctx_flags = RM_GetContextFlags(ctx);
|
||||
if (!ctx->module->onload &&
|
||||
!(ctx_flags & REDISMODULE_CTX_FLAGS_SERVER_STARTUP) &&
|
||||
!server.allow_keymeta_registration)
|
||||
return -1;
|
||||
|
||||
if (!confPtr)
|
||||
|
|
@ -9198,10 +9232,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
|
||||
|
|
@ -9244,10 +9279,13 @@ int RM_SubscribeToKeyspaceEvents(RedisModuleCtx *ctx, int types, RedisModuleNoti
|
|||
RedisModuleKeyspaceSubscriber *sub = zmalloc(sizeof(*sub));
|
||||
sub->module = ctx->module;
|
||||
sub->event_mask = types;
|
||||
sub->flags = REDISMODULE_NOTIFY_FLAG_NONE;
|
||||
sub->notify_callback = callback;
|
||||
sub->notify_callback_with_subkeys = NULL;
|
||||
sub->active = 0;
|
||||
|
||||
listAddNodeTail(moduleKeyspaceSubscribers, sub);
|
||||
moduleUpdateKeyspaceSubscribersTypes();
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
|
|
@ -9280,19 +9318,101 @@ int RM_UnsubscribeFromKeyspaceEvents(RedisModuleCtx *ctx, int types, RedisModule
|
|||
removed++;
|
||||
}
|
||||
}
|
||||
if (removed > 0) moduleUpdateKeyspaceSubscribersTypes();
|
||||
return removed > 0 ? REDISMODULE_OK : REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
/* Check any subscriber for event */
|
||||
int moduleHasSubscribersForKeyspaceEvent(int type) {
|
||||
/* Subscribe to keyspace notifications with subkey information.
|
||||
*
|
||||
* This is the extended version of RM_SubscribeToKeyspaceEvents. When subkeys
|
||||
* are available, the `subkeys` array and `count` are passed to the callback.
|
||||
* `subkeys` contains only the names of affected subkeys (values are not included),
|
||||
* and `count` is the number of elements. The array may contain duplicates when
|
||||
* the same subkey appears more than once in a command (e.g. HSET key f1 v1 f1 v2
|
||||
* produces subkeys=["f1","f1"], count=2). When no subkeys are present, `subkeys`
|
||||
* will be NULL and `count` will be 0. Whether events without subkeys are delivered
|
||||
* depends on the `flags` parameter (see below).
|
||||
*
|
||||
* `types` is a bit mask of event types the module is interested in
|
||||
* (using the same REDISMODULE_NOTIFY_* flags as RM_SubscribeToKeyspaceEvents).
|
||||
*
|
||||
* `flags` controls delivery filtering:
|
||||
* - REDISMODULE_NOTIFY_FLAG_NONE: The callback is invoked for all matching
|
||||
* events regardless of whether subkeys are present, so a separate
|
||||
* RM_SubscribeToKeyspaceEvents registration can be omitted.
|
||||
* - REDISMODULE_NOTIFY_FLAG_SUBKEYS_REQUIRED: The callback is only invoked
|
||||
* when subkeys are not empty. Events without subkey information (e.g. SET,
|
||||
* EXPIRE, DEL) are skipped.
|
||||
*
|
||||
* The callback signature is:
|
||||
* void callback(RedisModuleCtx *ctx, int type, const char *event,
|
||||
* RedisModuleString *key, RedisModuleString **subkeys, int count);
|
||||
*
|
||||
* The subkeys array and its contents are only valid during the callback.
|
||||
* The underlying objects may be stack-allocated or temporary, so
|
||||
* RM_RetainString must NOT be used on them. To keep a subkey beyond
|
||||
* the callback (e.g. in a RM_AddPostNotificationJob callback), use
|
||||
* RM_HoldString (which handles static objects by copying) or
|
||||
* RM_CreateStringFromString to make a deep copy before returning.
|
||||
*/
|
||||
int RM_SubscribeToKeyspaceEventsWithSubkeys(RedisModuleCtx *ctx, int types, int flags, RedisModuleNotificationWithSubkeysFunc callback) {
|
||||
RedisModuleKeyspaceSubscriber *sub = zmalloc(sizeof(*sub));
|
||||
sub->module = ctx->module;
|
||||
sub->event_mask = types;
|
||||
sub->flags = flags;
|
||||
sub->notify_callback = NULL;
|
||||
sub->notify_callback_with_subkeys = callback;
|
||||
sub->active = 0;
|
||||
|
||||
listAddNodeTail(moduleKeyspaceSubscribers, sub);
|
||||
moduleUpdateKeyspaceSubscribersTypes();
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* Unregister a module's callback from keyspace notifications with subkeys
|
||||
* for specific event types.
|
||||
*
|
||||
* This function removes a previously registered subscription identified by
|
||||
* the event mask, delivery flags, and the callback function.
|
||||
*
|
||||
* Parameters:
|
||||
* - ctx: The RedisModuleCtx associated with the calling module.
|
||||
* - types: The event mask representing the notification types to unsubscribe from.
|
||||
* - flags: The delivery flags that were used during registration.
|
||||
* - callback: The callback function pointer that was originally registered.
|
||||
*
|
||||
* Returns:
|
||||
* - REDISMODULE_OK on successful removal of the subscription.
|
||||
* - REDISMODULE_ERR if no matching subscription was found. */
|
||||
int RM_UnsubscribeFromKeyspaceEventsWithSubkeys(RedisModuleCtx *ctx, int types, int flags, RedisModuleNotificationWithSubkeysFunc callback) {
|
||||
if (!ctx || !callback) return REDISMODULE_ERR;
|
||||
int removed = 0;
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(moduleKeyspaceSubscribers,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
while ((ln = listNext(&li))) {
|
||||
RedisModuleKeyspaceSubscriber *sub = ln->value;
|
||||
if (sub->event_mask & type) return 1;
|
||||
if (sub->event_mask == types && sub->flags == flags &&
|
||||
sub->notify_callback_with_subkeys == callback &&
|
||||
sub->module == ctx->module)
|
||||
{
|
||||
zfree(sub);
|
||||
listDelNode(moduleKeyspaceSubscribers, ln);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
if (removed > 0) moduleUpdateKeyspaceSubscribersTypes();
|
||||
return removed > 0 ? REDISMODULE_OK : REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
/* Check any subscriber for event. */
|
||||
int moduleHasSubscribersForKeyspaceEvent(int type) {
|
||||
return (moduleKeyspaceSubscribersTypes & type) != 0;
|
||||
}
|
||||
|
||||
/* Check any subscriber for event with subkeys. */
|
||||
int moduleHasSubscribersForKeyspaceEventWithSubkeys(int type) {
|
||||
return (moduleKeyspaceSubscribersWithSubkeysTypes & type) != 0;
|
||||
}
|
||||
|
||||
void firePostExecutionUnitJobs(void) {
|
||||
|
|
@ -9366,10 +9486,29 @@ int RM_NotifyKeyspaceEvent(RedisModuleCtx *ctx, int type, const char *event, Red
|
|||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* Like RM_NotifyKeyspaceEvent, but also triggers subkey-level notifications
|
||||
* when subkeys are provided. Both key-level (keyspace/keyevent) and
|
||||
* subkey-level (subkeyspace/subkeyevent/subkeyspaceitem/subkeyspaceevent)
|
||||
* channels are published to, depending on the server configuration.
|
||||
*
|
||||
* This is the extended version of RM_NotifyKeyspaceEvent and can actually
|
||||
* replace it. When called with subkeys=NULL and count=0, it behaves
|
||||
* identically to RM_NotifyKeyspaceEvent. */
|
||||
int RM_NotifyKeyspaceEventWithSubkeys(RedisModuleCtx *ctx, int type, const char *event,
|
||||
RedisModuleString *key, RedisModuleString **subkeys, int count) {
|
||||
if (!ctx || !ctx->client)
|
||||
return REDISMODULE_ERR;
|
||||
notifyKeyspaceEventWithSubkeys(type, (char *)event, key, ctx->client->db->id, subkeys, count);
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* Dispatcher for keyspace notifications to module subscriber functions.
|
||||
* This gets called only if at least one module requested to be notified on
|
||||
* keyspace notifications */
|
||||
void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid) {
|
||||
* This gets called only if at least one module requested to be notified on
|
||||
* keyspace notifications. For each subscriber, if notify_callback is set it
|
||||
* is called; otherwise if notify_callback_with_subkeys is set it is called
|
||||
* for all events (subkeys may be NULL/0 when not applicable). */
|
||||
void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid,
|
||||
robj **subkeys, int count) {
|
||||
/* Don't do anything if there aren't any subscribers */
|
||||
if (listLength(moduleKeyspaceSubscribers) == 0) return;
|
||||
|
||||
|
|
@ -9397,7 +9536,9 @@ void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid)
|
|||
listRewind(moduleKeyspaceSubscribers,&li);
|
||||
|
||||
/* Remove irrelevant flags from the type mask */
|
||||
type &= ~(NOTIFY_KEYEVENT | NOTIFY_KEYSPACE);
|
||||
type &= ~(NOTIFY_KEYEVENT | NOTIFY_KEYSPACE |
|
||||
NOTIFY_SUBKEYSPACE | NOTIFY_SUBKEYEVENT |
|
||||
NOTIFY_SUBKEYSPACEITEM | NOTIFY_SUBKEYSPACEEVENT);
|
||||
|
||||
while((ln = listNext(&li))) {
|
||||
RedisModuleKeyspaceSubscriber *sub = ln->value;
|
||||
|
|
@ -9405,6 +9546,15 @@ void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid)
|
|||
* and avoid subscribers triggering themselves */
|
||||
if ((sub->event_mask & type) &&
|
||||
(sub->active == 0 || (sub->module->options & REDISMODULE_OPTIONS_ALLOW_NESTED_KEYSPACE_NOTIFICATIONS))) {
|
||||
|
||||
/* If SUBKEYS_REQUIRED is set, skip events without subkeys. */
|
||||
if (sub->notify_callback_with_subkeys &&
|
||||
(sub->flags & REDISMODULE_NOTIFY_FLAG_SUBKEYS_REQUIRED) &&
|
||||
(subkeys == NULL || count == 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RedisModuleCtx ctx;
|
||||
moduleCreateContext(&ctx, sub->module, REDISMODULE_CTX_TEMP_CLIENT);
|
||||
selectDb(ctx.client, dbid);
|
||||
|
|
@ -9416,7 +9566,11 @@ void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid)
|
|||
sub->active = 1;
|
||||
server.allow_access_expired++;
|
||||
server.allow_access_trimmed++;
|
||||
sub->notify_callback(&ctx, type, event, key);
|
||||
if (sub->notify_callback) {
|
||||
sub->notify_callback(&ctx, type, event, key);
|
||||
} else if (sub->notify_callback_with_subkeys) {
|
||||
sub->notify_callback_with_subkeys(&ctx, type, event, key, subkeys, count);
|
||||
}
|
||||
server.allow_access_expired--;
|
||||
server.allow_access_trimmed--;
|
||||
sub->active = prev_active;
|
||||
|
|
@ -9439,6 +9593,7 @@ void moduleUnsubscribeNotifications(RedisModule *module) {
|
|||
zfree(sub);
|
||||
}
|
||||
}
|
||||
moduleUpdateKeyspaceSubscribersTypes();
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
|
|
@ -9506,7 +9661,7 @@ void RM_RegisterClusterMessageReceiver(RedisModuleCtx *ctx, uint8_t type, RedisM
|
|||
if (prev)
|
||||
prev->next = r->next;
|
||||
else
|
||||
clusterReceivers[type]->next = r->next;
|
||||
clusterReceivers[type] = r->next; /* Update the head */
|
||||
zfree(r);
|
||||
}
|
||||
return;
|
||||
|
|
@ -15408,9 +15563,12 @@ void moduleRegisterCoreAPI(void) {
|
|||
REGISTER_API(DigestAddLongLong);
|
||||
REGISTER_API(DigestEndSequence);
|
||||
REGISTER_API(NotifyKeyspaceEvent);
|
||||
REGISTER_API(NotifyKeyspaceEventWithSubkeys);
|
||||
REGISTER_API(GetNotifyKeyspaceEvents);
|
||||
REGISTER_API(SubscribeToKeyspaceEvents);
|
||||
REGISTER_API(UnsubscribeFromKeyspaceEvents);
|
||||
REGISTER_API(SubscribeToKeyspaceEventsWithSubkeys);
|
||||
REGISTER_API(UnsubscribeFromKeyspaceEventsWithSubkeys);
|
||||
REGISTER_API(AddPostNotificationJob);
|
||||
REGISTER_API(RegisterClusterMessageReceiver);
|
||||
REGISTER_API(SendClusterMessage);
|
||||
|
|
|
|||
173
src/notify.c
173
src/notify.c
|
|
@ -40,6 +40,11 @@ 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;
|
||||
case 'S': flags |= NOTIFY_SUBKEYSPACE; break;
|
||||
case 'T': flags |= NOTIFY_SUBKEYEVENT; break;
|
||||
case 'I': flags |= NOTIFY_SUBKEYSPACEITEM; break;
|
||||
case 'V': flags |= NOTIFY_SUBKEYSPACEEVENT; break;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -70,45 +75,93 @@ 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);
|
||||
if (flags & NOTIFY_KEY_MISS) res = sdscatlen(res,"m",1);
|
||||
if (flags & NOTIFY_SUBKEYSPACE) res = sdscatlen(res,"S",1);
|
||||
if (flags & NOTIFY_SUBKEYEVENT) res = sdscatlen(res,"T",1);
|
||||
if (flags & NOTIFY_SUBKEYSPACEITEM) res = sdscatlen(res,"I",1);
|
||||
if (flags & NOTIFY_SUBKEYSPACEEVENT) res = sdscatlen(res,"V",1);
|
||||
return res;
|
||||
}
|
||||
|
||||
/* The API provided to the rest of the Redis core is a simple function:
|
||||
/* Append subkeys in length-prefixed format to 'dst'.
|
||||
* If 'dst' is NULL, a new sds is created.
|
||||
* Format: <len>:<subkey>[,<len>:<subkey>...]
|
||||
* Example: 3:abc,2:xx,5:hello */
|
||||
static sds catSubkeysPayload(sds dst, robj **subkeys, int count) {
|
||||
if (dst == NULL) dst = sdsempty();
|
||||
char lenbuf[32];
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
serverAssert(sdsEncodedObject(subkeys[i]));
|
||||
if (i > 0) dst = sdscatlen(dst, ",", 1);
|
||||
size_t subkeylen = sdslen(subkeys[i]->ptr);
|
||||
int lenlen = ll2string(lenbuf, sizeof(lenbuf), subkeylen);
|
||||
dst = sdscatlen(dst, lenbuf, lenlen);
|
||||
dst = sdscatlen(dst, ":", 1);
|
||||
dst = sdscatsds(dst, subkeys[i]->ptr);
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
/* Internal implementation for keyspace event notifications.
|
||||
*
|
||||
* The API provided to the rest of the Redis core is:
|
||||
*
|
||||
* notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);
|
||||
* notifyKeyspaceEventWithSubkeys(int type, char *event, robj *key, int dbid,
|
||||
* robj **subkeys, int count);
|
||||
*
|
||||
* 'type' is the notification class we define in `server.h`.
|
||||
* 'event' is a C string representing the event name.
|
||||
* 'key' is a Redis object representing the key name.
|
||||
* 'dbid' is the database ID where the key lives.
|
||||
* 'subkeys' is an array of Redis objects representing the subkey names (can be NULL).
|
||||
* 'count' is the number of subkeys in the array.
|
||||
*
|
||||
* For subkey notifications (4 channel types):
|
||||
* - __subkeyspace@<db>__:<key> payload: <event>|<subkeys>
|
||||
* - __subkeyevent@<db>__:<event> payload: <key_len>:<key>|<subkeys>
|
||||
* - __subkeyspaceitem@<db>__:<key>\n<subkey> payload: <event>
|
||||
* - __subkeyspaceevent@<db>__:<event>|<key> payload: <subkeys>
|
||||
*
|
||||
* Where <subkeys> is in length-prefixed format: <len>:<subkey>[,<len>:<subkey>...]
|
||||
* Example: 3:foo,5:hello
|
||||
*
|
||||
* NOTE: This function may invoke module notification callbacks, which may
|
||||
* cause the key's kvobj to be reallocated. */
|
||||
void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid) {
|
||||
static void notifyKeyspaceEventImpl(int type, const char *event, robj *key, int dbid,
|
||||
robj **subkeys, int count)
|
||||
{
|
||||
sds chan;
|
||||
robj *chanobj, *eventobj;
|
||||
int len = -1;
|
||||
char buf[24];
|
||||
serverAssert(sdsEncodedObject(key));
|
||||
|
||||
/* If any modules are interested in events, notify the module system now.
|
||||
* This bypasses the notifications configuration, but the module engine
|
||||
* will only call event subscribers if the event type matches the types
|
||||
* they are interested in. */
|
||||
moduleNotifyKeyspaceEvent(type, event, key, dbid);
|
||||
* they are interested in. Subkeys are passed through so that subscribers
|
||||
* with a subkey callback receive them. */
|
||||
moduleNotifyKeyspaceEvent(type, event, key, dbid, subkeys, count);
|
||||
|
||||
/* If notifications for this class of events are off, return ASAP. */
|
||||
if (!(server.notify_keyspace_events & type)) return;
|
||||
|
||||
/* If there are no Pub/Sub subscribers (neither pattern nor channel),
|
||||
* skip the remaining notification work since nobody would receive it. */
|
||||
if (dictSize(server.pubsub_patterns) == 0 && kvstoreSize(server.pubsub_channels) == 0)
|
||||
return;
|
||||
|
||||
eventobj = createStringObject(event,strlen(event));
|
||||
int len = ll2string(buf,sizeof(buf),dbid);
|
||||
|
||||
/* __keyspace@<db>__:<key> <event> notifications. */
|
||||
if (server.notify_keyspace_events & NOTIFY_KEYSPACE) {
|
||||
chan = sdsnewlen("__keyspace@",11);
|
||||
len = ll2string(buf,sizeof(buf),dbid);
|
||||
chan = sdscatlen(chan, buf, len);
|
||||
chan = sdscatlen(chan, "__:", 3);
|
||||
chan = sdscatsds(chan, key->ptr);
|
||||
|
|
@ -120,7 +173,6 @@ void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid) {
|
|||
/* __keyevent@<db>__:<event> <key> notifications. */
|
||||
if (server.notify_keyspace_events & NOTIFY_KEYEVENT) {
|
||||
chan = sdsnewlen("__keyevent@",11);
|
||||
if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
|
||||
chan = sdscatlen(chan, buf, len);
|
||||
chan = sdscatlen(chan, "__:", 3);
|
||||
chan = sdscatsds(chan, eventobj->ptr);
|
||||
|
|
@ -128,5 +180,112 @@ void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid) {
|
|||
pubsubPublishMessage(chanobj, key, 0);
|
||||
decrRefCount(chanobj);
|
||||
}
|
||||
|
||||
/* Subkey-level notifications (only when subkeys are provided). */
|
||||
if (subkeys != NULL && count > 0) {
|
||||
/* __subkeyspace@<db>__:<key> <event>|<len>:<subkey>[,...] notifications.
|
||||
* Skip if the event contains '|' to avoid parsing ambiguity since '|'
|
||||
* is used as a separator between event and subkeys in the payload. */
|
||||
if (server.notify_keyspace_events & NOTIFY_SUBKEYSPACE && !strchr(event, '|')) {
|
||||
chan = sdsnewlen("__subkeyspace@", 14);
|
||||
chan = sdscatlen(chan, buf, len);
|
||||
chan = sdscatlen(chan, "__:", 3);
|
||||
chan = sdscatsds(chan, key->ptr);
|
||||
chanobj = createObject(OBJ_STRING, chan);
|
||||
|
||||
/* Build payload: <event>|<subkeys_payload> */
|
||||
sds payload = sdsdup(eventobj->ptr);
|
||||
payload = sdscatlen(payload, "|", 1);
|
||||
payload = catSubkeysPayload(payload, subkeys, count);
|
||||
robj *payloadobj = createObject(OBJ_STRING, payload);
|
||||
pubsubPublishMessage(chanobj, payloadobj, 0);
|
||||
decrRefCount(chanobj);
|
||||
decrRefCount(payloadobj);
|
||||
}
|
||||
|
||||
/* __subkeyevent@<db>__:<event> <key_len>:<key>|<len>:<subkey>[,...] notifications. */
|
||||
if (server.notify_keyspace_events & NOTIFY_SUBKEYEVENT) {
|
||||
chan = sdsnewlen("__subkeyevent@", 14);
|
||||
chan = sdscatlen(chan, buf, len);
|
||||
chan = sdscatlen(chan, "__:", 3);
|
||||
chan = sdscatsds(chan, eventobj->ptr);
|
||||
chanobj = createObject(OBJ_STRING, chan);
|
||||
|
||||
/* Build payload: <key_len>:<key>|<subkeys_payload> */
|
||||
size_t keylen = sdslen(key->ptr);
|
||||
char keylenbuf[32];
|
||||
int keylenlen = ll2string(keylenbuf, sizeof(keylenbuf), keylen);
|
||||
sds payload = sdsnewlen(keylenbuf, keylenlen);
|
||||
payload = sdscatlen(payload, ":", 1);
|
||||
payload = sdscatsds(payload, key->ptr);
|
||||
payload = sdscatlen(payload, "|", 1);
|
||||
payload = catSubkeysPayload(payload, subkeys, count);
|
||||
robj *payloadobj = createObject(OBJ_STRING, payload);
|
||||
pubsubPublishMessage(chanobj, payloadobj, 0);
|
||||
decrRefCount(chanobj);
|
||||
decrRefCount(payloadobj);
|
||||
}
|
||||
|
||||
/* __subkeyspaceitem@<db>__:<key>\n<subkey> <event> notifications (per subkey).
|
||||
* Skip if the key contains '\n' to avoid parsing ambiguity in the channel name. */
|
||||
if (server.notify_keyspace_events & NOTIFY_SUBKEYSPACEITEM &&
|
||||
memchr(key->ptr, '\n', sdslen(key->ptr)) == NULL)
|
||||
{
|
||||
for (int i = 0; i < count; i++) {
|
||||
serverAssert(sdsEncodedObject(subkeys[i]));
|
||||
chan = sdsnewlen("__subkeyspaceitem@", 18);
|
||||
chan = sdscatlen(chan, buf, len);
|
||||
chan = sdscatlen(chan, "__:", 3);
|
||||
chan = sdscatsds(chan, key->ptr);
|
||||
chan = sdscatlen(chan, "\n", 1);
|
||||
chan = sdscatsds(chan, subkeys[i]->ptr);
|
||||
chanobj = createObject(OBJ_STRING, chan);
|
||||
pubsubPublishMessage(chanobj, eventobj, 0);
|
||||
decrRefCount(chanobj);
|
||||
}
|
||||
}
|
||||
|
||||
/* __subkeyspaceevent@<db>__:<event>|<key> <subkeys> notifications.
|
||||
* Skip if the event contains '|' to avoid parsing ambiguity since '|'
|
||||
* is used as a separator between event and key in the channel name. */
|
||||
if (server.notify_keyspace_events & NOTIFY_SUBKEYSPACEEVENT && !strchr(event, '|')) {
|
||||
chan = sdsnewlen("__subkeyspaceevent@", 19);
|
||||
chan = sdscatlen(chan, buf, len);
|
||||
chan = sdscatlen(chan, "__:", 3);
|
||||
chan = sdscatsds(chan, eventobj->ptr);
|
||||
chan = sdscatlen(chan, "|", 1);
|
||||
chan = sdscatsds(chan, key->ptr);
|
||||
chanobj = createObject(OBJ_STRING, chan);
|
||||
robj *payloadobj = createObject(OBJ_STRING, catSubkeysPayload(NULL, subkeys, count));
|
||||
pubsubPublishMessage(chanobj, payloadobj, 0);
|
||||
decrRefCount(chanobj);
|
||||
decrRefCount(payloadobj);
|
||||
}
|
||||
}
|
||||
|
||||
decrRefCount(eventobj);
|
||||
}
|
||||
|
||||
/* Public API for key-level notifications (backward compatible). */
|
||||
void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid) {
|
||||
notifyKeyspaceEventImpl(type, event, key, dbid, NULL, 0);
|
||||
}
|
||||
|
||||
/* Public API for notifications with subkeys (key-level + subkey-level). */
|
||||
void notifyKeyspaceEventWithSubkeys(int type, const char *event, robj *key, int dbid,
|
||||
robj **subkeys, int count) {
|
||||
notifyKeyspaceEventImpl(type, event, key, dbid, subkeys, count);
|
||||
}
|
||||
|
||||
/* Check if subkey information should be collected for the given event type.
|
||||
* Returns true if any module subscribed to this event with subkeys, or if
|
||||
* there are Pub/Sub subscribers and any subkey-level notification channel is
|
||||
* enabled for this event type. */
|
||||
int isSubkeyNotifyEnabled(int type) {
|
||||
if (moduleHasSubscribersForKeyspaceEventWithSubkeys(type)) return 1;
|
||||
if (dictSize(server.pubsub_patterns) == 0 && kvstoreSize(server.pubsub_channels) == 0)
|
||||
return 0;
|
||||
return (server.notify_keyspace_events & type) &&
|
||||
(server.notify_keyspace_events & (NOTIFY_SUBKEYSPACE | NOTIFY_SUBKEYEVENT |
|
||||
NOTIFY_SUBKEYSPACEITEM | NOTIFY_SUBKEYSPACEEVENT));
|
||||
}
|
||||
|
|
|
|||
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);
|
||||
|
|
|
|||
166
src/rdb.c
166
src/rdb.c
|
|
@ -712,7 +712,9 @@ int rdbSaveObjectType(rio *rdb, robj *o) {
|
|||
} else
|
||||
serverPanic("Unknown hash encoding");
|
||||
case OBJ_STREAM:
|
||||
return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_4);
|
||||
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:
|
||||
|
|
@ -1351,6 +1353,29 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid) {
|
|||
return -1;
|
||||
}
|
||||
nwritten += n;
|
||||
|
||||
/* Save NACK zone: count followed by the IDs of NACKed entries. */
|
||||
uint64_t nacked_count = pelListNackedCount(cg);
|
||||
if ((n = rdbSaveLen(rdb, nacked_count)) == -1) {
|
||||
raxStop(&ri);
|
||||
return -1;
|
||||
}
|
||||
nwritten += n;
|
||||
|
||||
if (cg->pel_nack_tail) {
|
||||
streamNACK *nack = cg->pel_time_head;
|
||||
while (nack) {
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf, &nack->id);
|
||||
if ((n = rdbWriteRaw(rdb, buf, sizeof(buf))) == -1) {
|
||||
raxStop(&ri);
|
||||
return -1;
|
||||
}
|
||||
nwritten += n;
|
||||
if (nack == cg->pel_nack_tail) break;
|
||||
nack = nack->pel_next;
|
||||
}
|
||||
}
|
||||
}
|
||||
raxStop(&ri);
|
||||
}
|
||||
|
|
@ -1376,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;
|
||||
|
|
@ -1657,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);
|
||||
|
|
@ -1684,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
|
||||
|
|
@ -1735,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1963,11 +2002,18 @@ void rdbRemoveTempFile(pid_t childpid, int from_signal) {
|
|||
|
||||
/* This function is called by rdbLoadObject() when the code is in RDB-check
|
||||
* mode and we find a module value of type 2 that can be parsed without
|
||||
* the need of the actual module. The value is parsed for errors, finally
|
||||
* a dummy redis object is returned just to conform to the API. */
|
||||
robj *rdbLoadCheckModuleValue(rio *rdb, char *modulename) {
|
||||
* the need of the actual module. The value is parsed for errors.
|
||||
* If null_on_error is true, NULL is returned when data corruption is detected;
|
||||
* otherwise a dummy redis object is always returned regardless of success or
|
||||
* failure. */
|
||||
robj *rdbLoadCheckModuleValue(rio *rdb, char *modulename, int null_on_error) {
|
||||
uint64_t opcode;
|
||||
while((opcode = rdbLoadLen(rdb,NULL)) != RDB_MODULE_OPCODE_EOF) {
|
||||
if (opcode == RDB_LENERR) {
|
||||
rdbReportCorruptRDB("Error reading module opcode length from module %s value", modulename);
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (opcode == RDB_MODULE_OPCODE_SINT ||
|
||||
opcode == RDB_MODULE_OPCODE_UINT)
|
||||
{
|
||||
|
|
@ -1975,12 +2021,14 @@ robj *rdbLoadCheckModuleValue(rio *rdb, char *modulename) {
|
|||
if (rdbLoadLenByRef(rdb,NULL,&len) == -1) {
|
||||
rdbReportCorruptRDB(
|
||||
"Error reading integer from module %s value", modulename);
|
||||
goto error;
|
||||
}
|
||||
} else if (opcode == RDB_MODULE_OPCODE_STRING) {
|
||||
robj *o = rdbGenericLoadStringObject(rdb,RDB_LOAD_NONE,NULL);
|
||||
if (o == NULL) {
|
||||
rdbReportCorruptRDB(
|
||||
"Error reading string from module %s value", modulename);
|
||||
goto error;
|
||||
}
|
||||
decrRefCount(o);
|
||||
} else if (opcode == RDB_MODULE_OPCODE_FLOAT) {
|
||||
|
|
@ -1988,16 +2036,24 @@ robj *rdbLoadCheckModuleValue(rio *rdb, char *modulename) {
|
|||
if (rdbLoadBinaryFloatValue(rdb,&val) == -1) {
|
||||
rdbReportCorruptRDB(
|
||||
"Error reading float from module %s value", modulename);
|
||||
goto error;
|
||||
}
|
||||
} else if (opcode == RDB_MODULE_OPCODE_DOUBLE) {
|
||||
double val;
|
||||
if (rdbLoadBinaryDoubleValue(rdb,&val) == -1) {
|
||||
rdbReportCorruptRDB(
|
||||
"Error reading double from module %s value", modulename);
|
||||
goto error;
|
||||
}
|
||||
} else {
|
||||
rdbReportCorruptRDB(
|
||||
"Unknown module opcode %llu reading module %s value", (unsigned long long)opcode, modulename);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
return createStringObject("module-dummy-value",18);
|
||||
error:
|
||||
return null_on_error ? NULL : createStringObject("module-dummy-value",18);
|
||||
}
|
||||
|
||||
/* Load object type and optional key metadata (into `keymeta`) from RDB stream.
|
||||
|
|
@ -2878,11 +2934,13 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
|
||||
/* search for duplicate records */
|
||||
sds field = sdstrynewlen(fstr, flen);
|
||||
if (!field || dictAdd(dupSearchDict, field, NULL) != DICT_OK ||
|
||||
!lpSafeToAdd(lp, (size_t)flen + vlen)) {
|
||||
int field_added = (field != NULL && dictAdd(dupSearchDict, field, NULL) == DICT_OK);
|
||||
if (!field_added || !lpSafeToAdd(lp, (size_t)flen + vlen)) {
|
||||
rdbReportCorruptRDB("Hash zipmap with dup elements, or big length (%u)", flen);
|
||||
/* If field was not added to dict, we still own it.
|
||||
* If it was added, dict owns it and dictRelease will free it. */
|
||||
if (!field_added) sdsfree(field);
|
||||
dictRelease(dupSearchDict);
|
||||
sdsfree(field);
|
||||
lpFree(lp);
|
||||
zfree(encoded);
|
||||
o->ptr = NULL;
|
||||
|
|
@ -3092,7 +3150,8 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
} else if (rdbtype == RDB_TYPE_STREAM_LISTPACKS ||
|
||||
rdbtype == RDB_TYPE_STREAM_LISTPACKS_2 ||
|
||||
rdbtype == RDB_TYPE_STREAM_LISTPACKS_3 ||
|
||||
rdbtype == RDB_TYPE_STREAM_LISTPACKS_4)
|
||||
rdbtype == RDB_TYPE_STREAM_LISTPACKS_4 ||
|
||||
rdbtype == RDB_TYPE_STREAM_LISTPACKS_5)
|
||||
{
|
||||
o = createStreamObject();
|
||||
stream *s = o->ptr;
|
||||
|
|
@ -3372,6 +3431,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
}
|
||||
streamNACK *nack = result;
|
||||
|
||||
/* If the NACK already has a consumer assigned, the
|
||||
* payload is corrupt — each global PEL entry must be
|
||||
* claimed by exactly one consumer. */
|
||||
if (nack->consumer != NULL) {
|
||||
rdbReportCorruptRDB("Stream consumer PEL entry already has a consumer assigned");
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
/* Set the NACK consumer, that was left to NULL when
|
||||
* loading the global PEL. Then set the same shared
|
||||
* NACK structure also in the consumer-specific PEL. */
|
||||
|
|
@ -3387,21 +3454,67 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
}
|
||||
}
|
||||
|
||||
/* Verify that each PEL eventually got a consumer assigned to it. */
|
||||
if (deep_integrity_validation) {
|
||||
raxIterator ri_cg_pel;
|
||||
raxStart(&ri_cg_pel,cgroup->pel);
|
||||
raxSeek(&ri_cg_pel,"^",NULL,0);
|
||||
while(raxNext(&ri_cg_pel)) {
|
||||
streamNACK *nack = ri_cg_pel.data;
|
||||
if (!nack->consumer) {
|
||||
raxStop(&ri_cg_pel);
|
||||
rdbReportCorruptRDB("Stream CG PEL entry without consumer");
|
||||
/* For RDB_TYPE_STREAM_LISTPACKS_5 and above, load the NACK
|
||||
* zone stream IDs and reconstruct the NACK zone. Entries with
|
||||
* delivery_time == 0 may exist for both nacked and owned PEL
|
||||
* entries, so we cannot rely on a simple walk — we use the
|
||||
* stored IDs to unlink each nacked entry from its sorted
|
||||
* position and re-insert it into the NACK zone. */
|
||||
if (rdbtype >= RDB_TYPE_STREAM_LISTPACKS_5) {
|
||||
uint64_t nacked_count = rdbLoadLen(rdb, NULL);
|
||||
if (nacked_count == RDB_LENERR) {
|
||||
rdbReportReadError("Stream NACK zone count loading failed.");
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Load each NACKed entry's stream ID, look it up in the
|
||||
* group PEL, unlink from its current time-list position,
|
||||
* and re-insert into the NACK zone. */
|
||||
for (uint64_t i = 0; i < nacked_count; i++) {
|
||||
unsigned char rawid[sizeof(streamID)];
|
||||
if (rioRead(rdb, rawid, sizeof(rawid)) == 0) {
|
||||
rdbReportReadError("Stream NACK zone entry ID loading failed.");
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *result;
|
||||
if (!raxFind(cgroup->pel, rawid, sizeof(rawid), &result)) {
|
||||
rdbReportCorruptRDB("Stream NACK zone entry not found "
|
||||
"in group global PEL");
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
streamNACK *nack = result;
|
||||
if (nack->consumer != NULL) {
|
||||
rdbReportCorruptRDB("Stream NACK zone entry has a "
|
||||
"consumer assigned");
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
pelListUnlink(cgroup, nack);
|
||||
pelListInsertNacked(cgroup, nack);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Verify entries outside the NACK zone all have a consumer
|
||||
* assigned. For old RDB types pel_nack_tail is NULL, so
|
||||
* this walks the entire PEL — equivalent to checking all. */
|
||||
if (deep_integrity_validation) {
|
||||
streamNACK *cur = cgroup->pel_nack_tail ?
|
||||
cgroup->pel_nack_tail->pel_next :
|
||||
cgroup->pel_time_head;
|
||||
while (cur) {
|
||||
if (!cur->consumer) {
|
||||
rdbReportCorruptRDB("Stream CG PEL entry without "
|
||||
"consumer outside NACK zone");
|
||||
decrRefCount(o);
|
||||
return NULL;
|
||||
}
|
||||
cur = cur->pel_next;
|
||||
}
|
||||
raxStop(&ri_cg_pel);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3473,7 +3586,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
if (rdbCheckMode) {
|
||||
char name[10];
|
||||
moduleTypeNameByID(name,moduleid);
|
||||
return rdbLoadCheckModuleValue(rdb,name);
|
||||
return rdbLoadCheckModuleValue(rdb, name, 0);
|
||||
}
|
||||
|
||||
if (mt == NULL) {
|
||||
|
|
@ -3520,6 +3633,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;
|
||||
|
|
@ -3925,7 +4045,7 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin
|
|||
continue;
|
||||
} else {
|
||||
/* RDB check mode. */
|
||||
robj *aux = rdbLoadCheckModuleValue(rdb,name);
|
||||
robj *aux = rdbLoadCheckModuleValue(rdb, name, 0);
|
||||
decrRefCount(aux);
|
||||
continue; /* Read next opcode. */
|
||||
}
|
||||
|
|
@ -4044,7 +4164,7 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin
|
|||
objectSetLRUOrLFU(val,lfu_freq,lru_idle,lru_clock,1000);
|
||||
|
||||
/* call key space notification on key loaded for modules only */
|
||||
moduleNotifyKeyspaceEvent(NOTIFY_LOADED, "loaded", &keyobj, db->id);
|
||||
moduleNotifyKeyspaceEvent(NOTIFY_LOADED, "loaded", &keyobj, db->id, NULL, 0);
|
||||
|
||||
/* Release key (sds), dictEntry stores a copy of it in embedded data */
|
||||
sdsfree(key);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
/* The current RDB version. When the format changes in a way that is no longer
|
||||
* backward compatible this number gets incremented. */
|
||||
#define RDB_VERSION 13
|
||||
#define RDB_VERSION 14
|
||||
|
||||
/* Defines related to the dump file format. To store 32 bits lengths for short
|
||||
* keys requires a lot of space, so we check the most significant 2 bits of
|
||||
|
|
@ -79,10 +79,12 @@
|
|||
#define RDB_TYPE_HASH_METADATA 24 /* Hash with HFEs. Attach min TTL at start */
|
||||
#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) <= 26))
|
||||
#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). */
|
||||
|
|
@ -150,7 +152,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error);
|
|||
void backgroundSaveDoneHandler(int exitcode, int bysignal);
|
||||
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime,int dbid);
|
||||
ssize_t rdbSaveSingleModuleAux(rio *rdb, int when, moduleType *mt);
|
||||
robj *rdbLoadCheckModuleValue(rio *rdb, char *modulename);
|
||||
robj *rdbLoadCheckModuleValue(rio *rdb, char *modulename, int null_on_error);
|
||||
int rdbResolveKeyType(rio *rdb, int *type, int dbid, KeyMetaSpec *keymeta);
|
||||
robj *rdbLoadStringObject(rio *rdb);
|
||||
ssize_t rdbSaveStringObject(rio *rdb, robj *obj);
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ char *rdb_type_string[] = {
|
|||
"hash-hashtable-md",
|
||||
"hash-listpack-md",
|
||||
"stream-v4",
|
||||
"stream-v5",
|
||||
"gcra",
|
||||
};
|
||||
|
||||
/* Show a few stats collected into 'rdbstate' */
|
||||
|
|
@ -254,7 +256,7 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) {
|
|||
uint32_t classSpec;
|
||||
if (rioRead(&rdb, &classSpec, 4) == 0) goto eoferr;
|
||||
/* Skip module value using rdbLoadCheckModuleValue */
|
||||
robj *o = rdbLoadCheckModuleValue(&rdb, "metadata");
|
||||
robj *o = rdbLoadCheckModuleValue(&rdb, "metadata", 1);
|
||||
if (o == NULL) goto eoferr;
|
||||
decrRefCount(o);
|
||||
}
|
||||
|
|
@ -324,7 +326,7 @@ int redis_check_rdb(char *rdbfilename, FILE *fp) {
|
|||
moduleTypeNameByID(name,moduleid);
|
||||
rdbCheckInfo("MODULE AUX for: %s", name);
|
||||
|
||||
robj *o = rdbLoadCheckModuleValue(&rdb,name);
|
||||
robj *o = rdbLoadCheckModuleValue(&rdb, name, 0);
|
||||
decrRefCount(o);
|
||||
continue; /* Read type again. */
|
||||
} else if (type == RDB_OPCODE_FUNCTION_PRE_GA) {
|
||||
|
|
|
|||
|
|
@ -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,22 @@ 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 */
|
||||
|
||||
#define REDISMODULE_NOTIFY_SUBKEYSPACE (1<<19) /* S */
|
||||
#define REDISMODULE_NOTIFY_SUBKEYEVENT (1<<20) /* T */
|
||||
#define REDISMODULE_NOTIFY_SUBKEYSPACEITEM (1<<21) /* I */
|
||||
#define REDISMODULE_NOTIFY_SUBKEYSPACEEVENT (1<<22) /* V */
|
||||
|
||||
/* 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<<23)
|
||||
|
||||
/* Delivery flags for RM_SubscribeToKeyspaceEventsWithSubkeys.
|
||||
* These are passed in the 'flags' parameter, not in 'types'. */
|
||||
#define REDISMODULE_NOTIFY_FLAG_NONE 0 /* Invoke callback for all matching events */
|
||||
#define REDISMODULE_NOTIFY_FLAG_SUBKEYS_REQUIRED (1<<0) /* Only invoke callback when subkeys are present */
|
||||
|
||||
#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 */
|
||||
|
||||
|
|
@ -975,6 +987,7 @@ typedef struct RedisModuleConfigIterator RedisModuleConfigIterator;
|
|||
typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
|
||||
typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc);
|
||||
typedef int (*RedisModuleNotificationFunc)(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key);
|
||||
typedef void (*RedisModuleNotificationWithSubkeysFunc)(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key, RedisModuleString **subkeys, int count);
|
||||
typedef void (*RedisModulePostNotificationJobFunc) (RedisModuleCtx *ctx, void *pd);
|
||||
typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver);
|
||||
typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value);
|
||||
|
|
@ -1360,8 +1373,11 @@ REDISMODULE_API int (*RedisModule_ThreadSafeContextTryLock)(RedisModuleCtx *ctx)
|
|||
REDISMODULE_API void (*RedisModule_ThreadSafeContextUnlock)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_SubscribeToKeyspaceEvents)(RedisModuleCtx *ctx, int types, RedisModuleNotificationFunc cb) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_UnsubscribeFromKeyspaceEvents)(RedisModuleCtx *ctx, int types, RedisModuleNotificationFunc cb) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_SubscribeToKeyspaceEventsWithSubkeys)(RedisModuleCtx *ctx, int types, int flags, RedisModuleNotificationWithSubkeysFunc cb) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_UnsubscribeFromKeyspaceEventsWithSubkeys)(RedisModuleCtx *ctx, int types, int flags, RedisModuleNotificationWithSubkeysFunc cb) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_AddPostNotificationJob)(RedisModuleCtx *ctx, RedisModulePostNotificationJobFunc callback, void *pd, void (*free_pd)(void*)) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_NotifyKeyspaceEvent)(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_NotifyKeyspaceEventWithSubkeys)(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key, RedisModuleString **subkeys, int count) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_GetNotifyKeyspaceEvents)(void) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_BlockedClientDisconnected)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
|
||||
REDISMODULE_API void (*RedisModule_RegisterClusterMessageReceiver)(RedisModuleCtx *ctx, uint8_t type, RedisModuleClusterMessageReceiver callback) REDISMODULE_ATTR;
|
||||
|
|
@ -1762,8 +1778,11 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
|||
REDISMODULE_GET_API(SetDisconnectCallback);
|
||||
REDISMODULE_GET_API(SubscribeToKeyspaceEvents);
|
||||
REDISMODULE_GET_API(UnsubscribeFromKeyspaceEvents);
|
||||
REDISMODULE_GET_API(SubscribeToKeyspaceEventsWithSubkeys);
|
||||
REDISMODULE_GET_API(UnsubscribeFromKeyspaceEventsWithSubkeys);
|
||||
REDISMODULE_GET_API(AddPostNotificationJob);
|
||||
REDISMODULE_GET_API(NotifyKeyspaceEvent);
|
||||
REDISMODULE_GET_API(NotifyKeyspaceEventWithSubkeys);
|
||||
REDISMODULE_GET_API(GetNotifyKeyspaceEvents);
|
||||
REDISMODULE_GET_API(BlockedClientDisconnected);
|
||||
REDISMODULE_GET_API(RegisterClusterMessageReceiver);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
43
src/server.c
43
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 = {
|
||||
|
|
@ -2232,6 +2246,7 @@ void createSharedObjects(void) {
|
|||
shared.srem = createStringObject("SREM",4);
|
||||
shared.xgroup = createStringObject("XGROUP",6);
|
||||
shared.xclaim = createStringObject("XCLAIM",6);
|
||||
shared.xack = createStringObject("XACK",4);
|
||||
shared.script = createStringObject("SCRIPT",6);
|
||||
shared.replconf = createStringObject("REPLCONF",8);
|
||||
shared.pexpireat = createStringObject("PEXPIREAT",9);
|
||||
|
|
@ -2342,6 +2357,7 @@ void initServerConfig(void) {
|
|||
server.allow_access_expired = 0;
|
||||
server.allow_access_trimmed = 0;
|
||||
server.skip_checksum_validation = 0;
|
||||
server.allow_keymeta_registration = 0;
|
||||
server.loading = 0;
|
||||
server.async_loading = 0;
|
||||
server.loading_rdb_used_mem = 0;
|
||||
|
|
@ -2994,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;
|
||||
|
|
@ -7481,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.
|
||||
|
|
@ -7519,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
|
||||
}
|
||||
|
||||
|
|
@ -7782,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;
|
||||
|
|
@ -7805,10 +7844,12 @@ struct redisTest {
|
|||
{"fwtree", fwtreeTest},
|
||||
{"estore", estoreTest},
|
||||
{"ebuckets", ebucketsTest},
|
||||
{"vector", vectorTest},
|
||||
{"bitmap", bitopsTest},
|
||||
{"rax", raxTest},
|
||||
{"zset", zsetTest},
|
||||
{"topk", chkTopKTest},
|
||||
{"fastfloat", fastFloatTest},
|
||||
};
|
||||
redisTestProc *getTestProcByName(const char *name) {
|
||||
int numtests = sizeof(redisTests)/sizeof(struct redisTest);
|
||||
|
|
|
|||
52
src/server.h
52
src/server.h
|
|
@ -287,6 +287,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 *
|
||||
* -------------- */
|
||||
|
|
@ -795,6 +796,11 @@ 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_SUBKEYSPACE (1<<19) /* S, subkey-level keyspace notification */
|
||||
#define NOTIFY_SUBKEYEVENT (1<<20) /* T, subkey-level keyevent notification */
|
||||
#define NOTIFY_SUBKEYSPACEITEM (1<<21) /* I, subkey-level notification per item: channel=key\nsubkey */
|
||||
#define NOTIFY_SUBKEYSPACEEVENT (1<<22) /* V, subkey-level notification: channel=event|key */
|
||||
#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
|
||||
|
|
@ -859,7 +865,25 @@ 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)
|
||||
* - tests/support/util.tcl:generate_fuzzy_traffic_on_key - add command(s) for the new object type to the `commands` dict.
|
||||
*
|
||||
* If the new object type requires new command group make sure to update the following:
|
||||
* - src/commands/command-docs.json - update the group:oneOf map with the new group
|
||||
* - utils/generate-command-code.py - add the new group to GROUPS and COMMAND_GROUP_STR arrays
|
||||
* - src/acl.c - add the new group to ACLDefaultCommandCategories array
|
||||
* - src/server.h - add the new group to redisCommandGroup enum
|
||||
* - if needed add new KSN type related to the group - search for NOTIFY_* and REDISMODULE_NOTIFY_* defines. */
|
||||
|
||||
/* Extract encver / signature from a module type ID. */
|
||||
#define REDISMODULE_TYPE_ENCVER_BITS 10
|
||||
|
|
@ -1690,7 +1714,7 @@ struct sharedObjectsStruct {
|
|||
*busykeyerr, *oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk,
|
||||
*unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *unlink,
|
||||
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
|
||||
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,
|
||||
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim, *xack,
|
||||
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
|
||||
*hdel, *hpexpireat, *hpersist, *hsetex,
|
||||
*time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
|
||||
|
|
@ -2158,6 +2182,7 @@ struct redisServer {
|
|||
int active_defrag_enabled;
|
||||
int sanitize_dump_payload; /* Enables deep sanitization for ziplist and listpack in RDB and RESTORE. */
|
||||
int skip_checksum_validation; /* Disable checksum validation for RDB and RESTORE payload. */
|
||||
int allow_keymeta_registration; /* Allow keymeta class registration outside server startup (for testing). */
|
||||
int jemalloc_bg_thread; /* Enable jemalloc background thread */
|
||||
int active_defrag_configuration_changed; /* defrag configuration has been changed and need to reconsider
|
||||
* active_defrag_running in computeDefragCycles. */
|
||||
|
|
@ -2770,6 +2795,7 @@ typedef enum {
|
|||
COMMAND_GROUP_STREAM,
|
||||
COMMAND_GROUP_BITMAP,
|
||||
COMMAND_GROUP_MODULE,
|
||||
COMMAND_GROUP_RATE_LIMIT,
|
||||
} redisCommandGroup;
|
||||
|
||||
typedef void redisCommandProc(client *c);
|
||||
|
|
@ -3003,6 +3029,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;
|
||||
|
|
@ -3063,7 +3090,7 @@ size_t moduleCount(void);
|
|||
void moduleAcquireGIL(void);
|
||||
int moduleTryAcquireGIL(void);
|
||||
void moduleReleaseGIL(void);
|
||||
void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid);
|
||||
void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid, robj **subkeys, int count);
|
||||
void firePostExecutionUnitJobs(void);
|
||||
void moduleCallCommandFilters(client *c);
|
||||
void modulePostExecutionUnitOperations(void);
|
||||
|
|
@ -3091,6 +3118,7 @@ void moduleDefragEnd(void);
|
|||
void *moduleGetHandleByName(char *modulename);
|
||||
int moduleIsModuleCommand(void *module_handle, struct redisCommand *cmd);
|
||||
int moduleHasSubscribersForKeyspaceEvent(int type);
|
||||
int moduleHasSubscribersForKeyspaceEventWithSubkeys(int type);
|
||||
|
||||
/* pcmd */
|
||||
void initPendingCommand(pendingCommand *pcmd);
|
||||
|
|
@ -3358,7 +3386,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);
|
||||
|
|
@ -3596,6 +3623,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);
|
||||
|
|
@ -3680,6 +3710,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);
|
||||
|
||||
|
|
@ -3822,8 +3854,18 @@ dict *getClientPubSubShardChannels(client *c);
|
|||
|
||||
/* Keyspace events notification */
|
||||
void notifyKeyspaceEvent(int type, const char *event, robj *key, int dbid);
|
||||
void notifyKeyspaceEventWithSubkeys(int type, const char *event, robj *key, int dbid, robj **subkeys, int count);
|
||||
int keyspaceEventsStringToFlags(char *classes);
|
||||
sds keyspaceEventsFlagsToString(int flags);
|
||||
int isSubkeyNotifyEnabled(int type);
|
||||
|
||||
/* As part of KSN the module should not attempt to modify the key. Nevertheless,
|
||||
* RediSearch does it in some specific flows and modifies key metadata which in
|
||||
* turn might invalidates the local kvobj pointer. Those specific flows are
|
||||
* protected by the following macro which invalidates the local kvobj pointer
|
||||
* after the notification to prevent further access to it (Currently it is only
|
||||
* using it with hash type keys, without hash field expiration) */
|
||||
#define KSN_INVALIDATE_KVOBJ(o) do { (o) = NULL; } while (0)
|
||||
|
||||
/* Configuration */
|
||||
/* Configuration Flags */
|
||||
|
|
@ -4439,6 +4481,7 @@ void xgroupCommand(client *c);
|
|||
void xsetidCommand(client *c);
|
||||
void xidmprecordCommand(client *c);
|
||||
void xackCommand(client *c);
|
||||
void xnackCommand(client *c);
|
||||
void xackdelCommand(client *c);
|
||||
void xpendingCommand(client *c);
|
||||
void xclaimCommand(client *c);
|
||||
|
|
@ -4457,6 +4500,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) {
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@ typedef struct streamCG {
|
|||
streamNACK *pel_time_tail; /* Tail of time-ordered doubly-linked list of pending
|
||||
entries (newest delivery_time). O(1) append for
|
||||
updates that set delivery_time to current time. */
|
||||
streamNACK *pel_nack_tail; /* Tail of the NACK zone at the head of the
|
||||
PEL time-ordered list. NACKed entries occupy
|
||||
positions from pel_time_head to pel_nack_tail.
|
||||
NULL if no NACKed entries exist. */
|
||||
rax *consumers; /* A radix tree representing the consumers by name
|
||||
and their associated representation in the form
|
||||
of streamConsumer structures. */
|
||||
|
|
@ -175,6 +179,7 @@ streamConsumer *streamLookupConsumer(streamCG *cg, sds name);
|
|||
streamConsumer *streamCreateConsumer(stream *s, streamCG *cg, sds name, robj *key, int dbid, int flags);
|
||||
streamCG *streamCreateCG(stream *s, char *name, size_t namelen, streamID *id, long long entries_read);
|
||||
streamNACK *streamCreateNACK(stream *s, streamConsumer *consumer, streamID *id);
|
||||
void streamEncodeID(void *buf, streamID *id);
|
||||
void streamDecodeID(void *buf, streamID *id);
|
||||
int streamCompareID(streamID *a, streamID *b);
|
||||
void streamFreeNACK(stream *s, streamNACK *na);
|
||||
|
|
@ -200,6 +205,9 @@ listNode *streamLinkCGroupToEntry(stream *s, streamCG *cg, unsigned char *key);
|
|||
|
||||
/* PEL time list management (used by RDB loading) */
|
||||
void pelListInsertSorted(streamCG *cg, streamNACK *nack);
|
||||
void pelListUnlink(streamCG *cg, streamNACK *nack);
|
||||
void pelListInsertNacked(streamCG *cg, streamNACK *nack);
|
||||
uint64_t pelListNackedCount(streamCG *cg);
|
||||
|
||||
/* IDMP functions */
|
||||
idmpEntry *idmpEntryCreate(const char *iid, size_t iid_len, size_t *alloc_size);
|
||||
|
|
|
|||
410
src/t_hash.c
410
src/t_hash.c
|
|
@ -15,6 +15,7 @@
|
|||
#include "ebuckets.h"
|
||||
#include "entry.h"
|
||||
#include "cluster_asm.h"
|
||||
#include "vector.h"
|
||||
#include <math.h>
|
||||
|
||||
/* Threshold for HEXPIRE and HPERSIST to be considered whether it is worth to
|
||||
|
|
@ -45,6 +46,18 @@ typedef enum GetFieldRes {
|
|||
|
||||
typedef listpackEntry CommonEntry; /* extend usage beyond lp */
|
||||
|
||||
#define FIELDS_STACK_SIZE 16
|
||||
|
||||
/* A vec with an embedded stack buffer, used to collect field robj pointers
|
||||
* for subkey notifications without heap allocation in the common case. */
|
||||
typedef struct fieldvec { vec v; void *buf[FIELDS_STACK_SIZE]; } fieldvec;
|
||||
|
||||
static inline vec *fieldvecInit(fieldvec *fv, size_t cap) {
|
||||
vecInit(&fv->v, fv->buf, FIELDS_STACK_SIZE);
|
||||
vecReserve(&fv->v, cap);
|
||||
return &fv->v;
|
||||
}
|
||||
|
||||
/* hash field expiration (HFE) funcs */
|
||||
static ExpireAction onFieldExpire(eItem item, void *ctx);
|
||||
static ExpireMeta* hentryGetExpireMeta(const eItem field);
|
||||
|
|
@ -126,6 +139,7 @@ typedef struct OnFieldExpireCtx {
|
|||
robj *hashObj;
|
||||
redisDb *db;
|
||||
int activeEx; /* 1 for active expire, 0 for lazy expire */
|
||||
vec *vexpired; /* Expired fields vector */
|
||||
} OnFieldExpireCtx;
|
||||
|
||||
/* The implementation of hashes by dict was modified from storing fields as sds
|
||||
|
|
@ -360,7 +374,8 @@ static uint64_t listpackExGetMinExpire(robj *o) {
|
|||
}
|
||||
|
||||
/* Walk over fields and delete the expired ones. */
|
||||
void listpackExExpire(redisDb *db, kvobj *kv, ExpireInfo *info, int activeEx) {
|
||||
void listpackExExpire(redisDb *db, kvobj *kv, ExpireInfo *info) {
|
||||
OnFieldExpireCtx *ctx = info->ctx;
|
||||
serverAssert(kv->encoding == OBJ_ENCODING_LISTPACK_EX);
|
||||
uint64_t expired = 0, min = EB_EXPIRE_TIME_INVALID;
|
||||
unsigned char *ptr;
|
||||
|
|
@ -387,9 +402,15 @@ void listpackExExpire(redisDb *db, kvobj *kv, ExpireInfo *info, int activeEx) {
|
|||
if (val == HASH_LP_NO_TTL || (uint64_t) val > info->now)
|
||||
break;
|
||||
|
||||
/* Collect expired field for subkey notification. */
|
||||
if (ctx->vexpired) {
|
||||
char *fstr = (char *)(fref ? fref : intbuf);
|
||||
vecPush(ctx->vexpired, createStringObject(fstr, flen));
|
||||
}
|
||||
|
||||
propagateHashFieldDeletion(db, key, (char *)((fref) ? fref : intbuf), flen);
|
||||
server.stat_expired_subkeys++;
|
||||
if (activeEx) server.stat_expired_subkeys_active++;
|
||||
if (ctx->activeEx) server.stat_expired_subkeys_active++;
|
||||
|
||||
ptr = lpNext(lpt->lp, ptr);
|
||||
|
||||
|
|
@ -780,9 +801,13 @@ GetFieldRes hashTypeGetValue(redisDb *db, kvobj *o, sds field, unsigned char **v
|
|||
/* If the field is the last one in the hash, then the hash will be deleted */
|
||||
res = GETF_EXPIRED;
|
||||
robj *keyObj = createStringObject(key, sdslen(key));
|
||||
if (!(hfeFlags & HFE_LAZY_NO_NOTIFICATION))
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", keyObj, db->id);
|
||||
if ((hashTypeLength(o, 0) == 0) && (!(hfeFlags & HFE_LAZY_AVOID_HASH_DEL))) {
|
||||
unsigned long length = hashTypeLength(o, 0);
|
||||
if ((length != 0) && !(hfeFlags & HFE_LAZY_NO_NOTIFICATION)) {
|
||||
robj fobj, *farr[1] = {&fobj};
|
||||
initStaticStringObject(fobj, field);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpired", keyObj, db->id, farr, 1);
|
||||
}
|
||||
if ((length == 0) && (!(hfeFlags & HFE_LAZY_AVOID_HASH_DEL))) {
|
||||
if (!(hfeFlags & HFE_LAZY_NO_NOTIFICATION))
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", keyObj, db->id);
|
||||
dbDelete(db,keyObj);
|
||||
|
|
@ -1022,7 +1047,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);
|
||||
}
|
||||
|
||||
|
|
@ -1877,30 +1901,29 @@ void hashTypeRandomElement(robj *hashobj, unsigned long hashsize, CommonEntry *k
|
|||
*/
|
||||
uint64_t hashTypeExpire(redisDb *db, kvobj *o, uint32_t *quota, int updateSubexpires, int activeEx) {
|
||||
uint64_t noExpireLeftRes = EB_EXPIRE_TIME_INVALID;
|
||||
ExpireInfo info = {0};
|
||||
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
info = (ExpireInfo) {
|
||||
/* Collect expired field names for batched subkey notification.
|
||||
* Skip allocation entirely when subkey notifications are disabled. */
|
||||
fieldvec fvexpired;
|
||||
vec *vexpired = isSubkeyNotifyEnabled(NOTIFY_HASH) ?
|
||||
fieldvecInit(&fvexpired, FIELDS_STACK_SIZE) : NULL;
|
||||
|
||||
OnFieldExpireCtx onFieldExpireCtx = { .hashObj = o, .db = db, .activeEx = activeEx, .vexpired = vexpired };
|
||||
ExpireInfo info = (ExpireInfo) {
|
||||
.maxToExpire = *quota,
|
||||
.now = commandTimeSnapshot(),
|
||||
.ctx = &onFieldExpireCtx,
|
||||
.itemsExpired = 0};
|
||||
|
||||
listpackExExpire(db, o, &info, activeEx);
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
listpackExExpire(db, o, &info);
|
||||
} else {
|
||||
serverAssert(o->encoding == OBJ_ENCODING_HT);
|
||||
|
||||
dict *d = o->ptr;
|
||||
htMetadataEx *dictExpireMeta = htGetMetadataEx(d);
|
||||
|
||||
OnFieldExpireCtx onFieldExpireCtx = { .hashObj = o, .db = db, .activeEx = activeEx };
|
||||
|
||||
info = (ExpireInfo){
|
||||
.maxToExpire = *quota,
|
||||
.onExpireItem = onFieldExpire,
|
||||
.ctx = &onFieldExpireCtx,
|
||||
.now = commandTimeSnapshot()
|
||||
};
|
||||
|
||||
info.onExpireItem = onFieldExpire;
|
||||
ebExpire(&dictExpireMeta->hfe, &hashFieldExpireBucketsType, &info);
|
||||
}
|
||||
|
||||
|
|
@ -1913,7 +1936,11 @@ uint64_t hashTypeExpire(redisDb *db, kvobj *o, uint32_t *quota, int updateSubexp
|
|||
if (info.itemsExpired) {
|
||||
sds keystr = kvobjGetKey(o);
|
||||
robj *key = createStringObject(keystr, sdslen(keystr));
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", key, db->id);
|
||||
|
||||
/* Send subkey notification with all expired fields */
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpired", key, db->id,
|
||||
vexpired ? (robj**)vecData(vexpired) : NULL, vexpired ? vecSize(vexpired) : 0);
|
||||
|
||||
int slot;
|
||||
int deleted = 0;
|
||||
|
||||
|
|
@ -1936,6 +1963,14 @@ uint64_t hashTypeExpire(redisDb *db, kvobj *o, uint32_t *quota, int updateSubexp
|
|||
decrRefCount(key);
|
||||
}
|
||||
|
||||
/* Free collected expired fields */
|
||||
if (vexpired) {
|
||||
for (size_t i = 0; i < vecSize(vexpired); i++) {
|
||||
decrRefCount(vecGet(vexpired, i));
|
||||
}
|
||||
vecRelease(vexpired);
|
||||
}
|
||||
|
||||
/* return 0 if hash got deleted, EB_EXPIRE_TIME_INVALID if no more fields
|
||||
* with expiration. Else return next expiration time */
|
||||
return (info.nextExpireTime == EB_EXPIRE_TIME_INVALID) ? noExpireLeftRes : info.nextExpireTime;
|
||||
|
|
@ -2104,7 +2139,8 @@ void hsetnxCommand(client *c) {
|
|||
updateKeysizesHist(c->db, OBJ_HASH, hlen - 1, hlen);
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), kv, oldsize, kvobjAllocSize(kv));
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hset", c->argv[1], c->db->id);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH,"hset",c->argv[1],c->db->id,&c->argv[2],1);
|
||||
KSN_INVALIDATE_KVOBJ(kv);
|
||||
server.dirty++;
|
||||
}
|
||||
|
||||
|
|
@ -2141,7 +2177,17 @@ void hsetCommand(client *c) {
|
|||
updateKeysizesHist(c->db, OBJ_HASH, l - created, l);
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), kv, oldsize, kvobjAllocSize(kv));
|
||||
notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
|
||||
|
||||
/* Collect field pointers for subkey notification. Fields are at argv[2,4,6...]. */
|
||||
int numfields = (c->argc - 2) / 2;
|
||||
fieldvec fvset;
|
||||
vec *vset = fieldvecInit(&fvset, numfields);
|
||||
for (i = 0; i < numfields; i++) {
|
||||
vecPush(vset, c->argv[2 + i * 2]);
|
||||
}
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH,"hset",c->argv[1],c->db->id,(robj**)vecData(vset),numfields);
|
||||
vecRelease(vset);
|
||||
KSN_INVALIDATE_KVOBJ(kv);
|
||||
server.dirty += (c->argc - 2)/2;
|
||||
}
|
||||
|
||||
|
|
@ -2354,8 +2400,7 @@ err_expiration:
|
|||
*/
|
||||
void hsetexCommand(client *c) {
|
||||
int flags = 0, first_field_pos = 0, field_count = 0, expire_time_pos = -1;
|
||||
int updated = 0, deleted = 0, set_expiry;
|
||||
int expired = 0, fields_set = 0;
|
||||
int set_expiry;
|
||||
long long expire_time = EB_EXPIRE_TIME_INVALID;
|
||||
int64_t oldlen, newlen;
|
||||
HashTypeSetEx setex;
|
||||
|
|
@ -2382,6 +2427,13 @@ void hsetexCommand(client *c) {
|
|||
if (server.memory_tracking_enabled)
|
||||
oldsize = kvobjAllocSize(o);
|
||||
|
||||
/* Track fields for subkey notifications by event type. */
|
||||
fieldvec fvexpired, fvset, fvdeleted, fvupdated;
|
||||
vec *vexpired = fieldvecInit(&fvexpired, field_count);
|
||||
vec *vset = fieldvecInit(&fvset, field_count);
|
||||
vec *vdeleted = fieldvecInit(&fvdeleted, field_count);
|
||||
vec *vupdated = fieldvecInit(&fvupdated, field_count);
|
||||
|
||||
if (flags & (HFE_FXX | HFE_FNX)) {
|
||||
int found = 0;
|
||||
for (int i = 0; i < field_count; i++) {
|
||||
|
|
@ -2397,7 +2449,9 @@ void hsetexCommand(client *c) {
|
|||
|
||||
GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll, opt, NULL);
|
||||
int exists = (res == GETF_OK);
|
||||
expired += (res == GETF_EXPIRED);
|
||||
if (res == GETF_EXPIRED) {
|
||||
vecPush(vexpired, c->argv[first_field_pos + (i * 2)]);
|
||||
}
|
||||
found += exists;
|
||||
|
||||
/* Check for early exit if the condition is already invalid. */
|
||||
|
|
@ -2434,12 +2488,15 @@ void hsetexCommand(client *c) {
|
|||
opt |= HASH_SET_KEEP_TTL;
|
||||
|
||||
hashTypeSet(c->db, o, field, value, opt);
|
||||
fields_set = 1;
|
||||
vecPush(vset, c->argv[first_field_pos + (i * 2)]);
|
||||
/* Update the expiration time. */
|
||||
if (set_expiry) {
|
||||
int ret = hashTypeSetEx(o, field, expire_time, &setex);
|
||||
updated += (ret == HSETEX_OK);
|
||||
deleted += (ret == HSETEX_DELETED);
|
||||
if (ret == HSETEX_OK) {
|
||||
vecPush(vupdated, c->argv[first_field_pos + (i * 2)]);
|
||||
} else if (ret == HSETEX_DELETED) {
|
||||
vecPush(vdeleted, c->argv[first_field_pos + (i * 2)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2448,7 +2505,7 @@ void hsetexCommand(client *c) {
|
|||
|
||||
server.dirty += field_count;
|
||||
|
||||
if (deleted) {
|
||||
if (vecSize(vdeleted)) {
|
||||
/* If fields are deleted due to timestamp is being in the past, hdel's
|
||||
* are already propagated. No need to propagate the command itself. */
|
||||
preventCommandPropagation(c);
|
||||
|
|
@ -2469,27 +2526,43 @@ out:
|
|||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldsize, kvobjAllocSize(o));
|
||||
/* Emit keyspace notifications based on field expiry, mutation, or key deletion */
|
||||
if (fields_set || expired) {
|
||||
if (vecSize(vset) || vecSize(vexpired)) {
|
||||
newlen = (int64_t) hashTypeLength(o, 0);
|
||||
keyModified(c, c->db, c->argv[1], o, 1);
|
||||
if (expired)
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
|
||||
if (fields_set) {
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hset", c->argv[1], c->db->id);
|
||||
if (deleted || updated)
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel" : "hexpire", c->argv[1], c->db->id);
|
||||
if (vecSize(vexpired)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpired", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vexpired), vecSize(vexpired));
|
||||
}
|
||||
if (vecSize(vset)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hset", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vset), vecSize(vset));
|
||||
if (vecSize(vdeleted)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hdel", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vdeleted), vecSize(vdeleted));
|
||||
} else if (vecSize(vupdated)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpire", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vupdated), vecSize(vupdated));
|
||||
}
|
||||
}
|
||||
|
||||
KSN_INVALIDATE_KVOBJ(o);
|
||||
|
||||
/* Key may become empty due to lazy expiry in hashTypeGetValue()
|
||||
* or the new expiration time is in the past.*/
|
||||
if (newlen == 0) {
|
||||
newlen = -1;
|
||||
/* Del key but don't update KEYSIZES. else it will decr wrong bin in histogram */
|
||||
dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
}
|
||||
if (oldlen != newlen)
|
||||
updateKeysizesHist(c->db, OBJ_HASH, oldlen, newlen);
|
||||
}
|
||||
/* Key may become empty due to lazy expiry in hashTypeExists()
|
||||
* or the new expiration time is in the past.*/
|
||||
newlen = (int64_t) hashTypeLength(o, 0);
|
||||
if (newlen == 0) {
|
||||
newlen = -1;
|
||||
/* Del key but don't update KEYSIZES. else it will decr wrong bin in histogram */
|
||||
dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
}
|
||||
if (oldlen != newlen)
|
||||
updateKeysizesHist(c->db, OBJ_HASH, oldlen, newlen);
|
||||
|
||||
vecRelease(vexpired);
|
||||
vecRelease(vset);
|
||||
vecRelease(vdeleted);
|
||||
vecRelease(vupdated);
|
||||
}
|
||||
|
||||
void hincrbyCommand(client *c) {
|
||||
|
|
@ -2539,7 +2612,8 @@ void hincrbyCommand(client *c) {
|
|||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldsize, kvobjAllocSize(o));
|
||||
addReplyLongLong(c,value);
|
||||
keyModified(c,c->db,c->argv[1], o, 1);
|
||||
notifyKeyspaceEvent(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH,"hincrby",c->argv[1],c->db->id,&c->argv[2],1);
|
||||
KSN_INVALIDATE_KVOBJ(o);
|
||||
server.dirty++;
|
||||
}
|
||||
|
||||
|
|
@ -2597,7 +2671,8 @@ void hincrbyfloatCommand(client *c) {
|
|||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldsize, kvobjAllocSize(o));
|
||||
addReplyBulkCBuffer(c,buf,len);
|
||||
keyModified(c,c->db,c->argv[1],o,1);
|
||||
notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id,&c->argv[2],1);
|
||||
KSN_INVALIDATE_KVOBJ(o);
|
||||
server.dirty++;
|
||||
|
||||
/* Always replicate HINCRBYFLOAT as an HSETEX command with the final value
|
||||
|
|
@ -2645,19 +2720,24 @@ void hgetCommand(client *c) {
|
|||
|
||||
void hmgetCommand(client *c) {
|
||||
GetFieldRes res = GETF_OK;
|
||||
int i;
|
||||
int expired = 0, deleted = 0;
|
||||
int i, deleted = 0;
|
||||
|
||||
/* Don't abort when the key cannot be found. Non-existing keys are empty
|
||||
* hashes, where HMGET should respond with a series of null bulks. */
|
||||
kvobj *o = lookupKeyRead(c->db, c->argv[1]);
|
||||
if (checkType(c,o,OBJ_HASH)) return;
|
||||
|
||||
/* Track expired fields for subkey notification. */
|
||||
fieldvec fvexpired;
|
||||
vec *vexpired = fieldvecInit(&fvexpired, c->argc-2);
|
||||
|
||||
addReplyArrayLen(c, c->argc-2);
|
||||
for (i = 2; i < c->argc ; i++) {
|
||||
if (!deleted) {
|
||||
res = addHashFieldToReply(c, o, c->argv[i]->ptr, HFE_LAZY_NO_NOTIFICATION);
|
||||
expired += (res == GETF_EXPIRED);
|
||||
if (res == GETF_EXPIRED) {
|
||||
vecPush(vexpired, c->argv[i]);
|
||||
}
|
||||
deleted += (res == GETF_EXPIRED_HASH);
|
||||
} else {
|
||||
/* If hash got lazy expired since all fields are expired (o is invalid),
|
||||
|
|
@ -2666,11 +2746,14 @@ void hmgetCommand(client *c) {
|
|||
}
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
|
||||
if (deleted)
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
if (vecSize(vexpired)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpired", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vexpired), vecSize(vexpired));
|
||||
}
|
||||
if (deleted)
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
|
||||
vecRelease(vexpired);
|
||||
}
|
||||
|
||||
/* Get and delete the value of one or more fields of a given hash key.
|
||||
|
|
@ -2679,7 +2762,7 @@ void hmgetCommand(client *c) {
|
|||
* doesn’t exist.
|
||||
*/
|
||||
void hgetdelCommand(client *c) {
|
||||
int res = 0, hfe = 0, deleted = 0, expired = 0;
|
||||
int res = 0, hfe = 0;
|
||||
int64_t oldlen = -1; /* not exists as long as it is not set */
|
||||
long num_fields = 0;
|
||||
size_t oldsize = 0;
|
||||
|
|
@ -2717,6 +2800,11 @@ void hgetdelCommand(client *c) {
|
|||
oldsize = kvobjAllocSize(o);
|
||||
}
|
||||
|
||||
/* Track fields for subkey notifications. */
|
||||
fieldvec fvexpired, fvdeleted;
|
||||
vec *vexpired = fieldvecInit(&fvexpired, num_fields);
|
||||
vec *vdeleted = fieldvecInit(&fvdeleted, num_fields);
|
||||
|
||||
addReplyArrayLen(c, num_fields);
|
||||
for (int i = 4; i < c->argc; i++) {
|
||||
const int flags = HFE_LAZY_NO_NOTIFICATION |
|
||||
|
|
@ -2725,27 +2813,47 @@ void hgetdelCommand(client *c) {
|
|||
HFE_LAZY_NO_UPDATE_KEYSIZES |
|
||||
HFE_LAZY_NO_UPDATE_ALLOCSIZES;
|
||||
res = addHashFieldToReply(c, o, c->argv[i]->ptr, flags);
|
||||
expired += (res == GETF_EXPIRED);
|
||||
if (res == GETF_EXPIRED) {
|
||||
vecPush(vexpired, c->argv[i]);
|
||||
}
|
||||
/* Try to delete only if it's found and not expired lazily. */
|
||||
if (res == GETF_OK) {
|
||||
deleted++;
|
||||
vecPush(vdeleted, c->argv[i]);
|
||||
serverAssert(hashTypeDelete(o, c->argv[i]->ptr) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Return if no modification has been made. */
|
||||
if (expired == 0 && deleted == 0)
|
||||
if (vecSize(vexpired) == 0 && vecSize(vdeleted) == 0) {
|
||||
vecRelease(vexpired);
|
||||
vecRelease(vdeleted);
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t newlen = (int64_t) hashTypeLength(o, 0);
|
||||
/* del key if become empty */
|
||||
int delete_key = (newlen == 0);
|
||||
/* update new len for keysizes histogram */
|
||||
int64_t hist_newlen = delete_key ? -1 : newlen;
|
||||
if (oldlen != hist_newlen)
|
||||
updateKeysizesHist(c->db, OBJ_HASH, oldlen, hist_newlen);
|
||||
/* update memory tracking */
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldsize, kvobjAllocSize(o));
|
||||
/* is it last HFE */
|
||||
if (!delete_key && hfe && (hashTypeIsFieldsWithExpire(o) == 0))
|
||||
estoreRemove(c->db->subexpires, getKeySlot(c->argv[1]->ptr), o);
|
||||
|
||||
keyModified(c, c->db, c->argv[1], o, 1);
|
||||
|
||||
if (expired)
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
|
||||
if (deleted) {
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hdel", c->argv[1], c->db->id);
|
||||
server.dirty += deleted;
|
||||
if (vecSize(vexpired)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpired", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vexpired), vecSize(vexpired));
|
||||
}
|
||||
if (vecSize(vdeleted)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hdel", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vdeleted), vecSize(vdeleted));
|
||||
server.dirty += vecSize(vdeleted);
|
||||
|
||||
/* Propagate as HDEL command.
|
||||
* Orig: HGETDEL <key> FIELDS <numfields> field1 field2 ...
|
||||
|
|
@ -2755,21 +2863,16 @@ void hgetdelCommand(client *c) {
|
|||
rewriteClientCommandArgument(c, 2, NULL); /* Delete <numfields> arg */
|
||||
}
|
||||
|
||||
vecRelease(vexpired);
|
||||
vecRelease(vdeleted);
|
||||
KSN_INVALIDATE_KVOBJ(o);
|
||||
|
||||
/* Key may have become empty because of deleting fields or lazy expire. */
|
||||
int64_t newlen = (int64_t) hashTypeLength(o, 0);
|
||||
if (newlen == 0) {
|
||||
newlen = -1;
|
||||
if (delete_key) {
|
||||
/* Del key but don't update KEYSIZES. else it will decr wrong bin in histogram */
|
||||
dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
} else {
|
||||
if (hfe && (hashTypeIsFieldsWithExpire(o) == 0)) { /*is it last HFE*/
|
||||
estoreRemove(c->db->subexpires, getKeySlot(kvobjGetKey(o)), o);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldlen != newlen)
|
||||
updateKeysizesHist(c->db, OBJ_HASH, oldlen, newlen);
|
||||
}
|
||||
|
||||
/* Get the value of one or more fields of a given hash key and optionally set
|
||||
|
|
@ -2783,7 +2886,6 @@ void hgetdelCommand(client *c) {
|
|||
* doesn’t exist.
|
||||
*/
|
||||
void hgetexCommand(client *c) {
|
||||
int expired = 0, deleted = 0, updated = 0;
|
||||
int parse_flags = 0, expire_time_pos = -1, first_field_pos = -1, num_fields = -1;
|
||||
long long expire_time = 0;
|
||||
int64_t oldlen = 0, newlen = -1;
|
||||
|
|
@ -2813,6 +2915,12 @@ void hgetexCommand(client *c) {
|
|||
if (parse_flags)
|
||||
hashTypeSetExInit(c->argv[1], o, c, c->db, 0, &setex);
|
||||
|
||||
/* Track fields for subkey notifications by event type. */
|
||||
fieldvec fvexpired, fvdeleted, fvupdated;
|
||||
vec *vexpired = fieldvecInit(&fvexpired, num_fields);
|
||||
vec *vdeleted = fieldvecInit(&fvdeleted, num_fields);
|
||||
vec *vupdated = fieldvecInit(&fvupdated, num_fields);
|
||||
|
||||
addReplyArrayLen(c, num_fields);
|
||||
for (int i = first_field_pos; i < first_field_pos + num_fields; i++) {
|
||||
const int flags = HFE_LAZY_NO_NOTIFICATION |
|
||||
|
|
@ -2822,7 +2930,9 @@ void hgetexCommand(client *c) {
|
|||
HFE_LAZY_NO_UPDATE_ALLOCSIZES;
|
||||
sds field = c->argv[i]->ptr;
|
||||
int res = addHashFieldToReply(c, o, c->argv[i]->ptr, flags);
|
||||
expired += (res == GETF_EXPIRED);
|
||||
if (res == GETF_EXPIRED) {
|
||||
vecPush(vexpired, c->argv[i]);
|
||||
}
|
||||
|
||||
/* Set expiration only if the field exists and not expired lazily. */
|
||||
if (res == GETF_OK && parse_flags) {
|
||||
|
|
@ -2830,8 +2940,11 @@ void hgetexCommand(client *c) {
|
|||
expire_time = EB_EXPIRE_TIME_INVALID;
|
||||
|
||||
res = hashTypeSetEx(o, field, expire_time, &setex);
|
||||
deleted += (res == HSETEX_DELETED);
|
||||
updated += (res == HSETEX_OK);
|
||||
if (res == HSETEX_DELETED) {
|
||||
vecPush(vdeleted, c->argv[i]);
|
||||
} else if (res == HSETEX_OK) {
|
||||
vecPush(vupdated, c->argv[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2842,10 +2955,14 @@ void hgetexCommand(client *c) {
|
|||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldsize, kvobjAllocSize(o));
|
||||
|
||||
/* Exit early if no modification has been made. */
|
||||
if (expired == 0 && deleted == 0 && updated == 0)
|
||||
if (vecSize(vexpired) == 0 && vecSize(vdeleted) == 0 && vecSize(vupdated) == 0) {
|
||||
vecRelease(vexpired);
|
||||
vecRelease(vdeleted);
|
||||
vecRelease(vupdated);
|
||||
return;
|
||||
}
|
||||
|
||||
server.dirty += deleted + updated;
|
||||
server.dirty += vecSize(vdeleted) + vecSize(vupdated);
|
||||
keyModified(c, c->db, c->argv[1], o, 1);
|
||||
|
||||
/* This command will never be propagated as it is. It will be propagated as
|
||||
|
|
@ -2856,16 +2973,19 @@ void hgetexCommand(client *c) {
|
|||
* If PERSIST flags is used, it will be propagated as HPERSIST command.
|
||||
* IF EX/EXAT/PX/PXAT flags are used, it will be replicated as HPEXPRITEAT.
|
||||
*/
|
||||
if (expired)
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
|
||||
if (updated) {
|
||||
if (vecSize(vexpired)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpired", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vexpired), vecSize(vexpired));
|
||||
}
|
||||
if (vecSize(vupdated)) {
|
||||
/* Build canonical command for propagation */
|
||||
int canonical_argc;
|
||||
robj **canonical_argv;
|
||||
int idx = 0;
|
||||
|
||||
if (parse_flags & HFE_PERSIST) {
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hpersist", c->argv[1], c->db->id);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hpersist", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vupdated), vecSize(vupdated));
|
||||
/* Build canonical HPERSIST command: HPERSIST key FIELDS numfields field1 field2 ... */
|
||||
canonical_argc = 4 + num_fields;
|
||||
canonical_argv = zmalloc(sizeof(robj*) * canonical_argc);
|
||||
|
|
@ -2874,7 +2994,8 @@ void hgetexCommand(client *c) {
|
|||
canonical_argv[idx++] = c->argv[1]; /* key */
|
||||
incrRefCount(c->argv[1]);
|
||||
} else {
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hexpire", c->argv[1], c->db->id);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpire", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vupdated), vecSize(vupdated));
|
||||
/* Build canonical HPEXPIREAT command: HPEXPIREAT key timestamp FIELDS numfields field1 field2 ... */
|
||||
canonical_argc = 5 + num_fields;
|
||||
canonical_argv = zmalloc(sizeof(robj*) * canonical_argc);
|
||||
|
|
@ -2894,13 +3015,18 @@ void hgetexCommand(client *c) {
|
|||
}
|
||||
|
||||
replaceClientCommandVector(c, canonical_argc, canonical_argv);
|
||||
} else if (deleted) {
|
||||
} else if (vecSize(vdeleted)) {
|
||||
/* If we are here, fields are deleted because new timestamp was in the
|
||||
* past. HDELs are already propagated as part of hashTypeSetEx(). */
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hdel", c->argv[1], c->db->id);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hdel", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vdeleted), vecSize(vdeleted));
|
||||
preventCommandPropagation(c);
|
||||
}
|
||||
|
||||
vecRelease(vexpired);
|
||||
vecRelease(vdeleted);
|
||||
vecRelease(vupdated);
|
||||
|
||||
/* Key may become empty due to lazy expiry in addHashFieldToReply()
|
||||
* or the new expiration time is in the past.*/
|
||||
newlen = hashTypeLength(o, 0);
|
||||
|
|
@ -2914,7 +3040,7 @@ void hgetexCommand(client *c) {
|
|||
|
||||
void hdelCommand(client *c) {
|
||||
kvobj *o;
|
||||
int j, deleted = 0, keyremoved = 0;
|
||||
int j, keyremoved = 0;
|
||||
size_t oldsize = 0;
|
||||
|
||||
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
|
||||
|
|
@ -2932,42 +3058,54 @@ void hdelCommand(client *c) {
|
|||
* field with expiration and removes it from global HFE DS. */
|
||||
int isHFE = hashTypeIsFieldsWithExpire(o);
|
||||
|
||||
/* Track which fields were actually deleted for subkey notification. */
|
||||
fieldvec fvdeleted;
|
||||
vec *vdeleted = fieldvecInit(&fvdeleted, c->argc - 2);
|
||||
|
||||
if (o->encoding == OBJ_ENCODING_HT)
|
||||
dictPauseAutoResize((dict*)o->ptr);
|
||||
for (j = 2; j < c->argc; j++) {
|
||||
if (hashTypeDelete(o,c->argv[j]->ptr)) {
|
||||
deleted++;
|
||||
vecPush(vdeleted, c->argv[j]);
|
||||
if (hashTypeLength(o, 0) == 0) {
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldsize, kvobjAllocSize(o));
|
||||
/* del key but don't update KEYSIZES. Else it will decr wrong bin in histogram */
|
||||
dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
|
||||
keyremoved = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyremoved && o->encoding == OBJ_ENCODING_HT) {
|
||||
dictResumeAutoResize((dict*)o->ptr);
|
||||
dictShrinkIfNeeded((dict*)o->ptr);
|
||||
}
|
||||
if (server.memory_tracking_enabled && !keyremoved)
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), o, oldsize, kvobjAllocSize(o));
|
||||
if (deleted) {
|
||||
int64_t newLen = -1; /* The value -1 indicates that the key is deleted. */
|
||||
keyModified(c, c->db, c->argv[1], keyremoved ? NULL : o, 1);
|
||||
notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);
|
||||
if (vecSize(vdeleted)) {
|
||||
/* Update keysizes histogram */
|
||||
int64_t newLen = (int64_t) hashTypeLength(o, 0);
|
||||
updateKeysizesHist(c->db, OBJ_HASH, oldLen, keyremoved ? -1 : newLen);
|
||||
|
||||
if (keyremoved) {
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
/* del key but don't update KEYSIZES. Else it will decr wrong bin in histogram */
|
||||
dbDeleteSkipKeysizesUpdate(c->db, c->argv[1]);
|
||||
} else {
|
||||
if (isHFE && (hashTypeIsFieldsWithExpire(o) == 0)) /* is it last HFE */
|
||||
/* is it last HFE */
|
||||
if (isHFE && (hashTypeIsFieldsWithExpire(o) == 0))
|
||||
estoreRemove(c->db->subexpires, getKeySlot(c->argv[1]->ptr), o);
|
||||
newLen = oldLen - deleted;
|
||||
}
|
||||
updateKeysizesHist(c->db, OBJ_HASH, oldLen, newLen);
|
||||
server.dirty += deleted;
|
||||
|
||||
/* Signal key modification */
|
||||
keyModified(c, c->db, c->argv[1], keyremoved ? NULL : o, 1);
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH,"hdel",c->argv[1],c->db->id,(robj**)vecData(vdeleted),vecSize(vdeleted));
|
||||
|
||||
KSN_INVALIDATE_KVOBJ(o); /* Invalidate local kvobj pointer */
|
||||
|
||||
/* Notify del event if key was deleted */
|
||||
if (keyremoved) notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
server.dirty += vecSize(vdeleted);
|
||||
}
|
||||
addReplyLongLong(c,deleted);
|
||||
addReplyLongLong(c,vecSize(vdeleted));
|
||||
vecRelease(vdeleted);
|
||||
}
|
||||
|
||||
void hlenCommand(client *c) {
|
||||
|
|
@ -3459,9 +3597,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);
|
||||
|
||||
|
|
@ -3508,6 +3643,11 @@ static ExpireAction onFieldExpire(eItem item, void *ctx) {
|
|||
if (server.memory_tracking_enabled)
|
||||
oldsize = kvobjAllocSize(kv);
|
||||
sds field = entryGetField(e);
|
||||
|
||||
/* Collect expired field for subkey notification (before deletion) */
|
||||
if (expCtx->vexpired)
|
||||
vecPush(expCtx->vexpired, createStringObject(field, sdslen(field)));
|
||||
|
||||
propagateHashFieldDeletion(expCtx->db, key, field, sdslen(field));
|
||||
|
||||
/* update keysizes */
|
||||
|
|
@ -3589,15 +3729,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;
|
||||
|
|
@ -3800,7 +3941,7 @@ static void httlGenericCommand(client *c, const char *cmd, long long basetime, i
|
|||
*/
|
||||
static void hexpireGenericCommand(client *c, long long basetime, int unit) {
|
||||
HashCommandArgs args;
|
||||
int fieldsNotSet = 0, updated = 0, deleted = 0;
|
||||
int fieldsNotSet = 0;
|
||||
int64_t oldlen, newlen;
|
||||
robj *keyArg = c->argv[1];
|
||||
size_t oldsize = 0;
|
||||
|
|
@ -3836,12 +3977,20 @@ static void hexpireGenericCommand(client *c, long long basetime, int unit) {
|
|||
int *fieldsToRemove = NULL;
|
||||
int removeCount = 0;
|
||||
|
||||
/* Track fields for subkey notifications. */
|
||||
fieldvec fvupdated, fvdeleted;
|
||||
vec *vupdated = fieldvecInit(&fvupdated, args.fieldCount);
|
||||
vec *vdeleted = fieldvecInit(&fvdeleted, args.fieldCount);
|
||||
|
||||
for (int i = 0; i < args.fieldCount; i++) {
|
||||
int fieldPos = args.firstFieldPos + i;
|
||||
sds field = c->argv[fieldPos]->ptr;
|
||||
SetExRes res = hashTypeSetEx(hashObj, field, args.expireTime, &exCtx);
|
||||
updated += (res == HSETEX_OK);
|
||||
deleted += (res == HSETEX_DELETED);
|
||||
if (res == HSETEX_OK) {
|
||||
vecPush(vupdated, c->argv[fieldPos]);
|
||||
} else if (res == HSETEX_DELETED) {
|
||||
vecPush(vdeleted, c->argv[fieldPos]);
|
||||
}
|
||||
|
||||
if (unlikely(res != HSETEX_OK)) {
|
||||
if (fieldsToRemove == NULL) {
|
||||
|
|
@ -3859,11 +4008,13 @@ static void hexpireGenericCommand(client *c, long long basetime, int unit) {
|
|||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(keyArg->ptr), hashObj, oldsize, kvobjAllocSize(hashObj));
|
||||
|
||||
if (deleted + updated > 0) {
|
||||
server.dirty += deleted + updated;
|
||||
if (vecSize(vdeleted) + vecSize(vupdated) > 0) {
|
||||
server.dirty += vecSize(vdeleted) + vecSize(vupdated);
|
||||
keyModified(c, c->db, keyArg, hashObj, 1);
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel" : "hexpire",
|
||||
keyArg, c->db->id);
|
||||
if (vecSize(vdeleted)) notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hdel",
|
||||
keyArg, c->db->id, (robj**)vecData(vdeleted), vecSize(vdeleted));
|
||||
if (vecSize(vupdated)) notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hexpire",
|
||||
keyArg, c->db->id, (robj**)vecData(vupdated), vecSize(vupdated));
|
||||
}
|
||||
|
||||
newlen = (int64_t) hashTypeLength(hashObj, 0);
|
||||
|
|
@ -3880,7 +4031,9 @@ static void hexpireGenericCommand(client *c, long long basetime, int unit) {
|
|||
/* Avoid propagating command if not even one field was updated (Either because
|
||||
* the time is in the past, and corresponding HDELs were sent, or conditions
|
||||
* not met) then it is useless and invalid to propagate command with no fields */
|
||||
if (updated == 0) {
|
||||
if (vecSize(vupdated) == 0) {
|
||||
vecRelease(vupdated);
|
||||
vecRelease(vdeleted);
|
||||
preventCommandPropagation(c);
|
||||
zfree(fieldsToRemove);
|
||||
return;
|
||||
|
|
@ -3901,13 +4054,16 @@ static void hexpireGenericCommand(client *c, long long basetime, int unit) {
|
|||
for (int i = removeCount - 1; i >= 0; i--) {
|
||||
rewriteClientCommandArgument(c, fieldsToRemove[i], NULL);
|
||||
}
|
||||
robj *newFieldCount = createStringObjectFromLongLong(updated);
|
||||
robj *newFieldCount = createStringObjectFromLongLong(vecSize(vupdated));
|
||||
rewriteClientCommandArgument(c, args.fieldsPos + 1, newFieldCount);
|
||||
decrRefCount(newFieldCount);
|
||||
}
|
||||
|
||||
if (fieldsToRemove)
|
||||
zfree(fieldsToRemove);
|
||||
|
||||
vecRelease(vupdated);
|
||||
vecRelease(vdeleted);
|
||||
}
|
||||
|
||||
/* HPEXPIRE key milliseconds [ NX | XX | GT | LT] FIELDS numfields <field [field ...]> */
|
||||
|
|
@ -3954,7 +4110,6 @@ void hpexpiretimeCommand(client *c) {
|
|||
/* HPERSIST key FIELDS numfields <field [field ...]> */
|
||||
void hpersistCommand(client *c) {
|
||||
long numFields = 0, numFieldsAt = 3;
|
||||
int changed = 0; /* Used to determine whether to send a notification. */
|
||||
|
||||
/* Read the hash object */
|
||||
kvobj *hashObj = lookupKeyWrite(c->db, c->argv[1]);
|
||||
|
|
@ -3987,6 +4142,10 @@ void hpersistCommand(client *c) {
|
|||
return;
|
||||
}
|
||||
|
||||
/* Track which fields were successfully persisted for subkey notification. */
|
||||
fieldvec fvpersisted;
|
||||
vec *vpersisted = fieldvecInit(&fvpersisted, numFields);
|
||||
|
||||
if (hashObj->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
addReplyArrayLen(c, numFields);
|
||||
for (int i = 0 ; i < numFields ; i++) {
|
||||
|
|
@ -4002,6 +4161,7 @@ void hpersistCommand(client *c) {
|
|||
else
|
||||
addReplyLongLong(c, HFE_PERSIST_NO_TTL);
|
||||
}
|
||||
vecRelease(vpersisted);
|
||||
return;
|
||||
} else if (hashObj->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
long long prevExpire;
|
||||
|
|
@ -4043,7 +4203,7 @@ void hpersistCommand(client *c) {
|
|||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), hashObj, oldsize, kvobjAllocSize(hashObj));
|
||||
addReplyLongLong(c, HFE_PERSIST_OK);
|
||||
changed = 1;
|
||||
vecPush(vpersisted, c->argv[numFieldsAt + 1 + i]);
|
||||
}
|
||||
} else if (hashObj->encoding == OBJ_ENCODING_HT) {
|
||||
dict *d = hashObj->ptr;
|
||||
|
|
@ -4075,7 +4235,7 @@ void hpersistCommand(client *c) {
|
|||
|
||||
hfieldPersist(hashObj, entry);
|
||||
addReplyLongLong(c, HFE_PERSIST_OK);
|
||||
changed = 1;
|
||||
vecPush(vpersisted, c->argv[numFieldsAt + 1 + i]);
|
||||
}
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db, getKeySlot(c->argv[1]->ptr), hashObj, oldsize, kvobjAllocSize(hashObj));
|
||||
|
|
@ -4085,9 +4245,11 @@ void hpersistCommand(client *c) {
|
|||
|
||||
/* Generates a hpersist event if the expiry time associated with any field
|
||||
* has been successfully deleted. */
|
||||
if (changed) {
|
||||
notifyKeyspaceEvent(NOTIFY_HASH, "hpersist", c->argv[1], c->db->id);
|
||||
if (vecSize(vpersisted)) {
|
||||
notifyKeyspaceEventWithSubkeys(NOTIFY_HASH, "hpersist", c->argv[1],
|
||||
c->db->id, (robj**)vecData(vpersisted), vecSize(vpersisted));
|
||||
keyModified(c, c->db, c->argv[1], hashObj, 1);
|
||||
server.dirty++;
|
||||
}
|
||||
vecRelease(vpersisted);
|
||||
}
|
||||
|
|
|
|||
455
src/t_stream.c
455
src/t_stream.c
|
|
@ -57,8 +57,8 @@ static int createIdempotencyHash(robj **argv, int64_t numfields, XXH128_hash_t *
|
|||
static void idmpEvictOldestEntry(stream *s, idmpProducer *producer);
|
||||
|
||||
/* Forward declarations for PEL time list functions */
|
||||
static void pelListInsertAfter(streamCG *cg, streamNACK *after, streamNACK *nack);
|
||||
static void pelListInsertAtTail(streamCG *cg, streamNACK *nack);
|
||||
static void pelListUnlink(streamCG *cg, streamNACK *nack);
|
||||
static void pelListUpdate(streamCG *cg, streamNACK *nack, mstime_t new_delivery_time);
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
|
|
@ -281,24 +281,19 @@ robj *streamDup(robj *o) {
|
|||
|
||||
serverAssert(new_cg != NULL);
|
||||
|
||||
/* Consumer Group PEL */
|
||||
raxIterator ri_cg_pel;
|
||||
raxStart(&ri_cg_pel,cg->pel);
|
||||
raxSeek(&ri_cg_pel,"^",NULL,0);
|
||||
while(raxNext(&ri_cg_pel)){
|
||||
streamNACK *nack = ri_cg_pel.data;
|
||||
streamID nack_id;
|
||||
streamDecodeID(ri_cg_pel.key, &nack_id);
|
||||
streamNACK *new_nack = streamCreateNACK(new_s, NULL, &nack_id);
|
||||
/* Consumer Group PEL -- walk the time-ordered list so we can
|
||||
* append directly and preserve NACK zone structure. */
|
||||
for (streamNACK *nack = cg->pel_time_head; nack; nack = nack->pel_next) {
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf, &nack->id);
|
||||
streamNACK *new_nack = streamCreateNACK(new_s, NULL, &nack->id);
|
||||
new_nack->delivery_time = nack->delivery_time;
|
||||
new_nack->delivery_count = nack->delivery_count;
|
||||
new_nack->cgroup_ref_node = streamLinkCGroupToEntry(new_s, new_cg, ri_cg_pel.key);
|
||||
raxInsert(new_cg->pel, ri_cg_pel.key, sizeof(streamID), new_nack, NULL);
|
||||
|
||||
/* Insert in sorted order to preserve ordering */
|
||||
pelListInsertSorted(new_cg, new_nack);
|
||||
new_nack->cgroup_ref_node = streamLinkCGroupToEntry(new_s, new_cg, buf);
|
||||
raxInsert(new_cg->pel, buf, sizeof(streamID), new_nack, NULL);
|
||||
pelListInsertAtTail(new_cg, new_nack);
|
||||
if (nack == cg->pel_nack_tail) new_cg->pel_nack_tail = new_nack;
|
||||
}
|
||||
raxStop(&ri_cg_pel);
|
||||
|
||||
/* Consumers */
|
||||
raxIterator ri_consumers;
|
||||
|
|
@ -802,6 +797,33 @@ typedef struct {
|
|||
#define DELETE_STRATEGY_DELREF 2 /* Delete from pending entries list */
|
||||
#define DELETE_STRATEGY_ACKED 3 /* Only delete messages that are acknowledged */
|
||||
|
||||
/* XNACK mode flags – control how the delivery counter is adjusted when
|
||||
* a pending entry is released back to the group (NACKed). */
|
||||
#define XNACK_SILENT 0 /* Decrement delivery_count by 1 (undo the delivery) */
|
||||
#define XNACK_FAIL 1 /* Keep delivery_count unchanged */
|
||||
#define XNACK_FATAL 2 /* Set delivery_count to LLONG_MAX (permanent failure) */
|
||||
|
||||
/* Set the delivery attempts counter on a NACK entry. When retrycount >= 0
|
||||
* the counter is set to that explicit value; otherwise it is adjusted
|
||||
* according to the XNACK mode (SILENT/FAIL/FATAL). */
|
||||
static void nackSetDeliveryCount(streamNACK *nack, int mode, long long retrycount) {
|
||||
if (retrycount >= 0) {
|
||||
nack->delivery_count = (uint64_t)retrycount;
|
||||
} else {
|
||||
switch (mode) {
|
||||
case XNACK_SILENT:
|
||||
if (nack->delivery_count > 0)
|
||||
nack->delivery_count--;
|
||||
break;
|
||||
case XNACK_FAIL:
|
||||
break;
|
||||
case XNACK_FATAL:
|
||||
nack->delivery_count = LLONG_MAX;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Trim the stream 's' according to args->trim_strategy, and return the
|
||||
* number of elements removed from the stream. The 'approx' option, if non-zero,
|
||||
* specifies that the trimming must be performed in a approximated way in
|
||||
|
|
@ -1869,6 +1891,18 @@ static inline void streamPropagateXCLAIMCopyFree(int dbid, robj *key, robj *grou
|
|||
alsoPropagate(dbid,argv,14,PROPAGATE_AOF|PROPAGATE_REPL);
|
||||
}
|
||||
|
||||
/* Propagate an XACK command to AOF and replicas. Used when a PEL entry is
|
||||
* removed implicitly (e.g. entry no longer exists during XCLAIM/XAUTOCLAIM)
|
||||
* and the NACK has no consumer, so XCLAIM propagation is not applicable. */
|
||||
static inline void streamPropagateXACK(int dbid, robj *key, robj *groupname, robj *id) {
|
||||
robj *argv[4];
|
||||
argv[0] = shared.xack;
|
||||
argv[1] = key;
|
||||
argv[2] = groupname;
|
||||
argv[3] = id;
|
||||
alsoPropagate(dbid,argv,4,PROPAGATE_AOF|PROPAGATE_REPL);
|
||||
}
|
||||
|
||||
/* As a result of an explicit XCLAIM or XREADGROUP command, new entries
|
||||
* are created in the pending list of the stream and consumers. We need
|
||||
* to propagate this changes in the form of XCLAIM commands. */
|
||||
|
|
@ -1908,11 +1942,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];
|
||||
|
|
@ -2062,11 +2099,12 @@ size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end
|
|||
if (nack->consumer != consumer) {
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf, &nack->id);
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
if (nack->consumer)
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
nack->consumer = consumer;
|
||||
raxInsert(consumer->pel,buf,sizeof(buf),nack,NULL);
|
||||
}
|
||||
nack->delivery_count++;
|
||||
nack->delivery_count += nack->delivery_count == LLONG_MAX ? 0 : 1;
|
||||
pelListUpdate(group, nack, cmd_time_snapshot); /* Moves element from beginning to end of list */
|
||||
|
||||
consumer->active_time = cmd_time_snapshot;
|
||||
|
|
@ -2204,7 +2242,8 @@ size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end
|
|||
nack = result;
|
||||
/* Only transfer between consumers if they're different */
|
||||
if (nack->consumer != consumer) {
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
if (nack->consumer)
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
nack->consumer = consumer;
|
||||
raxInsert(consumer->pel,buf,sizeof(buf),nack,NULL);
|
||||
}
|
||||
|
|
@ -2289,7 +2328,7 @@ size_t streamReplyWithRangeFromConsumerPEL(client *c, stream *s, streamID *start
|
|||
addReplyNullArray(c);
|
||||
} else {
|
||||
streamNACK *nack = ri.data;
|
||||
nack->delivery_count++;
|
||||
nack->delivery_count += nack->delivery_count == LLONG_MAX ? 0 : 1;
|
||||
pelListUpdate(group, nack, commandTimeSnapshot());
|
||||
}
|
||||
arraylen++;
|
||||
|
|
@ -2874,6 +2913,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 */
|
||||
|
||||
|
|
@ -2934,10 +2974,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 */
|
||||
|
|
@ -2963,6 +3000,7 @@ void xreadCommand(client *c) {
|
|||
flags |= STREAM_RWR_CLAIMED;
|
||||
}
|
||||
|
||||
unsigned long propCount = 0;
|
||||
if (serve_synchronously) {
|
||||
arraylen++;
|
||||
if (arraylen == 1) arraylen_ptr = addReplyDeferredLen(c);
|
||||
|
|
@ -2977,7 +3015,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)
|
||||
|
|
@ -2992,6 +3029,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. */
|
||||
|
|
@ -3133,7 +3178,8 @@ void streamCleanupEntryCGroupRefs(stream *s, streamID *id) {
|
|||
/* Remove from group and consumer PELs */
|
||||
pelListUnlink(group, nack);
|
||||
raxRemove(group->pel, buf, sizeof(buf), NULL);
|
||||
raxRemove(nack->consumer->pel, buf, sizeof(buf), NULL);
|
||||
if (nack->consumer)
|
||||
raxRemove(nack->consumer->pel, buf, sizeof(buf), NULL);
|
||||
/* Since we're removing all references from the cgroups_ref, we can directly
|
||||
* free the NACK without unlinking it from the cgroups_ref. */
|
||||
streamFreeNACK(s, nack);
|
||||
|
|
@ -3266,6 +3312,7 @@ streamCG *streamCreateCG(stream *s, char *name, size_t namelen, streamID *id, lo
|
|||
cg->pel = raxNewWithMetadata(0, &s->alloc_size);
|
||||
cg->pel_time_head = NULL;
|
||||
cg->pel_time_tail = NULL;
|
||||
cg->pel_nack_tail = NULL;
|
||||
cg->consumers = raxNewWithMetadata(0, &s->alloc_size);
|
||||
cg->last_id.ms = 0;
|
||||
cg->last_id.seq = 0;
|
||||
|
|
@ -3281,8 +3328,8 @@ static void streamFreeCG(stream *s, streamCG *cg) {
|
|||
streamFreeNACKCtx ctx = {s, cg};
|
||||
raxFreeWithCbAndContext(cg->pel, streamFreeNACKGeneric, &ctx);
|
||||
|
||||
/* pel_time_head/tail should now be NULL after unlinking all NACKs */
|
||||
serverAssert(cg->pel_time_head == NULL && cg->pel_time_tail == NULL);
|
||||
/* pel_time_head/tail/pel_nack_tail should now be NULL after unlinking all NACKs */
|
||||
serverAssert(cg->pel_time_head == NULL && cg->pel_time_tail == NULL && cg->pel_nack_tail == NULL);
|
||||
|
||||
raxFreeWithCbAndContext(cg->consumers, streamFreeConsumerGeneric, s);
|
||||
size_t usable;
|
||||
|
|
@ -3773,7 +3820,8 @@ void xackCommand(client *c) {
|
|||
streamNACK *nack = result;
|
||||
pelListUnlink(group, nack);
|
||||
raxRemove(group->pel,buf,sizeof(buf),NULL);
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
if (nack->consumer)
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
streamDestroyNACK(kv->ptr, nack, buf);
|
||||
acknowledged++;
|
||||
server.dirty++;
|
||||
|
|
@ -3787,6 +3835,161 @@ cleanup:
|
|||
if (ids != static_ids) zfree(ids);
|
||||
}
|
||||
|
||||
/* XNACK key group <SILENT|FAIL|FATAL> IDS numids id [id ...]
|
||||
* [RETRYCOUNT count] [FORCE]
|
||||
*
|
||||
* Release pending messages back to the group's PEL without acknowledging them.
|
||||
* Entries are disassociated from their consumer (consumer = NULL) and
|
||||
* repositioned to the head of the PEL time-ordered list (delivery_time = 0),
|
||||
* making them immediately claimable by other consumers.
|
||||
*
|
||||
* Delivery counter behavior (when RETRYCOUNT is not specified):
|
||||
* SILENT: decrement by 1 (undo the delivery increment)
|
||||
* FAIL: no change (already incremented during delivery)
|
||||
* FATAL: set to LLONG_MAX
|
||||
*
|
||||
* RETRYCOUNT count: directly sets delivery_count to the specified value,
|
||||
* overriding the mode-based adjustment.
|
||||
*
|
||||
* FORCE: create new unowned PEL entries (consumer = NULL) for IDs that
|
||||
* are not already in the group PEL. When FORCE creates an entry, the
|
||||
* delivery counter is set to 0 (or to RETRYCOUNT if specified, or to
|
||||
* LLONG_MAX if mode is FATAL). */
|
||||
void xnackCommand(client *c) {
|
||||
streamCG *group = NULL;
|
||||
kvobj *kv = lookupKeyWrite(c->db,c->argv[1]);
|
||||
if (kv) {
|
||||
if (checkType(c,kv,OBJ_STREAM)) return;
|
||||
group = streamLookupCG(kv->ptr,c->argv[2]->ptr);
|
||||
}
|
||||
|
||||
if (kv == NULL || group == NULL) {
|
||||
addReplyErrorFormat(c,"-NOGROUP No such key '%s' or "
|
||||
"consumer group '%s'", (char*)c->argv[1]->ptr,
|
||||
(char*)c->argv[2]->ptr);
|
||||
return;
|
||||
}
|
||||
|
||||
int mode;
|
||||
if (!strcasecmp(c->argv[3]->ptr,"SILENT")) {
|
||||
mode = XNACK_SILENT;
|
||||
} else if (!strcasecmp(c->argv[3]->ptr,"FAIL")) {
|
||||
mode = XNACK_FAIL;
|
||||
} else if (!strcasecmp(c->argv[3]->ptr,"FATAL")) {
|
||||
mode = XNACK_FATAL;
|
||||
} else {
|
||||
addReplyError(c,"mode must be SILENT, FAIL, or FATAL");
|
||||
return;
|
||||
}
|
||||
|
||||
int ids_start = 0;
|
||||
int numids = 0;
|
||||
int force = 0;
|
||||
long long retrycount = -1;
|
||||
for (int i = 4; i < c->argc; i++) {
|
||||
int moreargs = (c->argc-1) - i; /* Number of additional arguments. */
|
||||
char *opt = c->argv[i]->ptr;
|
||||
if (!strcasecmp(opt,"IDS") && moreargs) {
|
||||
long numids_long;
|
||||
if (getRangeLongFromObjectOrReply(c,c->argv[i+1],1,INT_MAX,
|
||||
&numids_long,"numids must be a positive integer") != C_OK)
|
||||
return;
|
||||
numids = (int)numids_long;
|
||||
ids_start = i + 2;
|
||||
if (numids > (c->argc - ids_start)) {
|
||||
addReplyError(c,"number of IDs doesn't match numids");
|
||||
return;
|
||||
}
|
||||
i = ids_start + numids - 1;
|
||||
} else if (!strcasecmp(opt,"FORCE")) {
|
||||
force = 1;
|
||||
} else if (!strcasecmp(opt,"RETRYCOUNT") && moreargs) {
|
||||
i++;
|
||||
if (getLongLongFromObjectOrReply(c,c->argv[i],&retrycount,NULL) != C_OK)
|
||||
return;
|
||||
if (retrycount < 0) {
|
||||
addReplyError(c,"Invalid RETRYCOUNT value, must be >= 0");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
addReplyErrorFormat(c,"Unrecognized XNACK option '%s'",
|
||||
(char *)c->argv[i]->ptr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ids_start == 0) {
|
||||
addReplyError(c,"syntax error, expected IDS keyword");
|
||||
return;
|
||||
}
|
||||
|
||||
streamID static_ids[STREAMID_STATIC_VECTOR_LEN];
|
||||
streamID *ids = static_ids;
|
||||
if (numids > STREAMID_STATIC_VECTOR_LEN)
|
||||
ids = zmalloc(sizeof(streamID)*numids);
|
||||
for (int j = 0; j < numids; j++) {
|
||||
if (streamParseStrictIDOrReply(c,c->argv[ids_start+j],&ids[j],0,NULL) != C_OK) goto cleanup;
|
||||
}
|
||||
|
||||
stream *s = kv->ptr;
|
||||
int nacked = 0;
|
||||
size_t old_alloc = server.memory_tracking_enabled ? kvobjAllocSize(kv) : 0;
|
||||
for (int j = 0; j < numids; j++) {
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf,&ids[j]);
|
||||
|
||||
void *result;
|
||||
int found = raxFind(group->pel,buf,sizeof(buf),&result);
|
||||
if (found) {
|
||||
streamNACK *nack = result;
|
||||
nackSetDeliveryCount(nack, mode, retrycount);
|
||||
if (nack->consumer != NULL) {
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
nack->consumer = NULL;
|
||||
}
|
||||
|
||||
/* Move to NACK zone: unlink from current position, insert at
|
||||
* end of NACK zone (head region of PEL). */
|
||||
pelListUnlink(group, nack);
|
||||
pelListInsertNacked(group, nack);
|
||||
} else if (force) {
|
||||
/* FORCE: create new unowned PEL entry only if the stream
|
||||
* entry exists, otherwise skip silently (same as XCLAIM). */
|
||||
if (!streamEntryExists(s, &ids[j]))
|
||||
continue;
|
||||
streamNACK *nack = streamCreateNACK(s, NULL, &ids[j]);
|
||||
|
||||
/* streamCreateNACK() initialises delivery_count to 1 (a real
|
||||
* delivery), but FORCE creates a synthetic entry with no actual
|
||||
* delivery, so reset to 0 before letting nackSetDeliveryCount()
|
||||
* apply the mode/retrycount logic on a clean baseline. */
|
||||
nack->delivery_count = 0;
|
||||
nackSetDeliveryCount(nack, mode, retrycount);
|
||||
|
||||
raxInsert(group->pel, buf, sizeof(buf), nack, NULL);
|
||||
pelListInsertNacked(group, nack);
|
||||
nack->cgroup_ref_node = streamLinkCGroupToEntry(s, group, buf);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
nacked++;
|
||||
}
|
||||
|
||||
if (nacked > 0) {
|
||||
server.dirty += nacked;
|
||||
keyModified(c,c->db,c->argv[1],kv,0);
|
||||
/* XNACK can make entries immediately claimable. */
|
||||
signalKeyAsReady(c->db, c->argv[1], OBJ_STREAM);
|
||||
}
|
||||
if (server.memory_tracking_enabled)
|
||||
updateSlotAllocSize(c->db,getKeySlot(c->argv[1]->ptr),kv,old_alloc,kvobjAllocSize(kv));
|
||||
|
||||
addReplyLongLong(c,nacked);
|
||||
|
||||
cleanup:
|
||||
if (ids != static_ids) zfree(ids);
|
||||
}
|
||||
|
||||
/* Used by xackdelCommand() */
|
||||
typedef enum XAckDelRes {
|
||||
XACKDEL_NO_ID = -1, /* ID not found in PEL. */
|
||||
|
|
@ -3849,7 +4052,8 @@ void xackdelCommand(client *c) {
|
|||
streamNACK *nack = result;
|
||||
pelListUnlink(group, nack);
|
||||
raxRemove(group->pel,buf,sizeof(buf),NULL);
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
if (nack->consumer)
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
streamDestroyNACK(s, nack, buf);
|
||||
server.dirty++;
|
||||
|
||||
|
|
@ -4059,7 +4263,7 @@ void xpendingCommand(client *c) {
|
|||
while(count && raxNext(&ri) && memcmp(ri.key,endkey,ri.key_len) <= 0) {
|
||||
streamNACK *nack = ri.data;
|
||||
|
||||
if (minidle) {
|
||||
if (nack->consumer && minidle) {
|
||||
mstime_t this_idle = now - nack->delivery_time;
|
||||
if (this_idle < minidle) continue;
|
||||
}
|
||||
|
|
@ -4073,13 +4277,22 @@ void xpendingCommand(client *c) {
|
|||
streamDecodeID(ri.key,&id);
|
||||
addReplyStreamID(c,&id);
|
||||
|
||||
/* Consumer name. */
|
||||
addReplyBulkCBuffer(c,nack->consumer->name,
|
||||
sdslen(nack->consumer->name));
|
||||
/* Consumer name (empty string if NACKed / unowned). */
|
||||
if (nack->consumer) {
|
||||
addReplyBulkCBuffer(c,nack->consumer->name,
|
||||
sdslen(nack->consumer->name));
|
||||
} else {
|
||||
addReplyBulkCBuffer(c,"",0);
|
||||
}
|
||||
|
||||
/* Milliseconds elapsed since last delivery. */
|
||||
mstime_t elapsed = now - nack->delivery_time;
|
||||
if (elapsed < 0) elapsed = 0;
|
||||
/* Milliseconds elapsed since last delivery (-1 if unowned / NACKed). */
|
||||
mstime_t elapsed;
|
||||
if (nack->consumer) {
|
||||
elapsed = now - nack->delivery_time;
|
||||
if (elapsed < 0) elapsed = 0;
|
||||
} else {
|
||||
elapsed = -1;
|
||||
}
|
||||
addReplyLongLong(c,elapsed);
|
||||
|
||||
/* Number of deliveries. */
|
||||
|
|
@ -4283,13 +4496,20 @@ void xclaimCommand(client *c) {
|
|||
/* Clear this entry from the PEL, it no longer exists */
|
||||
if (nack != NULL) {
|
||||
/* Propagate this change (we are going to delete the NACK). */
|
||||
streamPropagateXCLAIM(c,c->argv[1],group,c->argv[2],c->argv[j],nack);
|
||||
propagate_last_id = 0; /* Will be propagated by XCLAIM itself. */
|
||||
if (nack->consumer) {
|
||||
streamPropagateXCLAIM(c,c->argv[1],group,c->argv[2],c->argv[j],nack);
|
||||
propagate_last_id = 0; /* Will be propagated by XCLAIM itself. */
|
||||
} else {
|
||||
/* Unowned NACK (NACK zone entry from XNACK) — can't use
|
||||
* XCLAIM propagation without a consumer; use XACK instead. */
|
||||
streamPropagateXACK(c->db->id,c->argv[1],c->argv[2],c->argv[j]);
|
||||
}
|
||||
server.dirty++;
|
||||
/* Release the NACK */
|
||||
pelListUnlink(group, nack);
|
||||
raxRemove(group->pel,buf,sizeof(buf),NULL);
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
if (nack->consumer)
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
streamDestroyNACK(s, nack, buf);
|
||||
}
|
||||
continue;
|
||||
|
|
@ -4336,7 +4556,7 @@ void xclaimCommand(client *c) {
|
|||
if (retrycount >= 0) {
|
||||
nack->delivery_count = retrycount;
|
||||
} else if (!justid) {
|
||||
nack->delivery_count++;
|
||||
nack->delivery_count += nack->delivery_count == LLONG_MAX ? 0 : 1;
|
||||
}
|
||||
if (nack->consumer != consumer) {
|
||||
/* Add the entry in the new consumer local PEL. */
|
||||
|
|
@ -4482,14 +4702,23 @@ void xautoclaimCommand(client *c) {
|
|||
/* Item must exist for us to transfer it to another consumer. */
|
||||
if (!streamEntryExists(s,&id)) {
|
||||
/* Propagate this change (we are going to delete the NACK). */
|
||||
robj *idstr = createObjectFromStreamID(&id);
|
||||
streamPropagateXCLAIM(c,c->argv[1],group,c->argv[2],idstr,nack);
|
||||
decrRefCount(idstr);
|
||||
if (nack->consumer) {
|
||||
robj *idstr = createObjectFromStreamID(&id);
|
||||
streamPropagateXCLAIM(c,c->argv[1],group,c->argv[2],idstr,nack);
|
||||
decrRefCount(idstr);
|
||||
} else {
|
||||
/* Unowned NACK (NACK zone entry from XNACK) — can't use
|
||||
* XCLAIM propagation without a consumer; use XACK instead. */
|
||||
robj *idstr = createObjectFromStreamID(&id);
|
||||
streamPropagateXACK(c->db->id,c->argv[1],c->argv[2],idstr);
|
||||
decrRefCount(idstr);
|
||||
}
|
||||
server.dirty++;
|
||||
/* Clear this entry from the PEL, it no longer exists */
|
||||
pelListUnlink(group, nack);
|
||||
raxRemove(group->pel,ri.key,ri.key_len,NULL);
|
||||
raxRemove(nack->consumer->pel,ri.key,ri.key_len,NULL);
|
||||
if (nack->consumer)
|
||||
raxRemove(nack->consumer->pel,ri.key,ri.key_len,NULL);
|
||||
streamDestroyNACK(s, nack, ri.key);
|
||||
/* Remember the ID for later */
|
||||
deleted_ids[deleted_id_num++] = id;
|
||||
|
|
@ -4498,7 +4727,7 @@ void xautoclaimCommand(client *c) {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (minidle) {
|
||||
if (nack->consumer && minidle) {
|
||||
mstime_t this_idle = now - nack->delivery_time;
|
||||
if (this_idle < minidle)
|
||||
continue;
|
||||
|
|
@ -4518,7 +4747,7 @@ void xautoclaimCommand(client *c) {
|
|||
|
||||
/* Increment the delivery attempts counter unless JUSTID option provided */
|
||||
if (!justid)
|
||||
nack->delivery_count++;
|
||||
nack->delivery_count += nack->delivery_count == LLONG_MAX ? 0 : 1;
|
||||
|
||||
if (nack->consumer != consumer) {
|
||||
/* Add the entry in the new consumer local PEL. */
|
||||
|
|
@ -4809,7 +5038,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 */
|
||||
|
|
@ -4922,7 +5151,7 @@ void xinfoReplyWithStreamInfo(client *c, kvobj *kv) {
|
|||
raxSeek(&ri_cgroups,"^",NULL,0);
|
||||
while(raxNext(&ri_cgroups)) {
|
||||
streamCG *cg = ri_cgroups.data;
|
||||
addReplyMapLen(c,7);
|
||||
addReplyMapLen(c,8);
|
||||
|
||||
/* Name */
|
||||
addReplyBulkCString(c,"name");
|
||||
|
|
@ -4948,6 +5177,10 @@ void xinfoReplyWithStreamInfo(client *c, kvobj *kv) {
|
|||
addReplyBulkCString(c,"pel-count");
|
||||
addReplyLongLong(c,raxSize(cg->pel));
|
||||
|
||||
/* NACKed entries count (entries in the NACK zone) */
|
||||
addReplyBulkCString(c,"nacked-count");
|
||||
addReplyLongLong(c,pelListNackedCount(cg));
|
||||
|
||||
/* Group PEL */
|
||||
addReplyBulkCString(c,"pending");
|
||||
long long arraylen_cg_pel = 0;
|
||||
|
|
@ -4964,10 +5197,13 @@ void xinfoReplyWithStreamInfo(client *c, kvobj *kv) {
|
|||
streamDecodeID(ri_cg_pel.key,&id);
|
||||
addReplyStreamID(c,&id);
|
||||
|
||||
/* Consumer name. */
|
||||
serverAssert(nack->consumer); /* assertion for valgrind (avoid NPD) */
|
||||
addReplyBulkCBuffer(c,nack->consumer->name,
|
||||
sdslen(nack->consumer->name));
|
||||
/* Consumer name (empty string if NACKed / unowned). */
|
||||
if (nack->consumer) {
|
||||
addReplyBulkCBuffer(c,nack->consumer->name,
|
||||
sdslen(nack->consumer->name));
|
||||
} else {
|
||||
addReplyBulkCBuffer(c,"",0);
|
||||
}
|
||||
|
||||
/* Last delivery. */
|
||||
addReplyLongLong(c,nack->delivery_time);
|
||||
|
|
@ -5039,7 +5275,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>
|
||||
|
|
@ -5143,7 +5379,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);
|
||||
}
|
||||
|
|
@ -5340,21 +5576,39 @@ int streamValidateListpackIntegrity(unsigned char *lp, size_t size, int deep) {
|
|||
* O(1) unlink from any position, O(1) append to tail, O(1) access to oldest
|
||||
* entries for CLAIM operations. */
|
||||
|
||||
/* Insert a NACK after 'after' in the time-ordered list.
|
||||
* If after is NULL, insert at the head. */
|
||||
static void pelListInsertAfter(streamCG *cg, streamNACK *after, streamNACK *nack) {
|
||||
if (after) {
|
||||
nack->pel_prev = after;
|
||||
nack->pel_next = after->pel_next;
|
||||
if (after->pel_next)
|
||||
after->pel_next->pel_prev = nack;
|
||||
else
|
||||
cg->pel_time_tail = nack;
|
||||
after->pel_next = nack;
|
||||
} else {
|
||||
nack->pel_prev = NULL;
|
||||
nack->pel_next = cg->pel_time_head;
|
||||
if (cg->pel_time_head)
|
||||
cg->pel_time_head->pel_prev = nack;
|
||||
else
|
||||
cg->pel_time_tail = nack;
|
||||
cg->pel_time_head = nack;
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert a NACK at the tail of the PEL time-ordered list. This is used when
|
||||
* delivery_time is set to current time, which is the common case. */
|
||||
static void pelListInsertAtTail(streamCG *cg, streamNACK *nack) {
|
||||
nack->pel_prev = cg->pel_time_tail;
|
||||
nack->pel_next = NULL;
|
||||
if (cg->pel_time_tail) {
|
||||
cg->pel_time_tail->pel_next = nack;
|
||||
} else {
|
||||
cg->pel_time_head = nack;
|
||||
}
|
||||
cg->pel_time_tail = nack;
|
||||
pelListInsertAfter(cg, cg->pel_time_tail, nack);
|
||||
}
|
||||
|
||||
/* Unlink a NACK from the PEL time-ordered list. */
|
||||
static void pelListUnlink(streamCG *cg, streamNACK *nack) {
|
||||
void pelListUnlink(streamCG *cg, streamNACK *nack) {
|
||||
if (nack == cg->pel_nack_tail) {
|
||||
cg->pel_nack_tail = nack->pel_prev;
|
||||
}
|
||||
if (nack->pel_prev) {
|
||||
nack->pel_prev->pel_next = nack->pel_next;
|
||||
} else {
|
||||
|
|
@ -5373,43 +5627,52 @@ static void pelListUnlink(streamCG *cg, streamNACK *nack) {
|
|||
/* Insert a NACK in sorted order by delivery_time. Used for edge cases where
|
||||
* delivery_time is set to a past time, and also by RDB loading where entries
|
||||
* may not be time-ordered. We scan backwards from the tail since most times
|
||||
* are recent, so the common case is still fast. */
|
||||
* are recent, so the common case is still fast.
|
||||
*
|
||||
* The NACK zone (pel_time_head..pel_nack_tail) is skipped: new entries are
|
||||
* never placed before pel_nack_tail, so the NACK zone stays intact. */
|
||||
void pelListInsertSorted(streamCG *cg, streamNACK *nack) {
|
||||
/* Empty list. */
|
||||
if (cg->pel_time_head == NULL) {
|
||||
cg->pel_time_head = cg->pel_time_tail = nack;
|
||||
nack->pel_prev = nack->pel_next = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Append to tail (common case: delivery_time >= tail time). */
|
||||
if (nack->delivery_time >= cg->pel_time_tail->delivery_time) {
|
||||
/* Empty list or append to tail (common case). */
|
||||
if (cg->pel_time_head == NULL ||
|
||||
nack->delivery_time >= cg->pel_time_tail->delivery_time) {
|
||||
pelListInsertAtTail(cg, nack);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Prepend to head (rare: delivery_time < head time). */
|
||||
if (nack->delivery_time < cg->pel_time_head->delivery_time) {
|
||||
nack->pel_next = cg->pel_time_head;
|
||||
nack->pel_prev = NULL;
|
||||
cg->pel_time_head->pel_prev = nack;
|
||||
cg->pel_time_head = nack;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Insert in middle: scan backwards from tail since most times are recent. */
|
||||
/* Scan backwards from tail, stopping at the NACK-zone boundary
|
||||
* (pel_nack_tail) so we never insert inside the zone. If boundary
|
||||
* is NULL (no NACK zone), the scan may reach the list head. */
|
||||
streamNACK *boundary = cg->pel_nack_tail;
|
||||
streamNACK *curr = cg->pel_time_tail;
|
||||
while (curr && curr->delivery_time > nack->delivery_time) {
|
||||
while (curr != boundary && curr->delivery_time > nack->delivery_time) {
|
||||
curr = curr->pel_prev;
|
||||
}
|
||||
|
||||
/* Insert after curr. */
|
||||
nack->pel_next = curr->pel_next;
|
||||
nack->pel_prev = curr;
|
||||
if (curr->pel_next) {
|
||||
curr->pel_next->pel_prev = nack;
|
||||
pelListInsertAfter(cg, curr, nack);
|
||||
}
|
||||
|
||||
/* Insert a NACKed entry at the end of the NACK zone (head region of the PEL
|
||||
* time-ordered list). The NACK zone occupies positions from pel_time_head to
|
||||
* pel_nack_tail. This is O(1) and maintains FIFO order among NACKed entries. */
|
||||
void pelListInsertNacked(streamCG *cg, streamNACK *nack) {
|
||||
nack->delivery_time = 0;
|
||||
pelListInsertAfter(cg, cg->pel_nack_tail, nack);
|
||||
cg->pel_nack_tail = nack;
|
||||
}
|
||||
|
||||
/* Return the number of entries in the NACK zone (pel_time_head..pel_nack_tail).
|
||||
* Returns 0 when no NACKed entries exist. */
|
||||
uint64_t pelListNackedCount(streamCG *cg) {
|
||||
uint64_t count = 0;
|
||||
if (cg->pel_nack_tail) {
|
||||
streamNACK *nack = cg->pel_time_head;
|
||||
while (nack) {
|
||||
count++;
|
||||
if (nack == cg->pel_nack_tail) break;
|
||||
nack = nack->pel_next;
|
||||
}
|
||||
}
|
||||
curr->pel_next = nack;
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Update a NACK's delivery_time and reposition it in the time-ordered list. */
|
||||
|
|
@ -5720,7 +5983,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 &&
|
||||
|
|
|
|||
163
src/vector.c
Normal file
163
src/vector.c
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/* 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;
|
||||
}
|
||||
|
||||
/* 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];
|
||||
}
|
||||
|
||||
/* 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 (unlikely(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
|
||||
93
src/vector.h
Normal file
93
src/vector.h
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#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;
|
||||
|
||||
/* Return the contiguous backing array. */
|
||||
#define vecData(v) ((v)->data)
|
||||
|
||||
/* Return the number of elements in the vector. */
|
||||
#define vecSize(v) ((v)->size)
|
||||
|
||||
/* 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);
|
||||
|
||||
/* Requires index < vecSize(v). */
|
||||
void *vecGet(const vec *v, size_t index);
|
||||
|
||||
/* 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]
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ proc generate_types {} {
|
|||
# create other non-collection types
|
||||
r incr int
|
||||
r set string str
|
||||
r gcra gcra 10 5 60000
|
||||
|
||||
# create bigger objects with 10 items (more than a single ziplist / listpack)
|
||||
generate_collections big 10
|
||||
|
|
|
|||
|
|
@ -989,6 +989,31 @@ test {corrupt payload: fuzzer findings - vector sets with wrong encoding} {
|
|||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: fuzzer findings - decrRefCount on NULL robj on corrupt KEY_META payload} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
r config set sanitize-dump-payload no
|
||||
r debug set-skip-checksum-validation 1
|
||||
catch {r restore key 0 "\xF3\x02\x01\x0D\x00\x54\x23\x3F\xC9\x82\x32\x05\x8D" replace} err
|
||||
assert_match "*Bad data format*" $err
|
||||
r ping
|
||||
}
|
||||
}
|
||||
|
||||
test {corrupt payload: stream with NACK shared between two consumers} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no]] {
|
||||
r debug set-skip-checksum-validation 1
|
||||
# Payload: stream with entry 1-0, one consumer group (mygroup),
|
||||
# two consumers whose PELs both reference 1-0 (shared NACK).
|
||||
# XACK on one consumer frees the NACK, leaving a dangling
|
||||
# pointer in the other consumer's PEL (use-after-free).
|
||||
catch {r RESTORE mystream 0 "\x1a\x01\x10\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x1d\x00\x00\x00\x0a\x00\x01\x01\x00\x01\x01\x01\x81\x6b\x02\x00\x01\x02\x01\x00\x01\x00\x01\x81\x76\x02\x04\x01\xff\x01\x01\x00\x01\x00\x00\x00\x01\x01\x07\x6d\x79\x67\x72\x6f\x75\x70\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x64\x42\xb9\x9d\x01\x00\x00\x01\x02\x09\x63\x6f\x6e\x73\x75\x6d\x65\x72\x41\x01\x64\x42\xb9\x9d\x01\x00\x00\x01\x64\x42\xb9\x9d\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x09\x63\x6f\x6e\x73\x75\x6d\x65\x72\x42\x01\x64\x42\xb9\x9d\x01\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x40\x64\x40\x64\x00\x00\x00\x0d\x00\xe7\x12\xf7\xcc\x25\xd5\x0e\x44"} err
|
||||
catch {r XACK mystream mygroup 1-0} _
|
||||
catch {r XREADGROUP GROUP mygroup consumerA COUNT 10 STREAMS mystream 0} _
|
||||
catch {r DEL mystream} _
|
||||
assert_match "*Bad data format*" $err
|
||||
r ping
|
||||
}
|
||||
}
|
||||
|
||||
} ;# tags
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/* Test module: SetKeyMeta during keyspace notification callback.
|
||||
/* Test module for KSN paths that must tolerate keymeta writes.
|
||||
*
|
||||
* This module registers keyspace notification callbacks for HASH, STRING,
|
||||
* GENERIC, EXPIRED, and EVICTED events that write to key metadata (via
|
||||
* RedisModule_SetKeyMeta). It is used to verify that commands remain safe
|
||||
* when a notification callback modifies key metadata, which may trigger
|
||||
* kvobj reallocation.
|
||||
* In general, keyspace notification callbacks must not perform write
|
||||
* operations. However, Search module modifies key metadata as part of KSN, so
|
||||
* this module exercises the subset of KSN flows that must remain resilient to
|
||||
* such keymeta modifications, including cases that may trigger kvobj
|
||||
* reallocation.
|
||||
*
|
||||
* Commands:
|
||||
* KEYMETANOTIFY.GET <key> - Get the metadata value attached to a key
|
||||
|
|
@ -146,3 +146,14 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
|||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int RedisModule_OnUnload(RedisModuleCtx *ctx) {
|
||||
REDISMODULE_NOT_USED(ctx);
|
||||
|
||||
if (meta_class_id >= 0) {
|
||||
RedisModule_ReleaseKeyMetaClass(meta_class_id);
|
||||
meta_class_id = -1;
|
||||
}
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ RedisModuleDict *module_event_log = NULL;
|
|||
/** Counts how many deleted KSN we got on keys with a prefix of "count_dels_" **/
|
||||
static size_t dels = 0;
|
||||
|
||||
/* Subkey notification log */
|
||||
#define SUBKEY_LOG_MAX 256
|
||||
static char subkey_log[SUBKEY_LOG_MAX][512];
|
||||
static int subkey_log_count = 0;
|
||||
|
||||
static int KeySpace_NotificationLoaded(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){
|
||||
REDISMODULE_NOT_USED(ctx);
|
||||
REDISMODULE_NOT_USED(type);
|
||||
|
|
@ -298,6 +303,104 @@ static int cmdGetDels(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
|||
return RedisModule_ReplyWithLongLong(ctx, dels);
|
||||
}
|
||||
|
||||
/* Subkey notification callback */
|
||||
static void KeySpace_NotificationSubkeys(RedisModuleCtx *ctx, int type, const char *event,
|
||||
RedisModuleString *key, RedisModuleString **subkeys, int count) {
|
||||
REDISMODULE_NOT_USED(ctx);
|
||||
REDISMODULE_NOT_USED(type);
|
||||
|
||||
if (subkey_log_count >= SUBKEY_LOG_MAX) return;
|
||||
|
||||
const char *key_str = RedisModule_StringPtrLen(key, NULL);
|
||||
|
||||
/* Format: "<event> <key> <count> <subkey1> <subkey2> ..." or "<event> <key> 0" */
|
||||
char buf[512];
|
||||
int off = snprintf(buf, sizeof(buf), "%s %s %d", event, key_str, count);
|
||||
for (int i = 0; i < count && (size_t)off < sizeof(buf) - 1; i++) {
|
||||
const char *sk = RedisModule_StringPtrLen(subkeys[i], NULL);
|
||||
off += snprintf(buf + off, sizeof(buf) - off, " %s", sk);
|
||||
}
|
||||
snprintf(subkey_log[subkey_log_count], sizeof(subkey_log[0]), "%s", buf);
|
||||
subkey_log_count++;
|
||||
}
|
||||
|
||||
/* keyspace.get_subkey_events — return all logged subkey events as an array */
|
||||
static int cmdGetSubkeyEvents(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
RedisModule_ReplyWithArray(ctx, subkey_log_count);
|
||||
for (int i = 0; i < subkey_log_count; i++) {
|
||||
RedisModule_ReplyWithCString(ctx, subkey_log[i]);
|
||||
}
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* keyspace.reset_subkey_events — clear the log */
|
||||
static int cmdResetSubkeyEvents(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
subkey_log_count = 0;
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
}
|
||||
|
||||
/* keyspace.notify_with_subkeys <key> <subkey1> [subkey2 ...] — trigger a module subkey notification */
|
||||
static int cmdNotifyWithSubkeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
if (argc < 3) return RedisModule_WrongArity(ctx);
|
||||
|
||||
RedisModuleString *key = argv[1];
|
||||
RedisModuleString **subkeys = &argv[2];
|
||||
int count = argc - 2;
|
||||
|
||||
RedisModule_NotifyKeyspaceEventWithSubkeys(ctx, REDISMODULE_NOTIFY_HASH, "module_subkey_event", key, subkeys, count);
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
}
|
||||
|
||||
/* keyspace.subscribe_subkeys — subscribe with NONE flag (all events) */
|
||||
static int cmdSubscribeSubkeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
if (RedisModule_SubscribeToKeyspaceEventsWithSubkeys(ctx, REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_GENERIC,
|
||||
REDISMODULE_NOTIFY_FLAG_NONE, KeySpace_NotificationSubkeys) != REDISMODULE_OK) {
|
||||
return RedisModule_ReplyWithError(ctx, "ERR subscribe failed");
|
||||
}
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
}
|
||||
|
||||
/* keyspace.unsubscribe_subkeys — unsubscribe the subkey callback */
|
||||
static int cmdUnsubscribeSubkeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
if (RedisModule_UnsubscribeFromKeyspaceEventsWithSubkeys(ctx, REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_GENERIC,
|
||||
REDISMODULE_NOTIFY_FLAG_NONE, KeySpace_NotificationSubkeys) != REDISMODULE_OK) {
|
||||
return RedisModule_ReplyWithError(ctx, "ERR unsubscribe failed");
|
||||
}
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
}
|
||||
|
||||
/* keyspace.subscribe_require_subkeys — subscribe with SUBKEYS_REQUIRED flag */
|
||||
static int cmdSubscribeRequireSubkeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
if (RedisModule_SubscribeToKeyspaceEventsWithSubkeys(ctx, REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_GENERIC,
|
||||
REDISMODULE_NOTIFY_FLAG_SUBKEYS_REQUIRED,
|
||||
KeySpace_NotificationSubkeys) != REDISMODULE_OK) {
|
||||
return RedisModule_ReplyWithError(ctx, "ERR subscribe failed");
|
||||
}
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
}
|
||||
|
||||
/* keyspace.unsubscribe_require_subkeys — unsubscribe the SUBKEYS_REQUIRED callback */
|
||||
static int cmdUnsubscribeRequireSubkeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
if (RedisModule_UnsubscribeFromKeyspaceEventsWithSubkeys(ctx, REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_GENERIC,
|
||||
REDISMODULE_NOTIFY_FLAG_SUBKEYS_REQUIRED,
|
||||
KeySpace_NotificationSubkeys) != REDISMODULE_OK) {
|
||||
return RedisModule_ReplyWithError(ctx, "ERR unsubscribe failed");
|
||||
}
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
}
|
||||
|
||||
static RedisModuleNotificationFunc get_callback_for_event(int event_mask) {
|
||||
switch(event_mask) {
|
||||
case REDISMODULE_NOTIFY_LOADED:
|
||||
|
|
@ -442,6 +545,34 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
|||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (RedisModule_CreateCommand(ctx, "keyspace.subscribe_subkeys", cmdSubscribeSubkeys, "", 0, 0, 0) == REDISMODULE_ERR) {
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (RedisModule_CreateCommand(ctx, "keyspace.unsubscribe_subkeys", cmdUnsubscribeSubkeys, "", 0, 0, 0) == REDISMODULE_ERR) {
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (RedisModule_CreateCommand(ctx, "keyspace.get_subkey_events", cmdGetSubkeyEvents, "readonly", 0, 0, 0) == REDISMODULE_ERR) {
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (RedisModule_CreateCommand(ctx, "keyspace.reset_subkey_events", cmdResetSubkeyEvents, "", 0, 0, 0) == REDISMODULE_ERR) {
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (RedisModule_CreateCommand(ctx, "keyspace.notify_with_subkeys", cmdNotifyWithSubkeys, "write", 0, 0, 0) == REDISMODULE_ERR) {
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (RedisModule_CreateCommand(ctx, "keyspace.subscribe_require_subkeys", cmdSubscribeRequireSubkeys, "", 0, 0, 0) == REDISMODULE_ERR) {
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (RedisModule_CreateCommand(ctx, "keyspace.unsubscribe_require_subkeys", cmdUnsubscribeRequireSubkeys, "", 0, 0, 0) == REDISMODULE_ERR) {
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
if (argc == 1) {
|
||||
const char *ptr = RedisModule_StringPtrLen(argv[0], NULL);
|
||||
if (!strcasecmp(ptr, "noload")) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -796,9 +798,10 @@ proc generate_fuzzy_traffic_on_key {key type duration} {
|
|||
set zset_commands {ZADD ZCARD ZCOUNT ZINCRBY ZINTERSTORE ZLEXCOUNT ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYLEX ZRANGEBYSCORE ZRANK ZREM ZREMRANGEBYLEX ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREVRANGE ZREVRANGEBYLEX ZREVRANGEBYSCORE ZREVRANK ZSCAN ZSCORE ZUNIONSTORE ZRANDMEMBER}
|
||||
set list_commands {LINDEX LINSERT LLEN LPOP LPOS LPUSH LPUSHX LRANGE LREM LSET LTRIM RPOP RPOPLPUSH RPUSH RPUSHX}
|
||||
set set_commands {SADD SCARD SDIFF SDIFFSTORE SINTER SINTERSTORE SISMEMBER SMEMBERS SMOVE SPOP SRANDMEMBER SREM SSCAN SUNION SUNIONSTORE}
|
||||
set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM XDELEX XACKDEL}
|
||||
set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM XDELEX XACKDEL XNACK}
|
||||
set vset_commands {VADD VREM}
|
||||
set commands [dict create string $string_commands hash $hash_commands zset $zset_commands list $list_commands set $set_commands stream $stream_commands vectorset $vset_commands]
|
||||
set gcra_commands {GCRA}
|
||||
set commands [dict create string $string_commands hash $hash_commands zset $zset_commands list $list_commands set $set_commands stream $stream_commands vectorset $vset_commands gcra $gcra_commands]
|
||||
|
||||
set cmds [dict get $commands $type]
|
||||
set start_time [clock seconds]
|
||||
|
|
|
|||
|
|
@ -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 "" ""]
|
||||
|
|
|
|||
|
|
@ -227,6 +227,111 @@ start_server {tags {"gcra" "external:skip"}} {
|
|||
catch {r gcra mykey 1 1 2147483647 TOKENS 2147483647} err
|
||||
assert_match "*would cause an overflow*" $err
|
||||
}
|
||||
|
||||
test {GCRASETVALUE - basic functionality} {
|
||||
r del mykey
|
||||
set tat_us [expr {[clock microseconds] + 60000000}]
|
||||
assert_equal {OK} [r gcrasetvalue mykey $tat_us]
|
||||
assert_equal {gcra} [r type mykey]
|
||||
assert {[r pttl mykey] > 0}
|
||||
}
|
||||
}
|
||||
|
||||
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"}} {
|
||||
|
|
@ -240,27 +345,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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,14 @@ start_server {tags {"geo"}} {
|
|||
set err
|
||||
} {*valid*}
|
||||
|
||||
test {GEOADD out-of-range longitude/latitude error reply is well-formed} {
|
||||
r readraw 1
|
||||
set reply [r geoadd nyc 200 40 "bad lon"]
|
||||
r readraw 0
|
||||
# RESP simple error: single line starting with '-', no duplicated "-ERR" prefix.
|
||||
assert_match {-ERR invalid longitude,latitude pair*} $reply
|
||||
}
|
||||
|
||||
test {GEOADD multi add} {
|
||||
r geoadd nyc -73.9733487 40.7648057 "central park n/q/r" -73.9903085 40.7362513 "union square" -74.0131604 40.7126674 "wtc one" -73.7858139 40.6428986 "jfk" -73.9375699 40.7498929 "q4" -73.9564142 40.7480973 4545
|
||||
} {6}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ proc flushallAndVerifyCleanup {} {
|
|||
|
||||
start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
|
||||
r module load $testmodule
|
||||
r debug enable-keymeta-runtime-registration 1
|
||||
|
||||
array set classesSpec {}
|
||||
set classesSpec(1) "KEEPONCOPY:KEEPONRENAME:KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
|
||||
|
|
@ -763,6 +764,7 @@ test "RDB: Load with different module registration order preserves metadata corr
|
|||
# metadata values should still be correctly associated with their classes.
|
||||
start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
|
||||
r module load $testmodule
|
||||
r debug enable-keymeta-runtime-registration 1
|
||||
|
||||
# Helper function to generate class names (needed in inner scope)
|
||||
proc cname {id} { return "CLS$id" }
|
||||
|
|
@ -805,6 +807,7 @@ test "RDB: Load with different module registration order preserves metadata corr
|
|||
# INNER SERVER: Start new server, register classes in DIFFERENT order, then load RDB
|
||||
start_server [list overrides [list dir $rdb_dir enable-debug-command yes]] {
|
||||
r module load $testmodule
|
||||
r debug enable-keymeta-runtime-registration 1
|
||||
|
||||
# Helper function to generate class names (needed in inner scope)
|
||||
proc cname {id} { return "CLS$id" }
|
||||
|
|
@ -866,6 +869,7 @@ test "RDB: File size same with/without metadata when no rdb_save callback" {
|
|||
|
||||
start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
|
||||
r module load $testmodule
|
||||
r debug enable-keymeta-runtime-registration 1
|
||||
|
||||
# Get RDB directory
|
||||
set rdb_dir [lindex [r config get dir] 1]
|
||||
|
|
@ -900,11 +904,11 @@ test "RDB: File size same with/without metadata when no rdb_save callback" {
|
|||
} {} {external:skip needs:save}
|
||||
|
||||
test "Creating key metadata not during OnLoad should fail" {
|
||||
# This time start_server without "enable-debug-command yes"
|
||||
# Start server without enabling keymeta runtime registration debug flag
|
||||
start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command no}} {
|
||||
r module load $testmodule
|
||||
# Creating a class not during OnLoad should fail
|
||||
# Creating a class not during server startup should fail
|
||||
catch {r keymeta.register [cname 1] 1 "ALLOWIGNORE"} err
|
||||
assert_match {*failed to create metadata class*} $err
|
||||
assert_match {*failed to create metadata class*} $err
|
||||
}
|
||||
} {} {external:skip needs:save}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,139 @@ tags "modules external:skip" {
|
|||
assert_equal [r get testkeyspace:expired] 1
|
||||
}
|
||||
|
||||
test "Subkey notification: subscribe starts callback" {
|
||||
r keyspace.subscribe_subkeys
|
||||
r keyspace.reset_subkey_events
|
||||
r config set notify-keyspace-events ""
|
||||
}
|
||||
|
||||
test "Subkey notification: HSET triggers module subkey callback" {
|
||||
r keyspace.reset_subkey_events
|
||||
r hset myhash f1 v1 f2 v2
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 1 [llength $events]
|
||||
assert_equal "hset myhash 2 f1 f2" [lindex $events 0]
|
||||
r del myhash
|
||||
}
|
||||
|
||||
test "Subkey notification: HDEL triggers module subkey callback" {
|
||||
r hset myhash f1 v1 f2 v2
|
||||
r keyspace.reset_subkey_events
|
||||
r hdel myhash f1
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 1 [llength $events]
|
||||
assert_equal "hdel myhash 1 f1" [lindex $events 0]
|
||||
r del myhash
|
||||
}
|
||||
|
||||
test "Subkey notification: non-subkey event calls subkey callback with count=0" {
|
||||
r hset myhash f1 v1
|
||||
r keyspace.reset_subkey_events
|
||||
r del myhash
|
||||
set events [r keyspace.get_subkey_events]
|
||||
# DEL is NOTIFY_GENERIC — our callback is registered for
|
||||
# HASH|GENERIC, so it should be called with subkeys=NULL, count=0.
|
||||
assert_equal 1 [llength $events]
|
||||
assert_equal "del myhash 0" [lindex $events 0]
|
||||
}
|
||||
|
||||
test "Subkey notification: module-triggered NotifyKeyspaceEventWithSubkeys" {
|
||||
r keyspace.reset_subkey_events
|
||||
r keyspace.notify_with_subkeys mykey sk1 sk2 sk3
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 1 [llength $events]
|
||||
assert_equal "module_subkey_event mykey 3 sk1 sk2 sk3" [lindex $events 0]
|
||||
}
|
||||
|
||||
test "Subkey notification: lazy hash field expiry triggers hexpired with subkeys" {
|
||||
r debug set-active-expire 0
|
||||
r del myhash
|
||||
r hset myhash f1 v1 f2 v2 f3 v3
|
||||
r hpexpire myhash 10 FIELDS 2 f1 f2
|
||||
r keyspace.reset_subkey_events
|
||||
after 100
|
||||
r hmget myhash f1 f2
|
||||
assert_equal "hexpired myhash 2 f1 f2" [lindex [r keyspace.get_subkey_events] 0]
|
||||
r debug set-active-expire 1
|
||||
} {OK} {needs:debug}
|
||||
|
||||
test "Subkey notification: active hash field expiry triggers hexpired with subkeys" {
|
||||
r del myhash
|
||||
r hset myhash f1 v1 f2 v2
|
||||
r keyspace.reset_subkey_events
|
||||
r hpexpire myhash 10 FIELDS 2 f1 f2
|
||||
# wait for active expiry to kick in
|
||||
wait_for_condition 50 100 {
|
||||
[r exists myhash] == 0
|
||||
} else {
|
||||
fail "Fields not expired by active expiry"
|
||||
}
|
||||
# fields order is undefined
|
||||
assert_match "hexpired myhash 2 f* f*" [lindex [r keyspace.get_subkey_events] 1]
|
||||
r del myhash
|
||||
}
|
||||
|
||||
test "Subkey notification: unsubscribe stops callback and resubscribe resumes" {
|
||||
r keyspace.reset_subkey_events
|
||||
r hset myhash f1 v1
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 1 [llength $events]
|
||||
|
||||
# Unsubscribe — events should stop
|
||||
r keyspace.unsubscribe_subkeys
|
||||
r keyspace.reset_subkey_events
|
||||
r hset myhash f2 v2
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 0 [llength $events]
|
||||
# active expire should not trigger subkey callback
|
||||
r hpexpire myhash 10 FIELDS 2 f1 f2
|
||||
wait_for_condition 50 100 {
|
||||
[r exists myhash] == 0
|
||||
} else {
|
||||
fail "Fields not expired by active expiry"
|
||||
}
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 0 [llength $events]
|
||||
|
||||
# Re-subscribe — events should resume
|
||||
r keyspace.subscribe_subkeys
|
||||
r del myhash
|
||||
r hset myhash f1 v1 f2 v2
|
||||
r keyspace.reset_subkey_events
|
||||
r hpexpire myhash 10 FIELDS 2 f1 f2
|
||||
assert_match "hexpire myhash 2 f* f*" [lindex [r keyspace.get_subkey_events] 0]
|
||||
# active expire should also resume subkey callback
|
||||
wait_for_condition 50 100 {
|
||||
[r exists myhash] == 0
|
||||
} else {
|
||||
fail "Fields not expired by active expiry"
|
||||
}
|
||||
assert_match "hexpired myhash 2 f* f*" [lindex [r keyspace.get_subkey_events] 1]
|
||||
|
||||
r keyspace.unsubscribe_subkeys
|
||||
r keyspace.reset_subkey_events
|
||||
r del myhash
|
||||
}
|
||||
|
||||
test "Subkey notification: SUBKEYS_REQUIRED flag skips events without subkeys" {
|
||||
r keyspace.subscribe_require_subkeys
|
||||
r keyspace.reset_subkey_events
|
||||
|
||||
# HSET has subkeys — should trigger callback
|
||||
r hset myhash f1 v1 f2 v2
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 1 [llength $events]
|
||||
assert_equal "hset myhash 2 f1 f2" [lindex $events 0]
|
||||
|
||||
# DEL has no subkeys — the callback should be skipped.
|
||||
r keyspace.reset_subkey_events
|
||||
r del myhash
|
||||
set events [r keyspace.get_subkey_events]
|
||||
assert_equal 0 [llength $events]
|
||||
|
||||
r keyspace.unsubscribe_require_subkeys
|
||||
}
|
||||
|
||||
test "Unload the module - testkeyspace" {
|
||||
assert_equal {OK} [r module unload testkeyspace]
|
||||
}
|
||||
|
|
@ -125,6 +258,38 @@ tags "modules external:skip" {
|
|||
}
|
||||
}
|
||||
|
||||
# Replication test: replica module receives subkey notifications
|
||||
start_server [list overrides [list loadmodule "$testmodule"]] {
|
||||
set master [srv 0 client]
|
||||
set master_host [srv 0 host]
|
||||
set master_port [srv 0 port]
|
||||
|
||||
start_server [list overrides [list loadmodule "$testmodule"]] {
|
||||
set replica [srv 0 client]
|
||||
|
||||
$replica replicaof $master_host $master_port
|
||||
wait_for_sync $replica
|
||||
|
||||
test "Subkey notification: replica module receives subkey callback after replication" {
|
||||
$master keyspace.subscribe_subkeys
|
||||
$replica keyspace.subscribe_subkeys
|
||||
$replica keyspace.reset_subkey_events
|
||||
|
||||
$master hset myhash f1 v1 f2 v2
|
||||
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
set events [$replica keyspace.get_subkey_events]
|
||||
assert_equal 1 [llength $events]
|
||||
assert_equal "hset myhash 2 f1 f2" [lindex $events 0]
|
||||
|
||||
$master del myhash
|
||||
$master keyspace.unsubscribe_subkeys
|
||||
$replica keyspace.unsubscribe_subkeys
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
test {OnLoad failure will handle un-registration} {
|
||||
catch {r module load $testmodule noload}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
# Test for SetKeyMeta during keyspace notification (KSN) callbacks.
|
||||
#
|
||||
# This test loads a module that registers KSN callbacks for HASH, STRING,
|
||||
# GENERIC, EXPIRED, and EVICTED events. The callback writes to key metadata
|
||||
# (via RedisModule_SetKeyMeta), which may trigger kvobj reallocation.
|
||||
# It exercises various commands across these notification types to catch
|
||||
# regressions where the kvobj pointer becomes stale after a notification
|
||||
# callback reallocates it.
|
||||
#
|
||||
# Important: each test uses a fresh key so that SetKeyMeta triggers an actual
|
||||
# kvobj reallocation (the first metadata attachment grows the kvobj). We verify
|
||||
# this by checking that the setcount increases after each command.
|
||||
# Each test also validates that the metadata is properly accessible after the
|
||||
# operation by reading it back via RedisModule_GetKeyMeta.
|
||||
# On key space notification, the module shouldn't modify the key. This focused
|
||||
# regression tests makes an exception for RediSearch which uses SetKeyMeta
|
||||
# as part of its KSN callback (Currently only for hash keys without hash field
|
||||
# expiration). The test module mutates key metadata during selected notifications,
|
||||
# which may reallocate the underlying kvobj and invalidates any local pointer to
|
||||
# it. Each test uses fresh keys when possible so the first metadata write forces
|
||||
# the reallocation-sensitive path, then verifies the command still completes.
|
||||
|
||||
set testmodule [file normalize tests/modules/keymeta_notify.so]
|
||||
|
||||
start_server {tags {"modules" "external:skip"}} {
|
||||
start_server {tags {"modules" "external:skip"} overrides {enable-debug-command yes}} {
|
||||
r debug enable-keymeta-runtime-registration 1
|
||||
r module load $testmodule
|
||||
|
||||
# --- HASH notification tests ---
|
||||
|
|
@ -91,6 +87,107 @@ start_server {tags {"modules" "external:skip"}} {
|
|||
assert {[r keymetanotify.setcount] >= $before + 100}
|
||||
}
|
||||
|
||||
test {HGETDEL with SetKeyMeta in notification does not crash} {
|
||||
# To test the "first SetKeyMeta causes kvobj reallocation" scenario,
|
||||
# create the key BEFORE loading the module so the first metadata
|
||||
# attachment happens during HGETDEL, not during HSET.
|
||||
r module unload keymetanotify
|
||||
r HSET hgetdel_key f1 v1 f2 v2 f3 v3
|
||||
r module load $testmodule
|
||||
|
||||
# HGETDEL returns the value and deletes the field
|
||||
# This is the first SetKeyMeta call for this key, triggering kvobj reallocation
|
||||
set before [r keymetanotify.setcount]
|
||||
set result [r HGETDEL hgetdel_key FIELDS 1 f1]
|
||||
assert_equal $result "v1"
|
||||
assert_equal [r HEXISTS hgetdel_key f1] 0
|
||||
assert_equal [r HLEN hgetdel_key] 2
|
||||
# SetKeyMeta should be called during the hdel notification
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
assert_equal [r keymetanotify.get hgetdel_key] "notified"
|
||||
|
||||
# HGETDEL multiple fields
|
||||
set result [r HGETDEL hgetdel_key FIELDS 2 f2 f3]
|
||||
assert_equal [lindex $result 0] "v2"
|
||||
assert_equal [lindex $result 1] "v3"
|
||||
assert_equal [r HLEN hgetdel_key] 0
|
||||
}
|
||||
|
||||
test {HDEL with SetKeyMeta in notification does not crash} {
|
||||
# To test the "first SetKeyMeta causes kvobj reallocation" scenario,
|
||||
# create the key BEFORE loading the module so the first metadata
|
||||
# attachment happens during HDEL, not during HSET.
|
||||
r module unload keymetanotify
|
||||
r HSET hdel_key f1 v1 f2 v2 f3 v3
|
||||
r module load $testmodule
|
||||
|
||||
# HDEL single field - this is the first SetKeyMeta call for this key,
|
||||
# triggering kvobj reallocation during the hdel notification
|
||||
set before [r keymetanotify.setcount]
|
||||
r HDEL hdel_key f1
|
||||
assert_equal [r HEXISTS hdel_key f1] 0
|
||||
assert_equal [r HLEN hdel_key] 2
|
||||
# SetKeyMeta should be called during the hdel notification
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
assert_equal [r keymetanotify.get hdel_key] "notified"
|
||||
|
||||
# HDEL multiple fields (in-place metadata update)
|
||||
r HDEL hdel_key f2 f3
|
||||
assert_equal [r HLEN hdel_key] 0
|
||||
}
|
||||
|
||||
# --- GENERIC notification tests ---
|
||||
|
||||
test {PERSIST with SetKeyMeta in notification does not crash} {
|
||||
# Create key with expiration
|
||||
set before [r keymetanotify.setcount]
|
||||
r SET persist_key "value"
|
||||
r EXPIRE persist_key 1000
|
||||
assert_equal [r keymetanotify.get persist_key] "notified"
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
|
||||
# Verify TTL is set
|
||||
assert {[r TTL persist_key] > 0}
|
||||
|
||||
# PERSIST removes expiration
|
||||
set before [r keymetanotify.setcount]
|
||||
r PERSIST persist_key
|
||||
# persist notification triggers SetKeyMeta
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
|
||||
# Verify TTL is removed
|
||||
assert_equal [r TTL persist_key] -1
|
||||
assert_equal [r GET persist_key] "value"
|
||||
}
|
||||
|
||||
test {COPY with SetKeyMeta in notification does not crash} {
|
||||
# Create source key
|
||||
set before [r keymetanotify.setcount]
|
||||
r HSET copy_src_key f1 v1 f2 v2
|
||||
assert_equal [r keymetanotify.get copy_src_key] "notified"
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
|
||||
# COPY to new key
|
||||
set before [r keymetanotify.setcount]
|
||||
r COPY copy_src_key copy_dst_key
|
||||
# copy_to notification triggers SetKeyMeta on destination
|
||||
assert_equal [r keymetanotify.get copy_dst_key] "notified"
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
|
||||
# Verify both keys have same content
|
||||
assert_equal [r HGET copy_src_key f1] "v1"
|
||||
assert_equal [r HGET copy_dst_key f1] "v1"
|
||||
assert_equal [r HGET copy_src_key f2] "v2"
|
||||
assert_equal [r HGET copy_dst_key f2] "v2"
|
||||
|
||||
# COPY with REPLACE
|
||||
r HSET copy_src_key f3 v3
|
||||
set before [r keymetanotify.setcount]
|
||||
r COPY copy_src_key copy_dst_key REPLACE
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
assert_equal [r HGET copy_dst_key f3] "v3"
|
||||
}
|
||||
|
||||
# --- STRING notification tests ---
|
||||
# Each test uses a fresh key for actual kvobj reallocation.
|
||||
|
||||
|
|
@ -164,6 +261,37 @@ start_server {tags {"modules" "external:skip"}} {
|
|||
assert_equal [r EXISTS del_key] 0
|
||||
}
|
||||
|
||||
test {DELEX with SetKeyMeta in notification does not crash} {
|
||||
r SET delex_key "value"
|
||||
assert_equal [r keymetanotify.get delex_key] "notified"
|
||||
r DELEX delex_key IFEQ value
|
||||
assert_equal [r EXISTS delex_key] 0
|
||||
}
|
||||
|
||||
test {MOVE with SetKeyMeta in notification does not crash} {
|
||||
r select 10
|
||||
r DEL move_key
|
||||
r select 9
|
||||
|
||||
# Create the key before loading the module so the first metadata
|
||||
# attachment happens during MOVE, not during SET.
|
||||
r module unload keymetanotify
|
||||
r SET move_key "value"
|
||||
r module load $testmodule
|
||||
|
||||
set before [r keymetanotify.setcount]
|
||||
r MOVE move_key 10
|
||||
assert_equal [r EXISTS move_key] 0
|
||||
|
||||
r select 10
|
||||
assert_equal [r GET move_key] "value"
|
||||
assert_equal [r keymetanotify.get move_key] "notified"
|
||||
assert {[r keymetanotify.setcount] > $before}
|
||||
r DEL move_key
|
||||
r select 9
|
||||
set _ {}
|
||||
} {} {singledb:skip}
|
||||
|
||||
test {RENAME with SetKeyMeta in notification does not crash} {
|
||||
r SET rename_src "value"
|
||||
r RENAME rename_src rename_dst
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -602,7 +602,6 @@ start_server {tags {"pubsub network"}} {
|
|||
after 15
|
||||
r hget myhash f2
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
|
||||
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
|
||||
|
||||
# FNX on logically expired field
|
||||
|
|
@ -962,6 +961,364 @@ start_server {tags {"pubsub network"}} {
|
|||
$rd1 close
|
||||
}
|
||||
|
||||
### Subkey-level notification tests for HASH type ###
|
||||
|
||||
# Helper: build expected payload "event|len:field0,len:field1,..."
|
||||
proc build_expected_payload {event prefix count} {
|
||||
set parts {}
|
||||
for {set i 0} {$i < $count} {incr i} {
|
||||
set f "${prefix}${i}"
|
||||
lappend parts "[string length $f]:$f"
|
||||
}
|
||||
return "${event}|[join $parts ,]"
|
||||
}
|
||||
|
||||
# Compare subkey notification payloads as sets (order-insensitive).
|
||||
# Parses "event|f1,f2,..." and checks event matches and fields match as sets.
|
||||
proc assert_subkey_payload_equal {expected actual} {
|
||||
set ep [split $expected "|"]
|
||||
set ap [split $actual "|"]
|
||||
assert_equal [lindex $ep 0] [lindex $ap 0] ;# event name
|
||||
set ef [lsort [split [lindex $ep 1] ","]]
|
||||
set af [lsort [split [lindex $ap 1] ","]]
|
||||
assert_equal $ef $af
|
||||
}
|
||||
|
||||
# Generate N field-value pairs: {f0 v0 f1 v1 ...}
|
||||
proc gen_field_values {prefix n} {
|
||||
set args {}
|
||||
for {set i 0} {$i < $n} {incr i} {
|
||||
lappend args "${prefix}${i}" "v${i}"
|
||||
}
|
||||
return $args
|
||||
}
|
||||
|
||||
# Generate N field names: {f0 f1 ...}
|
||||
proc gen_fields {prefix n} {
|
||||
set fields {}
|
||||
for {set i 0} {$i < $n} {incr i} {
|
||||
lappend fields "${prefix}${i}"
|
||||
}
|
||||
return $fields
|
||||
}
|
||||
|
||||
# Subkey notification: subkeyspace channel
|
||||
foreach {type max_lp_entries} {listpackex 512 hashtable 0} {
|
||||
r config set hash-max-listpack-entries $max_lp_entries
|
||||
r config set notify-keyspace-events Sh
|
||||
set rd1 [redis_deferring_client]
|
||||
assert_equal {1} [subscribe $rd1 "__subkeyspace@${db}__:myhash"]
|
||||
|
||||
test "Subkey notifications: subkeyspace - HSET single field ($type)" {
|
||||
r del myhash
|
||||
r hset myhash f1 v1
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hset|2:f1" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: subkeyspace - HINCRBY ($type)" {
|
||||
r del myhash
|
||||
r hset myhash counter 10
|
||||
r hincrby myhash counter 5
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hset|7:counter" [$rd1 read]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hincrby|7:counter" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: subkeyspace - HSETNX ($type)" {
|
||||
r del myhash
|
||||
r hsetnx myhash newfield val
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hset|8:newfield" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: subkeyspace - HINCRBYFLOAT ($type)" {
|
||||
r del myhash
|
||||
r hset myhash counter 10.5
|
||||
r hincrbyfloat myhash counter 2.5
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hset|7:counter" [$rd1 read]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hincrbyfloat|7:counter" [$rd1 read]
|
||||
}
|
||||
|
||||
# Test with N=3 (stack path, within FIELDS_STACK_SIZE=16) and
|
||||
# N=32 (heap path, exceeds FIELDS_STACK_SIZE).
|
||||
foreach N {3 32} {
|
||||
|
||||
test "Subkey notifications: HSET $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
set expected [build_expected_payload "hset" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HDEL $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
$rd1 read ;# consume hset notification
|
||||
r hdel myhash {*}[gen_fields "f" $N]
|
||||
set expected [build_expected_payload "hdel" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HGETDEL $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
$rd1 read ;# consume hset notification
|
||||
r hgetdel myhash FIELDS $N {*}[gen_fields "f" $N]
|
||||
set expected [build_expected_payload "hdel" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HEXPIRE $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
$rd1 read ;# consume hset notification
|
||||
r hexpire myhash 1000 FIELDS $N {*}[gen_fields "f" $N]
|
||||
set expected [build_expected_payload "hexpire" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HEXPIRE past timestamp $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
$rd1 read ;# consume hset notification
|
||||
r hexpireat myhash 1 FIELDS $N {*}[gen_fields "f" $N]
|
||||
set expected [build_expected_payload "hdel" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HPERSIST $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
set fields [gen_fields "f" $N]
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
r hexpire myhash 1000 FIELDS $N {*}$fields
|
||||
$rd1 read ;# consume hset
|
||||
$rd1 read ;# consume hexpire
|
||||
r hpersist myhash FIELDS $N {*}$fields
|
||||
set expected [build_expected_payload "hpersist" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HGETEX with expire $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
$rd1 read ;# consume hset
|
||||
r hgetex myhash EX 1000 FIELDS $N {*}[gen_fields "f" $N]
|
||||
set expected [build_expected_payload "hexpire" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HGETEX with persist $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
set fields [gen_fields "f" $N]
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
r hexpire myhash 1000 FIELDS $N {*}$fields
|
||||
$rd1 read ;# consume hset
|
||||
$rd1 read ;# consume hexpire
|
||||
r hgetex myhash PERSIST FIELDS $N {*}$fields
|
||||
set expected [build_expected_payload "hpersist" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HGETEX past timestamp $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hset myhash {*}[gen_field_values "f" $N]
|
||||
$rd1 read ;# consume hset
|
||||
r hgetex myhash PX 0 FIELDS $N {*}[gen_fields "f" $N]
|
||||
set expected [build_expected_payload "hdel" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HSETEX $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hsetex myhash EX 1000 FIELDS $N {*}[gen_field_values "f" $N]
|
||||
set expected_hset [build_expected_payload "hset" "f" $N]
|
||||
set expected_hexpire [build_expected_payload "hexpire" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected_hset" [$rd1 read]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected_hexpire" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: HSETEX past timestamp $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
r hsetex myhash PX 0 FIELDS $N {*}[gen_field_values "f" $N]
|
||||
set expected_hset [build_expected_payload "hset" "f" $N]
|
||||
set expected_hdel [build_expected_payload "hdel" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected_hset" [$rd1 read]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected_hdel" [$rd1 read]
|
||||
}
|
||||
|
||||
test "Subkey notifications: lazy field expiry triggers hexpired $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
# Create N+1 fields, expire N of them; keep one to prevent hash deletion.
|
||||
set fields [gen_fields "f" $N]
|
||||
set args [gen_field_values "f" $N]
|
||||
lappend args "keep" "val"
|
||||
r hset myhash {*}$args
|
||||
r debug set-active-expire 0
|
||||
r hpexpire myhash 10 FIELDS $N {*}$fields
|
||||
$rd1 read ;# consume hset
|
||||
$rd1 read ;# consume hexpire
|
||||
# Trigger lazy expiry by reading the fields
|
||||
after 100
|
||||
r hmget myhash {*}$fields
|
||||
set expected_hexpired [build_expected_payload "hexpired" "f" $N]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash $expected_hexpired" [$rd1 read]
|
||||
r debug set-active-expire 1
|
||||
} {OK} {needs:debug}
|
||||
|
||||
test "Subkey notifications: active field expiry triggers hexpired $N fields ($type, [expr {$N <= 16 ? {stack} : {heap}}])" {
|
||||
r del myhash
|
||||
# Create N+1 fields, expire N of them; keep one to prevent hash deletion.
|
||||
set fields [gen_fields "f" $N]
|
||||
set args [gen_field_values "f" $N]
|
||||
lappend args "keep" "val"
|
||||
r hset myhash {*}$args
|
||||
r hpexpire myhash 10 FIELDS $N {*}$fields
|
||||
$rd1 read ;# consume hset
|
||||
$rd1 read ;# consume hexpire
|
||||
# Wait for active expiry; field order depends on hash table iteration,
|
||||
# so compare as set.
|
||||
set expected_hexpired [build_expected_payload "hexpired" "f" $N]
|
||||
set actual [$rd1 read]
|
||||
set prefix "message __subkeyspace@${db}__:myhash "
|
||||
assert_equal $prefix [string range $actual 0 [expr {[string length $prefix]-1}]]
|
||||
assert_subkey_payload_equal $expected_hexpired [string range $actual [string length $prefix] end]
|
||||
}
|
||||
} ;# end foreach N
|
||||
$rd1 close
|
||||
} ;# end foreach type
|
||||
|
||||
# Subkey notification format tests for subkeyevent/subkeyspaceitem/subkeyspaceevent
|
||||
# Full command coverage is done via subkeyspace channel below; here we only verify channel format.
|
||||
foreach {type max_lp_entries} {listpackex 512 hashtable 0} {
|
||||
r config set hash-max-listpack-entries $max_lp_entries
|
||||
|
||||
test "Subkey notifications: subkeyevent format ($type)" {
|
||||
r config set notify-keyspace-events Th
|
||||
r del myhash
|
||||
set rd1 [redis_deferring_client]
|
||||
assert_equal {1} [subscribe $rd1 "__subkeyevent@${db}__:hset"]
|
||||
r hset myhash f1 v1 f2 v2 f3 v3
|
||||
assert_equal "message __subkeyevent@${db}__:hset 6:myhash|2:f1,2:f2,2:f3" [$rd1 read]
|
||||
$rd1 close
|
||||
}
|
||||
|
||||
test "Subkey notifications: subkeyspaceitem format ($type)" {
|
||||
r config set notify-keyspace-events Ih
|
||||
r del myhash
|
||||
set rd1 [redis_deferring_client]
|
||||
$rd1 subscribe "__subkeyspaceitem@${db}__:myhash\nf1"
|
||||
$rd1 read ;# consume subscribe confirmation
|
||||
r hset myhash f1 v1
|
||||
set msg [$rd1 read]
|
||||
assert_equal "message" [lindex $msg 0]
|
||||
assert_equal "__subkeyspaceitem@${db}__:myhash\nf1" [lindex $msg 1]
|
||||
assert_equal "hset" [lindex $msg 2]
|
||||
$rd1 close
|
||||
}
|
||||
|
||||
test "Subkey notifications: subkeyspaceitem per-subkey delivery with psubscribe ($type)" {
|
||||
r config set notify-keyspace-events Ih
|
||||
r del myhash
|
||||
set rd1 [redis_deferring_client]
|
||||
assert_equal {1} [psubscribe $rd1 "__subkeyspaceitem@${db}__:myhash*"]
|
||||
r hset myhash f1 v1 f2 v2
|
||||
# Should get one notification per subkey
|
||||
set msg1 [$rd1 read]
|
||||
set msg2 [$rd1 read]
|
||||
assert_equal "pmessage" [lindex $msg1 0]
|
||||
assert_equal "__subkeyspaceitem@${db}__:myhash\nf1" [lindex $msg1 2]
|
||||
assert_equal "hset" [lindex $msg1 3]
|
||||
assert_equal "pmessage" [lindex $msg2 0]
|
||||
assert_equal "__subkeyspaceitem@${db}__:myhash\nf2" [lindex $msg2 2]
|
||||
assert_equal "hset" [lindex $msg2 3]
|
||||
$rd1 close
|
||||
}
|
||||
|
||||
test "Subkey notifications: subkeyspaceitem skips key with newline ($type)" {
|
||||
r config set notify-keyspace-events Ih
|
||||
r del "key\nwith\nnewline"
|
||||
set rd1 [redis_deferring_client]
|
||||
assert_equal {1} [psubscribe $rd1 "__subkeyspaceitem@${db}__:*"]
|
||||
r hset "key\nwith\nnewline" f1 v1
|
||||
# Normal key to verify notifications still work
|
||||
r hset normalkey f1 v1
|
||||
# Should only get notification for normalkey
|
||||
set msg [$rd1 read]
|
||||
assert_equal "pmessage" [lindex $msg 0]
|
||||
assert_equal "__subkeyspaceitem@${db}__:normalkey\nf1" [lindex $msg 2]
|
||||
assert_equal "hset" [lindex $msg 3]
|
||||
r del "key\nwith\nnewline"
|
||||
r del normalkey
|
||||
$rd1 close
|
||||
}
|
||||
|
||||
test "Subkey notifications: subkeyspaceevent format ($type)" {
|
||||
r config set notify-keyspace-events Vh
|
||||
r del myhash
|
||||
set rd1 [redis_deferring_client]
|
||||
assert_equal {1} [subscribe $rd1 "__subkeyspaceevent@${db}__:hset|myhash"]
|
||||
r hset myhash f1 v1 f2 v2
|
||||
assert_equal "message __subkeyspaceevent@${db}__:hset|myhash 2:f1,2:f2" [$rd1 read]
|
||||
$rd1 close
|
||||
}
|
||||
} ;
|
||||
|
||||
# Test all 4 channels enabled simultaneously
|
||||
test "Subkey notifications: all 4 channels enabled simultaneously" {
|
||||
r config set notify-keyspace-events STIVh
|
||||
r del myhash
|
||||
set rd_s [redis_deferring_client]
|
||||
set rd_t [redis_deferring_client]
|
||||
set rd_i [redis_deferring_client]
|
||||
set rd_v [redis_deferring_client]
|
||||
assert_equal {1} [subscribe $rd_s "__subkeyspace@${db}__:myhash"]
|
||||
assert_equal {1} [subscribe $rd_t "__subkeyevent@${db}__:hset"]
|
||||
assert_equal {1} [subscribe $rd_v "__subkeyspaceevent@${db}__:hset|myhash"]
|
||||
$rd_i subscribe "__subkeyspaceitem@${db}__:myhash\nf1"
|
||||
$rd_i read ;# consume subscribe confirmation
|
||||
r hset myhash f1 v1
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hset|2:f1" [$rd_s read]
|
||||
assert_equal "message __subkeyevent@${db}__:hset 6:myhash|2:f1" [$rd_t read]
|
||||
assert_equal "message __subkeyspaceevent@${db}__:hset|myhash 2:f1" [$rd_v read]
|
||||
set msg_i [$rd_i read]
|
||||
assert_equal "message" [lindex $msg_i 0]
|
||||
assert_equal "__subkeyspaceitem@${db}__:myhash\nf1" [lindex $msg_i 1]
|
||||
assert_equal "hset" [lindex $msg_i 2]
|
||||
$rd_s close
|
||||
$rd_t close
|
||||
$rd_i close
|
||||
$rd_v close
|
||||
}
|
||||
|
||||
# Test that subkey notifications are triggered on replica after replication
|
||||
test "Subkey notifications: replica receives subkey notifications after replication" {
|
||||
start_server {tags {"repl external:skip"}} {
|
||||
set master [srv -1 client]
|
||||
set master_host [srv -1 host]
|
||||
set master_port [srv -1 port]
|
||||
set replica [srv 0 client]
|
||||
|
||||
$replica replicaof $master_host $master_port
|
||||
wait_for_sync $replica
|
||||
|
||||
# Enable subkeyspace notifications on replica
|
||||
$replica config set notify-keyspace-events Sh
|
||||
|
||||
# Subscribe on replica
|
||||
set rd1 [redis_deferring_client -1]
|
||||
assert_equal {1} [subscribe $rd1 "__subkeyspace@${db}__:myhash"]
|
||||
|
||||
# Write on master
|
||||
$master hset myhash f1 v1 f2 v2
|
||||
$master hpexpire myhash 100 FIELDS 2 f1 f2
|
||||
|
||||
# Replica should receive subkey notification
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hset|2:f1,2:f2" [$rd1 read]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hexpire|2:f1,2:f2" [$rd1 read]
|
||||
assert_equal "message __subkeyspace@${db}__:myhash hexpired|2:f1,2:f2" [$rd1 read]
|
||||
$rd1 close
|
||||
$master del myhash
|
||||
}
|
||||
}
|
||||
|
||||
test "publish to self inside multi" {
|
||||
r hello 3
|
||||
r subscribe foo
|
||||
|
|
|
|||
|
|
@ -471,6 +471,21 @@ proc test_scan {type} {
|
|||
}
|
||||
}
|
||||
|
||||
test "{$type} SCAN COUNT overflow" {
|
||||
r flushdb
|
||||
populate 10
|
||||
|
||||
# count = LONG_MAX/10 + 1, within LONG_MAX so it parses fine,
|
||||
# but count*10 overflows signed long which is undefined behavior.
|
||||
# Compute dynamically to support both 32-bit and 64-bit builds.
|
||||
set long_max [expr {[s arch_bits] == 32 ? 2147483647 : 9223372036854775807}]
|
||||
set big_count [expr {$long_max / 10 + 1}]
|
||||
set res [r scan 0 count $big_count]
|
||||
assert {[llength $res] == 2}
|
||||
assert_equal 0 [lindex $res 0]
|
||||
assert_equal 10 [llength [lindex $res 1]]
|
||||
}
|
||||
|
||||
test "{$type} SCAN MATCH pattern implies cluster slot" {
|
||||
# Tests the code path for an optimization for patterns like "{foo}-*"
|
||||
# which implies that all matching keys belong to one slot.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ start_server {tags {"external:skip needs:debug"}} {
|
|||
test "HPEXPIRETIME persists after RDB reload ($type)" {
|
||||
r del myhash
|
||||
r hset myhash field1 value1 field2 value2
|
||||
r hpexpire myhash 300 NX FIELDS 1 field1
|
||||
r hpexpire myhash 500 NX FIELDS 1 field1
|
||||
set before [r HPEXPIRETIME myhash FIELDS 1 field1]
|
||||
r debug reload
|
||||
set after [r HPEXPIRETIME myhash FIELDS 1 field1]
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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}
|
||||
|
|
@ -1708,6 +1761,38 @@ start_server {tags {"zset"}} {
|
|||
}
|
||||
} {} {needs:debug}
|
||||
|
||||
test "ZSCORE 17-19 significant digit mantissas (widened fast path) - $encoding" {
|
||||
# Exercise the widened fast_float_strtod path that handles
|
||||
# mantissas > 2^53 (via __uint128_t arithmetic). ZADD/ZSCORE
|
||||
# must round-trip bit-exactly through the listpack/skiplist
|
||||
# encoding (parse on ingest, parse again on retrieval). Each
|
||||
# input string below parses to a specific IEEE double whose
|
||||
# canonical string representation is itself, so `expr` in Tcl
|
||||
# re-evaluates to the same numeric value.
|
||||
r del zscorewide
|
||||
set widecases {
|
||||
0.49606648747577575
|
||||
0.8731899671198792
|
||||
0.34912978268081996
|
||||
0.0033318113277969186
|
||||
0.9955843393406656
|
||||
-0.8731899671198792
|
||||
}
|
||||
set i 0
|
||||
foreach s $widecases {
|
||||
r zadd zscorewide $s m$i
|
||||
assert_equal [expr $s] [expr [r zscore zscorewide m$i]]
|
||||
incr i
|
||||
}
|
||||
r debug reload
|
||||
assert_encoding $encoding zscorewide
|
||||
set i 0
|
||||
foreach s $widecases {
|
||||
assert_equal [expr $s] [expr [r zscore zscorewide m$i]]
|
||||
incr i
|
||||
}
|
||||
} {} {needs:debug}
|
||||
|
||||
test "ZSET sorting stresser - $encoding" {
|
||||
set delta 0
|
||||
for {set test 0} {$test < 2} {incr test} {
|
||||
|
|
|
|||
|
|
@ -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