├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── actions │ └── run-tests │ │ └── action.yml ├── dependabot.yml ├── release-drafter-config.yml ├── spellcheck-settings.yml ├── wordlist.txt └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── doctests.yaml │ ├── golangci-lint.yml │ ├── release-drafter.yml │ ├── spellcheck.yml │ ├── stale-issues.yml │ └── test-redis-enterprise.yml ├── .gitignore ├── .golangci.yml ├── .prettierrc.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASE-NOTES.md ├── RELEASING.md ├── acl_commands.go ├── acl_commands_test.go ├── auth ├── auth.go ├── auth_test.go └── reauth_credentials_listener.go ├── bench_decode_test.go ├── bench_test.go ├── bitmap_commands.go ├── bitmap_commands_test.go ├── cluster_commands.go ├── command.go ├── command_recorder_test.go ├── command_test.go ├── commands.go ├── commands_test.go ├── doc.go ├── docker-compose.yml ├── dockers ├── .gitignore └── sentinel.conf ├── doctests ├── Makefile ├── README.md ├── bf_tutorial_test.go ├── bitfield_tutorial_test.go ├── bitmap_tutorial_test.go ├── cmds_generic_test.go ├── cmds_hash_test.go ├── cmds_list_test.go ├── cmds_servermgmt_test.go ├── cmds_set_test.go ├── cmds_sorted_set_test.go ├── cmds_string_test.go ├── cms_tutorial_test.go ├── cuckoo_tutorial_test.go ├── geo_index_test.go ├── geo_tutorial_test.go ├── hash_tutorial_test.go ├── hll_tutorial_test.go ├── home_json_example_test.go ├── json_tutorial_test.go ├── list_tutorial_test.go ├── lpush_lrange_test.go ├── main_test.go ├── pipe_trans_example_test.go ├── query_agg_test.go ├── query_em_test.go ├── query_ft_test.go ├── query_geo_test.go ├── query_range_test.go ├── set_get_test.go ├── sets_example_test.go ├── ss_tutorial_test.go ├── stream_tutorial_test.go ├── string_example_test.go ├── tdigest_tutorial_test.go ├── topk_tutorial_test.go └── vec_set_test.go ├── error.go ├── error_test.go ├── example ├── del-keys-without-ttl │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── hll │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── hset-struct │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── lua-scripting │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── otel │ ├── README.md │ ├── client.go │ ├── config │ │ ├── otel-collector.yaml │ │ └── vector.toml │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ ├── image │ │ ├── metrics.png │ │ └── redis-trace.png │ └── uptrace.yml ├── redis-bloom │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go └── scan-struct │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── example_instrumentation_test.go ├── example_test.go ├── export_test.go ├── extra ├── rediscensus │ ├── go.mod │ ├── go.sum │ └── rediscensus.go ├── rediscmd │ ├── go.mod │ ├── go.sum │ ├── rediscmd.go │ ├── rediscmd_test.go │ ├── safe.go │ └── unsafe.go ├── redisotel │ ├── README.md │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── metrics.go │ ├── tracing.go │ └── tracing_test.go └── redisprometheus │ ├── README.md │ ├── collector.go │ ├── go.mod │ └── go.sum ├── fuzz └── fuzz.go ├── generic_commands.go ├── geo_commands.go ├── go.mod ├── go.sum ├── hash_commands.go ├── helper └── helper.go ├── hyperloglog_commands.go ├── internal ├── arg.go ├── customvet │ ├── .gitignore │ ├── checks │ │ └── setval │ │ │ ├── setval.go │ │ │ ├── setval_test.go │ │ │ └── testdata │ │ │ └── src │ │ │ └── a │ │ │ └── a.go │ ├── go.mod │ ├── go.sum │ └── main.go ├── hashtag │ ├── hashtag.go │ └── hashtag_test.go ├── hscan │ ├── hscan.go │ ├── hscan_test.go │ └── structmap.go ├── internal.go ├── internal_test.go ├── log.go ├── once.go ├── pool │ ├── bench_test.go │ ├── conn.go │ ├── conn_check.go │ ├── conn_check_dummy.go │ ├── conn_check_test.go │ ├── export_test.go │ ├── main_test.go │ ├── pool.go │ ├── pool_single.go │ ├── pool_sticky.go │ └── pool_test.go ├── proto │ ├── proto_test.go │ ├── reader.go │ ├── reader_test.go │ ├── scan.go │ ├── scan_test.go │ ├── writer.go │ └── writer_test.go ├── rand │ └── rand.go ├── util.go ├── util │ ├── convert.go │ ├── convert_test.go │ ├── safe.go │ ├── strconv.go │ ├── strconv_test.go │ ├── type.go │ └── unsafe.go └── util_test.go ├── internal_test.go ├── iterator.go ├── iterator_test.go ├── json.go ├── json_test.go ├── list_commands.go ├── main_test.go ├── monitor_test.go ├── options.go ├── options_test.go ├── osscluster.go ├── osscluster_commands.go ├── osscluster_test.go ├── pipeline.go ├── pipeline_test.go ├── pool_test.go ├── probabilistic.go ├── probabilistic_test.go ├── pubsub.go ├── pubsub_commands.go ├── pubsub_test.go ├── race_test.go ├── redis.go ├── redis_test.go ├── result.go ├── ring.go ├── ring_test.go ├── script.go ├── scripting_commands.go ├── scripts ├── bump_deps.sh ├── release.sh └── tag.sh ├── search_commands.go ├── search_test.go ├── sentinel.go ├── sentinel_test.go ├── set_commands.go ├── sortedset_commands.go ├── stream_commands.go ├── string_commands.go ├── timeseries_commands.go ├── timeseries_commands_test.go ├── tx.go ├── tx_test.go ├── unit_test.go ├── universal.go ├── universal_test.go ├── vectorset_commands.go ├── vectorset_commands_integration_test.go ├── vectorset_commands_test.go └── version.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | doctests/* @dmaier-redislabs 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://uptrace.dev/sponsor'] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | Issue tracker is used for reporting bugs and discussing new features. Please use 10 | [stackoverflow](https://stackoverflow.com) for supporting issues. 11 | 12 | 13 | 14 | ## Expected Behavior 15 | 16 | 17 | 18 | ## Current Behavior 19 | 20 | 21 | 22 | ## Possible Solution 23 | 24 | 25 | 26 | ## Steps to Reproduce 27 | 28 | 29 | 30 | 31 | 1. 32 | 2. 33 | 3. 34 | 4. 35 | 36 | ## Context (Environment) 37 | 38 | 39 | 40 | 41 | 42 | 43 | ## Detailed Description 44 | 45 | 46 | 47 | ## Possible Implementation 48 | 49 | 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discussions 4 | url: https://github.com/go-redis/redis/discussions 5 | about: Ask a question via GitHub Discussions 6 | -------------------------------------------------------------------------------- /.github/actions/run-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Run go-redis tests' 2 | description: 'Runs go-redis tests against different Redis versions and configurations' 3 | inputs: 4 | go-version: 5 | description: 'Go version to use for running tests' 6 | default: '1.23' 7 | redis-version: 8 | description: 'Redis version to test against' 9 | required: true 10 | runs: 11 | using: "composite" 12 | steps: 13 | - name: Set up ${{ inputs.go-version }} 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: ${{ inputs.go-version }} 17 | 18 | - name: Setup Test environment 19 | env: 20 | REDIS_VERSION: ${{ inputs.redis-version }} 21 | CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ inputs.redis-version }}" 22 | run: | 23 | set -e 24 | redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+') 25 | 26 | # Mapping of redis version to redis testing containers 27 | declare -A redis_version_mapping=( 28 | ["8.0.1"]="8.0.1-pre" 29 | ["7.4.2"]="rs-7.4.0-v2" 30 | ["7.2.7"]="rs-7.2.0-v14" 31 | ) 32 | 33 | if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then 34 | echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV 35 | echo "REDIS_IMAGE=redis:${{ inputs.redis-version }}" >> $GITHUB_ENV 36 | echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV 37 | else 38 | echo "Version not found in the mapping." 39 | exit 1 40 | fi 41 | sleep 10 # wait for redis to start 42 | shell: bash 43 | - name: Set up Docker Compose environment with redis ${{ inputs.redis-version }} 44 | run: | 45 | make docker.start 46 | shell: bash 47 | - name: Run tests 48 | env: 49 | RCE_DOCKER: "true" 50 | RE_CLUSTER: "false" 51 | run: | 52 | make test.ci 53 | shell: bash 54 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/release-drafter-config.yml: -------------------------------------------------------------------------------- 1 | name-template: '$NEXT_MINOR_VERSION' 2 | tag-template: 'v$NEXT_MINOR_VERSION' 3 | autolabeler: 4 | - label: 'maintenance' 5 | files: 6 | - '*.md' 7 | - '.github/*' 8 | - label: 'bug' 9 | branch: 10 | - '/bug-.+' 11 | - label: 'maintenance' 12 | branch: 13 | - '/maintenance-.+' 14 | - label: 'feature' 15 | branch: 16 | - '/feature-.+' 17 | categories: 18 | - title: 'Breaking Changes' 19 | labels: 20 | - 'breakingchange' 21 | - title: '🧪 Experimental Features' 22 | labels: 23 | - 'experimental' 24 | - title: '🚀 New Features' 25 | labels: 26 | - 'feature' 27 | - 'enhancement' 28 | - title: '🐛 Bug Fixes' 29 | labels: 30 | - 'fix' 31 | - 'bugfix' 32 | - 'bug' 33 | - 'BUG' 34 | - title: '🧰 Maintenance' 35 | label: 'maintenance' 36 | change-template: '- $TITLE (#$NUMBER)' 37 | exclude-labels: 38 | - 'skip-changelog' 39 | template: | 40 | # Changes 41 | 42 | $CHANGES 43 | 44 | ## Contributors 45 | We'd like to thank all the contributors who worked on this release! 46 | 47 | $CONTRIBUTORS 48 | 49 | -------------------------------------------------------------------------------- /.github/spellcheck-settings.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: Markdown 3 | expect_match: false 4 | apsell: 5 | lang: en 6 | d: en_US 7 | ignore-case: true 8 | dictionary: 9 | wordlists: 10 | - .github/wordlist.txt 11 | output: wordlist.dic 12 | pipeline: 13 | - pyspelling.filters.markdown: 14 | markdown_extensions: 15 | - markdown.extensions.extra: 16 | - pyspelling.filters.html: 17 | comments: false 18 | attributes: 19 | - alt 20 | ignores: 21 | - ':matches(code, pre)' 22 | - code 23 | - pre 24 | - blockquote 25 | - img 26 | sources: 27 | - 'README.md' 28 | - 'FAQ.md' 29 | - 'docs/**' 30 | -------------------------------------------------------------------------------- /.github/wordlist.txt: -------------------------------------------------------------------------------- 1 | ACLs 2 | APIs 3 | autoload 4 | autoloader 5 | autoloading 6 | analytics 7 | Autoloading 8 | backend 9 | backends 10 | behaviour 11 | CAS 12 | ClickHouse 13 | config 14 | customizable 15 | Customizable 16 | dataset 17 | de 18 | DisableIdentity 19 | ElastiCache 20 | extensibility 21 | FPM 22 | Golang 23 | IANA 24 | keyspace 25 | keyspaces 26 | Kvrocks 27 | localhost 28 | Lua 29 | MSSQL 30 | namespace 31 | NoSQL 32 | OpenTelemetry 33 | ORM 34 | Packagist 35 | PhpRedis 36 | pipelining 37 | pluggable 38 | Predis 39 | PSR 40 | Quickstart 41 | README 42 | rebalanced 43 | rebalancing 44 | redis 45 | Redis 46 | RocksDB 47 | runtime 48 | SHA 49 | sharding 50 | SETNAME 51 | SpellCheck 52 | SSL 53 | struct 54 | stunnel 55 | SynDump 56 | TCP 57 | TLS 58 | UnstableResp 59 | uri 60 | URI 61 | url 62 | variadic 63 | RedisStack 64 | RedisGears 65 | RedisTimeseries 66 | RediSearch 67 | RawResult 68 | RawVal 69 | entra 70 | EntraID 71 | Entra 72 | OAuth 73 | Azure 74 | StreamingCredentialsProvider 75 | oauth 76 | entraid -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [master, v9, v9.7, v9.8] 6 | pull_request: 7 | branches: [master, v9, v9.7, v9.8] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | 14 | benchmark: 15 | name: benchmark 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | redis-version: 21 | - "8.0.1" # 8.0.1 22 | - "7.4.2" # should use redis stack 7.4 23 | go-version: 24 | - "1.23.x" 25 | - "1.24.x" 26 | 27 | steps: 28 | - name: Set up ${{ matrix.go-version }} 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: ${{ matrix.go-version }} 32 | 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | 36 | - name: Setup Test environment 37 | env: 38 | REDIS_VERSION: ${{ matrix.redis-version }} 39 | CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ matrix.redis-version }}" 40 | run: | 41 | set -e 42 | redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+') 43 | 44 | # Mapping of redis version to redis testing containers 45 | declare -A redis_version_mapping=( 46 | ["8.0.1"]="8.0.1-pre" 47 | ["7.4.2"]="rs-7.4.0-v2" 48 | ) 49 | if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then 50 | echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV 51 | echo "REDIS_IMAGE=redis:${{ matrix.redis-version }}" >> $GITHUB_ENV 52 | echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV 53 | else 54 | echo "Version not found in the mapping." 55 | exit 1 56 | fi 57 | shell: bash 58 | - name: Set up Docker Compose environment with redis ${{ matrix.redis-version }} 59 | run: make docker.start 60 | shell: bash 61 | - name: Benchmark Tests 62 | env: 63 | RCE_DOCKER: "true" 64 | RE_CLUSTER: "false" 65 | run: make bench 66 | shell: bash 67 | 68 | test-redis-ce: 69 | name: test-redis-ce 70 | runs-on: ubuntu-latest 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | redis-version: 75 | - "8.0.1" # 8.0.1 76 | - "7.4.2" # should use redis stack 7.4 77 | - "7.2.7" # should redis stack 7.2 78 | go-version: 79 | - "1.23.x" 80 | - "1.24.x" 81 | 82 | steps: 83 | - name: Checkout code 84 | uses: actions/checkout@v4 85 | 86 | - name: Run tests 87 | uses: ./.github/actions/run-tests 88 | with: 89 | go-version: ${{matrix.go-version}} 90 | redis-version: ${{ matrix.redis-version }} 91 | 92 | - name: Upload to Codecov 93 | uses: codecov/codecov-action@v5 94 | with: 95 | files: coverage.txt 96 | token: ${{ secrets.CODECOV_TOKEN }} 97 | 98 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master, v9, v9.7, v9.8] 17 | pull_request: 18 | branches: [master, v9, v9.7, v9.8] 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | runs-on: ubuntu-latest 24 | permissions: 25 | actions: read 26 | contents: read 27 | security-events: write 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'go' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 34 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/doctests.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation Tests 2 | 3 | on: 4 | push: 5 | branches: [master, examples] 6 | pull_request: 7 | branches: [master, examples] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | doctests: 14 | name: doctests 15 | runs-on: ubuntu-latest 16 | 17 | services: 18 | redis-stack: 19 | image: redislabs/client-libs-test:8.0.1-pre 20 | env: 21 | TLS_ENABLED: no 22 | REDIS_CLUSTER: no 23 | PORT: 6379 24 | ports: 25 | - 6379:6379 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | go-version: ["1.24"] 31 | 32 | steps: 33 | - name: Set up ${{ matrix.go-version }} 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version: ${{ matrix.go-version }} 37 | 38 | - name: Checkout code 39 | uses: actions/checkout@v4 40 | 41 | - name: Test doc examples 42 | working-directory: ./doctests 43 | run: make test 44 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | - main 10 | - v9 11 | - v9.8 12 | pull_request: 13 | 14 | permissions: 15 | contents: read 16 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 17 | 18 | jobs: 19 | golangci: 20 | name: lint 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: golangci-lint 25 | uses: golangci/golangci-lint-action@v8.0.0 26 | with: 27 | verify: true 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | permissions: {} 10 | jobs: 11 | update_release_draft: 12 | permissions: 13 | pull-requests: write # to add label to PR (release-drafter/release-drafter) 14 | contents: write # to create a github release (release-drafter/release-drafter) 15 | 16 | runs-on: ubuntu-latest 17 | steps: 18 | # Drafts your next Release notes as Pull Requests are merged into "master" 19 | - uses: release-drafter/release-drafter@v6 20 | with: 21 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 22 | config-name: release-drafter-config.yml 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yml: -------------------------------------------------------------------------------- 1 | name: spellcheck 2 | on: 3 | pull_request: 4 | jobs: 5 | check-spelling: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v4 10 | - name: Check Spelling 11 | uses: rojopolis/spellcheck-github-actions@0.49.0 12 | with: 13 | config_path: .github/spellcheck-settings.yml 14 | task_name: Markdown 15 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | permissions: {} 7 | jobs: 8 | stale: 9 | permissions: 10 | issues: write # to close stale issues (actions/stale) 11 | pull-requests: write # to close stale PRs (actions/stale) 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.' 19 | stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.' 20 | days-before-stale: 365 21 | days-before-close: 30 22 | stale-issue-label: "Stale" 23 | stale-pr-label: "Stale" 24 | operations-per-run: 10 25 | remove-stale-when-updated: true 26 | -------------------------------------------------------------------------------- /.github/workflows/test-redis-enterprise.yml: -------------------------------------------------------------------------------- 1 | name: RE Tests 2 | 3 | on: 4 | push: 5 | branches: [master, v9, v9.7, v9.8] 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | name: build 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | go-version: [1.24.x] 19 | re-build: ["7.4.2-54"] 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Clone Redis EE docker repository 26 | uses: actions/checkout@v4 27 | with: 28 | repository: RedisLabs/redis-ee-docker 29 | path: redis-ee 30 | 31 | - name: Set up ${{ matrix.go-version }} 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: ${{ matrix.go-version }} 35 | 36 | - name: Build cluster 37 | working-directory: redis-ee 38 | env: 39 | IMAGE: "redislabs/redis:${{ matrix.re-build }}" 40 | RE_USERNAME: test@test.com 41 | RE_PASS: 12345 42 | RE_CLUSTER_NAME: re-test 43 | RE_USE_OSS_CLUSTER: false 44 | RE_DB_PORT: 6379 45 | run: ./build.sh 46 | 47 | - name: Test 48 | env: 49 | RE_CLUSTER: true 50 | REDIS_VERSION: "7.4" 51 | run: | 52 | go test \ 53 | --ginkgo.skip-file="ring_test.go" \ 54 | --ginkgo.skip-file="sentinel_test.go" \ 55 | --ginkgo.skip-file="osscluster_test.go" \ 56 | --ginkgo.skip-file="pubsub_test.go" \ 57 | --ginkgo.label-filter='!NonRedisEnterprise' 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rdb 2 | testdata/* 3 | .idea/ 4 | .DS_Store 5 | *.tar.gz 6 | *.dic 7 | redis8tests.sh 8 | coverage.txt 9 | **/coverage.txt 10 | .vscode 11 | tmp/* 12 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | timeout: 5m 4 | tests: false 5 | linters: 6 | settings: 7 | staticcheck: 8 | checks: 9 | - all 10 | # Incorrect or missing package comment. 11 | # https://staticcheck.dev/docs/checks/#ST1000 12 | - -ST1000 13 | # Omit embedded fields from selector expression. 14 | # https://staticcheck.dev/docs/checks/#QF1008 15 | - -QF1008 16 | - -ST1003 17 | exclusions: 18 | generated: lax 19 | presets: 20 | - comments 21 | - common-false-positives 22 | - legacy 23 | - std-error-handling 24 | paths: 25 | - third_party$ 26 | - builtin$ 27 | - examples$ 28 | formatters: 29 | exclusions: 30 | generated: lax 31 | paths: 32 | - third_party$ 33 | - builtin$ 34 | - examples$ 35 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | proseWrap: always 4 | printWidth: 100 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 The github.com/redis/go-redis Authors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) 2 | 3 | docker.start: 4 | docker compose --profile all up -d --quiet-pull 5 | 6 | docker.stop: 7 | docker compose --profile all down 8 | 9 | test: 10 | $(MAKE) docker.start 11 | @if [ -z "$(REDIS_VERSION)" ]; then \ 12 | echo "REDIS_VERSION not set, running all tests"; \ 13 | $(MAKE) test.ci; \ 14 | else \ 15 | MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \ 16 | if [ "$$MAJOR_VERSION" -ge 8 ]; then \ 17 | echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \ 18 | $(MAKE) test.ci; \ 19 | else \ 20 | echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \ 21 | $(MAKE) test.ci.skip-vectorsets; \ 22 | fi; \ 23 | fi 24 | $(MAKE) docker.stop 25 | 26 | test.ci: 27 | set -e; for dir in $(GO_MOD_DIRS); do \ 28 | echo "go test in $${dir}"; \ 29 | (cd "$${dir}" && \ 30 | go mod tidy -compat=1.18 && \ 31 | go vet && \ 32 | go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \ 33 | done 34 | cd internal/customvet && go build . 35 | go vet -vettool ./internal/customvet/customvet 36 | 37 | test.ci.skip-vectorsets: 38 | set -e; for dir in $(GO_MOD_DIRS); do \ 39 | echo "go test in $${dir} (skipping vector sets)"; \ 40 | (cd "$${dir}" && \ 41 | go mod tidy -compat=1.18 && \ 42 | go vet && \ 43 | go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \ 44 | -run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \ 45 | done 46 | cd internal/customvet && go build . 47 | go vet -vettool ./internal/customvet/customvet 48 | 49 | bench: 50 | go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example 51 | 52 | .PHONY: all test test.ci test.ci.skip-vectorsets bench fmt 53 | 54 | build: 55 | go build . 56 | 57 | fmt: 58 | gofumpt -w ./ 59 | goimports -w -local github.com/redis/go-redis ./ 60 | 61 | go_mod_tidy: 62 | set -e; for dir in $(GO_MOD_DIRS); do \ 63 | echo "go mod tidy in $${dir}"; \ 64 | (cd "$${dir}" && \ 65 | go get -u ./... && \ 66 | go mod tidy -compat=1.18); \ 67 | done 68 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub: 4 | 5 | ```shell 6 | TAG=v1.0.0 ./scripts/release.sh 7 | ``` 8 | 9 | 2. Open a pull request and wait for the build to finish. 10 | 11 | 3. Merge the pull request and run `tag.sh` to create tags for packages: 12 | 13 | ```shell 14 | TAG=v1.0.0 ./scripts/tag.sh 15 | ``` 16 | -------------------------------------------------------------------------------- /acl_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "context" 4 | 5 | type ACLCmdable interface { 6 | ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd 7 | 8 | ACLLog(ctx context.Context, count int64) *ACLLogCmd 9 | ACLLogReset(ctx context.Context) *StatusCmd 10 | 11 | ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd 12 | ACLDelUser(ctx context.Context, username string) *IntCmd 13 | ACLList(ctx context.Context) *StringSliceCmd 14 | 15 | ACLCat(ctx context.Context) *StringSliceCmd 16 | ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd 17 | } 18 | 19 | type ACLCatArgs struct { 20 | Category string 21 | } 22 | 23 | func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd { 24 | args := make([]interface{}, 0, 3+len(command)) 25 | args = append(args, "acl", "dryrun", username) 26 | args = append(args, command...) 27 | cmd := NewStringCmd(ctx, args...) 28 | _ = c(ctx, cmd) 29 | return cmd 30 | } 31 | 32 | func (c cmdable) ACLLog(ctx context.Context, count int64) *ACLLogCmd { 33 | args := make([]interface{}, 0, 3) 34 | args = append(args, "acl", "log") 35 | if count > 0 { 36 | args = append(args, count) 37 | } 38 | cmd := NewACLLogCmd(ctx, args...) 39 | _ = c(ctx, cmd) 40 | return cmd 41 | } 42 | 43 | func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd { 44 | cmd := NewStatusCmd(ctx, "acl", "log", "reset") 45 | _ = c(ctx, cmd) 46 | return cmd 47 | } 48 | 49 | func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd { 50 | cmd := NewIntCmd(ctx, "acl", "deluser", username) 51 | _ = c(ctx, cmd) 52 | return cmd 53 | } 54 | 55 | func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd { 56 | args := make([]interface{}, 3+len(rules)) 57 | args[0] = "acl" 58 | args[1] = "setuser" 59 | args[2] = username 60 | for i, rule := range rules { 61 | args[i+3] = rule 62 | } 63 | cmd := NewStatusCmd(ctx, args...) 64 | _ = c(ctx, cmd) 65 | return cmd 66 | } 67 | 68 | func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd { 69 | cmd := NewStringSliceCmd(ctx, "acl", "list") 70 | _ = c(ctx, cmd) 71 | return cmd 72 | } 73 | 74 | func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd { 75 | cmd := NewStringSliceCmd(ctx, "acl", "cat") 76 | _ = c(ctx, cmd) 77 | return cmd 78 | } 79 | 80 | func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd { 81 | // if there is a category passed, build new cmd, if there isn't - use the ACLCat method 82 | if options != nil && options.Category != "" { 83 | cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category) 84 | _ = c(ctx, cmd) 85 | return cmd 86 | } 87 | 88 | return c.ACLCat(ctx) 89 | } 90 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | // Package auth package provides authentication-related interfaces and types. 2 | // It also includes a basic implementation of credentials using username and password. 3 | package auth 4 | 5 | // StreamingCredentialsProvider is an interface that defines the methods for a streaming credentials provider. 6 | // It is used to provide credentials for authentication. 7 | // The CredentialsListener is used to receive updates when the credentials change. 8 | type StreamingCredentialsProvider interface { 9 | // Subscribe subscribes to the credentials provider for updates. 10 | // It returns the current credentials, a cancel function to unsubscribe from the provider, 11 | // and an error if any. 12 | // TODO(ndyakov): Should we add context to the Subscribe method? 13 | Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) 14 | } 15 | 16 | // UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider. 17 | // It is used to unsubscribe from the provider when the credentials are no longer needed. 18 | type UnsubscribeFunc func() error 19 | 20 | // CredentialsListener is an interface that defines the methods for a credentials listener. 21 | // It is used to receive updates when the credentials change. 22 | // The OnNext method is called when the credentials change. 23 | // The OnError method is called when an error occurs while requesting the credentials. 24 | type CredentialsListener interface { 25 | OnNext(credentials Credentials) 26 | OnError(err error) 27 | } 28 | 29 | // Credentials is an interface that defines the methods for credentials. 30 | // It is used to provide the credentials for authentication. 31 | type Credentials interface { 32 | // BasicAuth returns the username and password for basic authentication. 33 | BasicAuth() (username string, password string) 34 | // RawCredentials returns the raw credentials as a string. 35 | // This can be used to extract the username and password from the raw credentials or 36 | // additional information if present in the token. 37 | RawCredentials() string 38 | } 39 | 40 | type basicAuth struct { 41 | username string 42 | password string 43 | } 44 | 45 | // RawCredentials returns the raw credentials as a string. 46 | func (b *basicAuth) RawCredentials() string { 47 | return b.username + ":" + b.password 48 | } 49 | 50 | // BasicAuth returns the username and password for basic authentication. 51 | func (b *basicAuth) BasicAuth() (username string, password string) { 52 | return b.username, b.password 53 | } 54 | 55 | // NewBasicCredentials creates a new Credentials object from the given username and password. 56 | func NewBasicCredentials(username, password string) Credentials { 57 | return &basicAuth{ 58 | username: username, 59 | password: password, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /auth/reauth_credentials_listener.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // ReAuthCredentialsListener is a struct that implements the CredentialsListener interface. 4 | // It is used to re-authenticate the credentials when they are updated. 5 | // It contains: 6 | // - reAuth: a function that takes the new credentials and returns an error if any. 7 | // - onErr: a function that takes an error and handles it. 8 | type ReAuthCredentialsListener struct { 9 | reAuth func(credentials Credentials) error 10 | onErr func(err error) 11 | } 12 | 13 | // OnNext is called when the credentials are updated. 14 | // It calls the reAuth function with the new credentials. 15 | // If the reAuth function returns an error, it calls the onErr function with the error. 16 | func (c *ReAuthCredentialsListener) OnNext(credentials Credentials) { 17 | if c.reAuth == nil { 18 | return 19 | } 20 | 21 | err := c.reAuth(credentials) 22 | if err != nil { 23 | c.OnError(err) 24 | } 25 | } 26 | 27 | // OnError is called when an error occurs. 28 | // It can be called from both the credentials provider and the reAuth function. 29 | func (c *ReAuthCredentialsListener) OnError(err error) { 30 | if c.onErr == nil { 31 | return 32 | } 33 | 34 | c.onErr(err) 35 | } 36 | 37 | // NewReAuthCredentialsListener creates a new ReAuthCredentialsListener. 38 | // Implements the auth.CredentialsListener interface. 39 | func NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, onErr func(err error)) *ReAuthCredentialsListener { 40 | return &ReAuthCredentialsListener{ 41 | reAuth: reAuth, 42 | onErr: onErr, 43 | } 44 | } 45 | 46 | // Ensure ReAuthCredentialsListener implements the CredentialsListener interface. 47 | var _ CredentialsListener = (*ReAuthCredentialsListener)(nil) 48 | -------------------------------------------------------------------------------- /bitmap_commands_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | . "github.com/bsm/ginkgo/v2" 5 | . "github.com/bsm/gomega" 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | type bitCountExpected struct { 10 | Start int64 11 | End int64 12 | Expected int64 13 | } 14 | 15 | var _ = Describe("BitCountBite", func() { 16 | var client *redis.Client 17 | key := "bit_count_test" 18 | 19 | BeforeEach(func() { 20 | client = redis.NewClient(redisOptions()) 21 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 22 | values := []int{0, 1, 0, 0, 1, 0, 1, 0, 1, 1} 23 | for i, v := range values { 24 | cmd := client.SetBit(ctx, key, int64(i), v) 25 | Expect(cmd.Err()).NotTo(HaveOccurred()) 26 | } 27 | }) 28 | 29 | AfterEach(func() { 30 | Expect(client.Close()).NotTo(HaveOccurred()) 31 | }) 32 | 33 | It("bit count bite", func() { 34 | var expected = []bitCountExpected{ 35 | {0, 0, 0}, 36 | {0, 1, 1}, 37 | {0, 2, 1}, 38 | {0, 3, 1}, 39 | {0, 4, 2}, 40 | {0, 5, 2}, 41 | {0, 6, 3}, 42 | {0, 7, 3}, 43 | {0, 8, 4}, 44 | {0, 9, 5}, 45 | } 46 | 47 | for _, e := range expected { 48 | cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexBit}) 49 | Expect(cmd.Err()).NotTo(HaveOccurred()) 50 | Expect(cmd.Val()).To(Equal(e.Expected)) 51 | } 52 | }) 53 | }) 54 | 55 | var _ = Describe("BitCountByte", func() { 56 | var client *redis.Client 57 | key := "bit_count_test" 58 | 59 | BeforeEach(func() { 60 | client = redis.NewClient(redisOptions()) 61 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 62 | values := []int{0, 0, 0, 0, 0, 0, 0, 1, 1, 1} 63 | for i, v := range values { 64 | cmd := client.SetBit(ctx, key, int64(i), v) 65 | Expect(cmd.Err()).NotTo(HaveOccurred()) 66 | } 67 | }) 68 | 69 | AfterEach(func() { 70 | Expect(client.Close()).NotTo(HaveOccurred()) 71 | }) 72 | 73 | It("bit count byte", func() { 74 | var expected = []bitCountExpected{ 75 | {0, 0, 1}, 76 | {0, 1, 3}, 77 | } 78 | 79 | for _, e := range expected { 80 | cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexByte}) 81 | Expect(cmd.Err()).NotTo(HaveOccurred()) 82 | Expect(cmd.Val()).To(Equal(e.Expected)) 83 | } 84 | }) 85 | 86 | It("bit count byte with no unit specified", func() { 87 | var expected = []bitCountExpected{ 88 | {0, 0, 1}, 89 | {0, 1, 3}, 90 | } 91 | 92 | for _, e := range expected { 93 | cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End}) 94 | Expect(cmd.Err()).NotTo(HaveOccurred()) 95 | Expect(cmd.Val()).To(Equal(e.Expected)) 96 | } 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /command_recorder_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | // commandRecorder records the last N commands executed by a Redis client. 12 | type commandRecorder struct { 13 | mu sync.Mutex 14 | commands []string 15 | maxSize int 16 | } 17 | 18 | // newCommandRecorder creates a new command recorder with the specified maximum size. 19 | func newCommandRecorder(maxSize int) *commandRecorder { 20 | return &commandRecorder{ 21 | commands: make([]string, 0, maxSize), 22 | maxSize: maxSize, 23 | } 24 | } 25 | 26 | // Record adds a command to the recorder. 27 | func (r *commandRecorder) Record(cmd string) { 28 | cmd = strings.ToLower(cmd) 29 | r.mu.Lock() 30 | defer r.mu.Unlock() 31 | 32 | r.commands = append(r.commands, cmd) 33 | if len(r.commands) > r.maxSize { 34 | r.commands = r.commands[1:] 35 | } 36 | } 37 | 38 | // LastCommands returns a copy of the recorded commands. 39 | func (r *commandRecorder) LastCommands() []string { 40 | r.mu.Lock() 41 | defer r.mu.Unlock() 42 | return append([]string(nil), r.commands...) 43 | } 44 | 45 | // Contains checks if the recorder contains a specific command. 46 | func (r *commandRecorder) Contains(cmd string) bool { 47 | cmd = strings.ToLower(cmd) 48 | r.mu.Lock() 49 | defer r.mu.Unlock() 50 | for _, c := range r.commands { 51 | if strings.Contains(c, cmd) { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | // Hook returns a Redis hook that records commands. 59 | func (r *commandRecorder) Hook() redis.Hook { 60 | return &commandHook{recorder: r} 61 | } 62 | 63 | // commandHook implements the redis.Hook interface to record commands. 64 | type commandHook struct { 65 | recorder *commandRecorder 66 | } 67 | 68 | func (h *commandHook) DialHook(next redis.DialHook) redis.DialHook { 69 | return next 70 | } 71 | 72 | func (h *commandHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 73 | return func(ctx context.Context, cmd redis.Cmder) error { 74 | h.recorder.Record(cmd.String()) 75 | return next(ctx, cmd) 76 | } 77 | } 78 | 79 | func (h *commandHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 80 | return func(ctx context.Context, cmds []redis.Cmder) error { 81 | for _, cmd := range cmds { 82 | h.recorder.Record(cmd.String()) 83 | } 84 | return next(ctx, cmds) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/redis/go-redis/v9" 8 | 9 | . "github.com/bsm/ginkgo/v2" 10 | . "github.com/bsm/gomega" 11 | ) 12 | 13 | var _ = Describe("Cmd", func() { 14 | var client *redis.Client 15 | 16 | BeforeEach(func() { 17 | client = redis.NewClient(redisOptions()) 18 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 19 | }) 20 | 21 | AfterEach(func() { 22 | Expect(client.Close()).NotTo(HaveOccurred()) 23 | }) 24 | 25 | It("implements Stringer", func() { 26 | set := client.Set(ctx, "foo", "bar", 0) 27 | Expect(set.String()).To(Equal("set foo bar: OK")) 28 | 29 | get := client.Get(ctx, "foo") 30 | Expect(get.String()).To(Equal("get foo: bar")) 31 | }) 32 | 33 | It("has val/err", func() { 34 | set := client.Set(ctx, "key", "hello", 0) 35 | Expect(set.Err()).NotTo(HaveOccurred()) 36 | Expect(set.Val()).To(Equal("OK")) 37 | 38 | get := client.Get(ctx, "key") 39 | Expect(get.Err()).NotTo(HaveOccurred()) 40 | Expect(get.Val()).To(Equal("hello")) 41 | 42 | Expect(set.Err()).NotTo(HaveOccurred()) 43 | Expect(set.Val()).To(Equal("OK")) 44 | }) 45 | 46 | It("has helpers", func() { 47 | set := client.Set(ctx, "key", "10", 0) 48 | Expect(set.Err()).NotTo(HaveOccurred()) 49 | 50 | n, err := client.Get(ctx, "key").Int64() 51 | Expect(err).NotTo(HaveOccurred()) 52 | Expect(n).To(Equal(int64(10))) 53 | 54 | un, err := client.Get(ctx, "key").Uint64() 55 | Expect(err).NotTo(HaveOccurred()) 56 | Expect(un).To(Equal(uint64(10))) 57 | 58 | f, err := client.Get(ctx, "key").Float64() 59 | Expect(err).NotTo(HaveOccurred()) 60 | Expect(f).To(Equal(float64(10))) 61 | }) 62 | 63 | It("supports float32", func() { 64 | f := float32(66.97) 65 | 66 | err := client.Set(ctx, "float_key", f, 0).Err() 67 | Expect(err).NotTo(HaveOccurred()) 68 | 69 | val, err := client.Get(ctx, "float_key").Float32() 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(val).To(Equal(f)) 72 | }) 73 | 74 | It("supports time.Time", func() { 75 | tm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC) 76 | 77 | err := client.Set(ctx, "time_key", tm, 0).Err() 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | s, err := client.Get(ctx, "time_key").Result() 81 | Expect(err).NotTo(HaveOccurred()) 82 | Expect(s).To(Equal("2019-01-01T09:45:10.000222125Z")) 83 | 84 | tm2, err := client.Get(ctx, "time_key").Time() 85 | Expect(err).NotTo(HaveOccurred()) 86 | Expect(tm2).To(BeTemporally("==", tm)) 87 | }) 88 | 89 | It("allows to set custom error", func() { 90 | e := errors.New("custom error") 91 | cmd := redis.Cmd{} 92 | cmd.SetErr(e) 93 | _, err := cmd.Result() 94 | Expect(err).To(Equal(e)) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package redis implements a Redis client. 3 | */ 4 | package redis 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | services: 4 | redis: 5 | image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} 6 | platform: linux/amd64 7 | container_name: redis-standalone 8 | environment: 9 | - TLS_ENABLED=yes 10 | - REDIS_CLUSTER=no 11 | - PORT=6379 12 | - TLS_PORT=6666 13 | command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} 14 | ports: 15 | - 6379:6379 16 | - 6666:6666 # TLS port 17 | volumes: 18 | - "./dockers/standalone:/redis/work" 19 | profiles: 20 | - standalone 21 | - sentinel 22 | - all-stack 23 | - all 24 | 25 | osscluster: 26 | image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} 27 | platform: linux/amd64 28 | container_name: redis-osscluster 29 | environment: 30 | - NODES=6 31 | - PORT=16600 32 | command: "--cluster-enabled yes" 33 | ports: 34 | - "16600-16605:16600-16605" 35 | volumes: 36 | - "./dockers/osscluster:/redis/work" 37 | profiles: 38 | - cluster 39 | - all-stack 40 | - all 41 | 42 | sentinel-cluster: 43 | image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} 44 | platform: linux/amd64 45 | container_name: redis-sentinel-cluster 46 | network_mode: "host" 47 | environment: 48 | - NODES=3 49 | - TLS_ENABLED=yes 50 | - REDIS_CLUSTER=no 51 | - PORT=9121 52 | command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} 53 | #ports: 54 | # - "9121-9123:9121-9123" 55 | volumes: 56 | - "./dockers/sentinel-cluster:/redis/work" 57 | profiles: 58 | - sentinel 59 | - all-stack 60 | - all 61 | 62 | sentinel: 63 | image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} 64 | platform: linux/amd64 65 | container_name: redis-sentinel 66 | depends_on: 67 | - sentinel-cluster 68 | environment: 69 | - NODES=3 70 | - REDIS_CLUSTER=no 71 | - PORT=26379 72 | command: ${REDIS_EXTRA_ARGS:---sentinel} 73 | network_mode: "host" 74 | #ports: 75 | # - 26379:26379 76 | # - 26380:26380 77 | # - 26381:26381 78 | volumes: 79 | - "./dockers/sentinel.conf:/redis/config-default/redis.conf" 80 | - "./dockers/sentinel:/redis/work" 81 | profiles: 82 | - sentinel 83 | - all-stack 84 | - all 85 | 86 | ring-cluster: 87 | image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} 88 | platform: linux/amd64 89 | container_name: redis-ring-cluster 90 | environment: 91 | - NODES=3 92 | - TLS_ENABLED=yes 93 | - REDIS_CLUSTER=no 94 | - PORT=6390 95 | command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} 96 | ports: 97 | - 6390:6390 98 | - 6391:6391 99 | - 6392:6392 100 | volumes: 101 | - "./dockers/ring:/redis/work" 102 | profiles: 103 | - ring 104 | - cluster 105 | - all-stack 106 | - all 107 | -------------------------------------------------------------------------------- /dockers/.gitignore: -------------------------------------------------------------------------------- 1 | osscluster/ 2 | ring/ 3 | standalone/ 4 | sentinel-cluster/ 5 | sentinel/ 6 | 7 | -------------------------------------------------------------------------------- /dockers/sentinel.conf: -------------------------------------------------------------------------------- 1 | sentinel resolve-hostnames yes 2 | sentinel monitor go-redis-test 127.0.0.1 9121 2 3 | sentinel down-after-milliseconds go-redis-test 5000 4 | sentinel failover-timeout go-redis-test 60000 5 | sentinel parallel-syncs go-redis-test 1 -------------------------------------------------------------------------------- /doctests/Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @if [ -z "$(REDIS_VERSION)" ]; then \ 3 | echo "REDIS_VERSION not set, running all tests"; \ 4 | go test -v ./...; \ 5 | else \ 6 | MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \ 7 | if [ "$$MAJOR_VERSION" -ge 8 ]; then \ 8 | echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \ 9 | go test -v ./...; \ 10 | else \ 11 | echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \ 12 | go test -v ./... -run '^(?!.*(?:vectorset|ExampleClient_vectorset)).*$$'; \ 13 | fi; \ 14 | fi 15 | 16 | .PHONY: test -------------------------------------------------------------------------------- /doctests/README.md: -------------------------------------------------------------------------------- 1 | # Command examples for redis.io 2 | 3 | These examples appear on the [Redis documentation](https://redis.io) site as part of the tabbed examples interface. 4 | 5 | ## How to add examples 6 | 7 | - Create a Go test file with a meaningful name in the current folder. 8 | - Create a single method prefixed with `Example` and write your test in it. 9 | - Determine the id for the example you're creating and add it as the first line of the file: `// EXAMPLE: set_and_get`. 10 | - We're using the [Testable Examples](https://go.dev/blog/examples) feature of Go to test the desired output has been written to stdout. 11 | 12 | ### Special markup 13 | 14 | See https://github.com/redis-stack/redis-stack-website#readme for more details. 15 | 16 | ## How to test the examples 17 | 18 | - Start a Redis server locally on port 6379 19 | - CD into the `doctests` directory 20 | - Run `go test` to test all examples in the directory. 21 | - Run `go test filename.go` to test a single file 22 | 23 | -------------------------------------------------------------------------------- /doctests/bf_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: bf_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_bloom() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:models") 27 | // REMOVE_END 28 | 29 | // STEP_START bloom 30 | res1, err := rdb.BFReserve(ctx, "bikes:models", 0.01, 1000).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.BFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // >>> true 45 | 46 | res3, err := rdb.BFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // >>> true 53 | 54 | res4, err := rdb.BFMAdd(ctx, "bikes:models", 55 | "Rocky Mountain Racer", 56 | "Cloudy City Cruiser", 57 | "Windy City Wippet", 58 | ).Result() 59 | 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | fmt.Println(res4) // >>> [true true true] 65 | 66 | res5, err := rdb.BFMExists(ctx, "bikes:models", 67 | "Rocky Mountain Racer", 68 | "Cloudy City Cruiser", 69 | "Windy City Wippet", 70 | ).Result() 71 | 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | fmt.Println(res5) // >>> [true true true] 77 | // STEP_END 78 | 79 | // Output: 80 | // OK 81 | // true 82 | // true 83 | // [true true true] 84 | // [true true true] 85 | } 86 | -------------------------------------------------------------------------------- /doctests/bitfield_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: bitfield_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_bf() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bike:1:stats") 27 | // REMOVE_END 28 | 29 | // STEP_START bf 30 | res1, err := rdb.BitField(ctx, "bike:1:stats", 31 | "set", "u32", "#0", "1000", 32 | ).Result() 33 | 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | fmt.Println(res1) // >>> [0] 39 | 40 | res2, err := rdb.BitField(ctx, 41 | "bike:1:stats", 42 | "incrby", "u32", "#0", "-50", 43 | "incrby", "u32", "#1", "1", 44 | ).Result() 45 | 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | fmt.Println(res2) // >>> [950 1] 51 | 52 | res3, err := rdb.BitField(ctx, 53 | "bike:1:stats", 54 | "incrby", "u32", "#0", "500", 55 | "incrby", "u32", "#1", "1", 56 | ).Result() 57 | 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | fmt.Println(res3) // >>> [1450 2] 63 | 64 | res4, err := rdb.BitField(ctx, "bike:1:stats", 65 | "get", "u32", "#0", 66 | "get", "u32", "#1", 67 | ).Result() 68 | 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | fmt.Println(res4) // >>> [1450 2] 74 | // STEP_END 75 | 76 | // Output: 77 | // [0] 78 | // [950 1] 79 | // [1450 2] 80 | // [1450 2] 81 | } 82 | -------------------------------------------------------------------------------- /doctests/bitmap_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: bitmap_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_ping() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "pings:2024-01-01-00:00") 27 | // REMOVE_END 28 | 29 | // STEP_START ping 30 | res1, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> 0 37 | 38 | res2, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 123).Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // >>> 1 45 | 46 | res3, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 456).Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // >>> 0 53 | // STEP_END 54 | 55 | // Output: 56 | // 0 57 | // 1 58 | // 0 59 | } 60 | 61 | func ExampleClient_bitcount() { 62 | ctx := context.Background() 63 | 64 | rdb := redis.NewClient(&redis.Options{ 65 | Addr: "localhost:6379", 66 | Password: "", // no password docs 67 | DB: 0, // use default DB 68 | }) 69 | 70 | // REMOVE_START 71 | // start with fresh database 72 | rdb.FlushDB(ctx) 73 | _, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() 74 | 75 | if err != nil { 76 | panic(err) 77 | } 78 | // REMOVE_END 79 | 80 | // STEP_START bitcount 81 | res4, err := rdb.BitCount(ctx, "pings:2024-01-01-00:00", 82 | &redis.BitCount{ 83 | Start: 0, 84 | End: 456, 85 | }).Result() 86 | 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | fmt.Println(res4) // >>> 1 92 | // STEP_END 93 | 94 | // Output: 95 | // 1 96 | } 97 | -------------------------------------------------------------------------------- /doctests/cmds_servermgmt_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cmds_servermgmt 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cmd_flushall() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // STEP_START flushall 24 | // REMOVE_START 25 | // make sure we are working with fresh database 26 | rdb.FlushDB(ctx) 27 | rdb.Set(ctx, "testkey1", "1", 0) 28 | rdb.Set(ctx, "testkey2", "2", 0) 29 | rdb.Set(ctx, "testkey3", "3", 0) 30 | // REMOVE_END 31 | flushAllResult1, err := rdb.FlushAll(ctx).Result() 32 | 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Println(flushAllResult1) // >>> OK 38 | 39 | flushAllResult2, err := rdb.Keys(ctx, "*").Result() 40 | 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | fmt.Println(flushAllResult2) // >>> [] 46 | // STEP_END 47 | 48 | // Output: 49 | // OK 50 | // [] 51 | } 52 | 53 | func ExampleClient_cmd_info() { 54 | ctx := context.Background() 55 | 56 | rdb := redis.NewClient(&redis.Options{ 57 | Addr: "localhost:6379", 58 | Password: "", // no password docs 59 | DB: 0, // use default DB 60 | }) 61 | 62 | // STEP_START info 63 | infoResult, err := rdb.Info(ctx).Result() 64 | 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | // Check the first 8 characters (the full info string contains 70 | // much more text than this). 71 | fmt.Println(infoResult[:8]) // >>> # Server 72 | // STEP_END 73 | 74 | // Output: 75 | // # Server 76 | } 77 | -------------------------------------------------------------------------------- /doctests/cmds_set_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cmds_set 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_sadd_cmd() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | rdb.Del(ctx, "myset") 25 | // REMOVE_END 26 | 27 | // STEP_START sadd 28 | sAddResult1, err := rdb.SAdd(ctx, "myset", "Hello").Result() 29 | 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | fmt.Println(sAddResult1) // >>> 1 35 | 36 | sAddResult2, err := rdb.SAdd(ctx, "myset", "World").Result() 37 | 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | fmt.Println(sAddResult2) // >>> 1 43 | 44 | sAddResult3, err := rdb.SAdd(ctx, "myset", "World").Result() 45 | 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | fmt.Println(sAddResult3) // >>> 0 51 | 52 | sMembersResult, err := rdb.SMembers(ctx, "myset").Result() 53 | 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | fmt.Println(sMembersResult) // >>> [Hello World] 59 | // STEP_END 60 | 61 | // Output: 62 | // 1 63 | // 1 64 | // 0 65 | // [Hello World] 66 | } 67 | 68 | func ExampleClient_smembers_cmd() { 69 | ctx := context.Background() 70 | 71 | rdb := redis.NewClient(&redis.Options{ 72 | Addr: "localhost:6379", 73 | Password: "", // no password docs 74 | DB: 0, // use default DB 75 | }) 76 | 77 | // REMOVE_START 78 | rdb.Del(ctx, "myset") 79 | // REMOVE_END 80 | 81 | // STEP_START smembers 82 | sAddResult, err := rdb.SAdd(ctx, "myset", "Hello", "World").Result() 83 | 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | fmt.Println(sAddResult) // >>> 2 89 | 90 | sMembersResult, err := rdb.SMembers(ctx, "myset").Result() 91 | 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | fmt.Println(sMembersResult) // >>> [Hello World] 97 | // STEP_END 98 | 99 | // Output: 100 | // 2 101 | // [Hello World] 102 | } 103 | -------------------------------------------------------------------------------- /doctests/cmds_string_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cmds_string 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cmd_incr() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "mykey") 27 | // REMOVE_END 28 | 29 | // STEP_START incr 30 | incrResult1, err := rdb.Set(ctx, "mykey", "10", 0).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(incrResult1) // >>> OK 37 | 38 | incrResult2, err := rdb.Incr(ctx, "mykey").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(incrResult2) // >>> 11 45 | 46 | incrResult3, err := rdb.Get(ctx, "mykey").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(incrResult3) // >>> 11 53 | // STEP_END 54 | 55 | // Output: 56 | // OK 57 | // 11 58 | // 11 59 | } 60 | -------------------------------------------------------------------------------- /doctests/cms_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cms_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cms() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:profit") 27 | // REMOVE_END 28 | 29 | // STEP_START cms 30 | res1, err := rdb.CMSInitByProb(ctx, "bikes:profit", 0.001, 0.002).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.CMSIncrBy(ctx, "bikes:profit", 39 | "Smoky Mountain Striker", 100, 40 | ).Result() 41 | 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | fmt.Println(res2) // >>> [100] 47 | 48 | res3, err := rdb.CMSIncrBy(ctx, "bikes:profit", 49 | "Rocky Mountain Racer", 200, 50 | "Cloudy City Cruiser", 150, 51 | ).Result() 52 | 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | fmt.Println(res3) // >>> [200 150] 58 | 59 | res4, err := rdb.CMSQuery(ctx, "bikes:profit", 60 | "Smoky Mountain Striker", 61 | ).Result() 62 | 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | fmt.Println(res4) // >>> [100] 68 | 69 | res5, err := rdb.CMSInfo(ctx, "bikes:profit").Result() 70 | 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | fmt.Printf("Width: %v, Depth: %v, Count: %v", 76 | res5.Width, res5.Depth, res5.Count) 77 | // >>> Width: 2000, Depth: 9, Count: 450 78 | // STEP_END 79 | 80 | // Output: 81 | // OK 82 | // [100] 83 | // [200 150] 84 | // [100] 85 | // Width: 2000, Depth: 9, Count: 450 86 | } 87 | -------------------------------------------------------------------------------- /doctests/cuckoo_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cuckoo_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cuckoo() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:models") 27 | // REMOVE_END 28 | 29 | // STEP_START cuckoo 30 | res1, err := rdb.CFReserve(ctx, "bikes:models", 1000000).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.CFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // >>> true 45 | 46 | res3, err := rdb.CFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // >>> true 53 | 54 | res4, err := rdb.CFExists(ctx, "bikes:models", "Terrible Bike Name").Result() 55 | 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | fmt.Println(res4) // >>> false 61 | 62 | res5, err := rdb.CFDel(ctx, "bikes:models", "Smoky Mountain Striker").Result() 63 | 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | fmt.Println(res5) // >>> true 69 | // STEP_END 70 | 71 | // Output: 72 | // OK 73 | // true 74 | // true 75 | // false 76 | // true 77 | } 78 | -------------------------------------------------------------------------------- /doctests/geo_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: geo_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_geoadd() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:rentable") 27 | // REMOVE_END 28 | 29 | // STEP_START geoadd 30 | res1, err := rdb.GeoAdd(ctx, "bikes:rentable", 31 | &redis.GeoLocation{ 32 | Longitude: -122.27652, 33 | Latitude: 37.805186, 34 | Name: "station:1", 35 | }).Result() 36 | 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | fmt.Println(res1) // >>> 1 42 | 43 | res2, err := rdb.GeoAdd(ctx, "bikes:rentable", 44 | &redis.GeoLocation{ 45 | Longitude: -122.2674626, 46 | Latitude: 37.8062344, 47 | Name: "station:2", 48 | }).Result() 49 | 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | fmt.Println(res2) // >>> 1 55 | 56 | res3, err := rdb.GeoAdd(ctx, "bikes:rentable", 57 | &redis.GeoLocation{ 58 | Longitude: -122.2469854, 59 | Latitude: 37.8104049, 60 | Name: "station:3", 61 | }).Result() 62 | 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | fmt.Println(res3) // >>> 1 68 | // STEP_END 69 | 70 | // Output: 71 | // 1 72 | // 1 73 | // 1 74 | } 75 | 76 | func ExampleClient_geosearch() { 77 | ctx := context.Background() 78 | 79 | rdb := redis.NewClient(&redis.Options{ 80 | Addr: "localhost:6379", 81 | Password: "", // no password docs 82 | DB: 0, // use default DB 83 | }) 84 | 85 | // REMOVE_START 86 | // start with fresh database 87 | rdb.FlushDB(ctx) 88 | rdb.Del(ctx, "bikes:rentable") 89 | 90 | _, err := rdb.GeoAdd(ctx, "bikes:rentable", 91 | &redis.GeoLocation{ 92 | Longitude: -122.27652, 93 | Latitude: 37.805186, 94 | Name: "station:1", 95 | }).Result() 96 | 97 | if err != nil { 98 | panic(err) 99 | } 100 | 101 | _, err = rdb.GeoAdd(ctx, "bikes:rentable", 102 | &redis.GeoLocation{ 103 | Longitude: -122.2674626, 104 | Latitude: 37.8062344, 105 | Name: "station:2", 106 | }).Result() 107 | 108 | if err != nil { 109 | panic(err) 110 | } 111 | 112 | _, err = rdb.GeoAdd(ctx, "bikes:rentable", 113 | &redis.GeoLocation{ 114 | Longitude: -122.2469854, 115 | Latitude: 37.8104049, 116 | Name: "station:3", 117 | }).Result() 118 | 119 | if err != nil { 120 | panic(err) 121 | } 122 | // REMOVE_END 123 | 124 | // STEP_START geosearch 125 | res4, err := rdb.GeoSearch(ctx, "bikes:rentable", 126 | &redis.GeoSearchQuery{ 127 | Longitude: -122.27652, 128 | Latitude: 37.805186, 129 | Radius: 5, 130 | RadiusUnit: "km", 131 | }, 132 | ).Result() 133 | 134 | if err != nil { 135 | panic(err) 136 | } 137 | 138 | fmt.Println(res4) // >>> [station:1 station:2 station:3] 139 | // STEP_END 140 | 141 | // Output: 142 | // [station:1 station:2 station:3] 143 | } 144 | -------------------------------------------------------------------------------- /doctests/hll_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: hll_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_pfadd() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes", "commuter_bikes", "all_bikes") 27 | // REMOVE_END 28 | 29 | // STEP_START pfadd 30 | res1, err := rdb.PFAdd(ctx, "bikes", "Hyperion", "Deimos", "Phoebe", "Quaoar").Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // 1 37 | 38 | res2, err := rdb.PFCount(ctx, "bikes").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // 4 45 | 46 | res3, err := rdb.PFAdd(ctx, "commuter_bikes", "Salacia", "Mimas", "Quaoar").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // 1 53 | 54 | res4, err := rdb.PFMerge(ctx, "all_bikes", "bikes", "commuter_bikes").Result() 55 | 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | fmt.Println(res4) // OK 61 | 62 | res5, err := rdb.PFCount(ctx, "all_bikes").Result() 63 | 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | fmt.Println(res5) // 6 69 | // STEP_END 70 | 71 | // Output: 72 | // 1 73 | // 4 74 | // 1 75 | // OK 76 | // 6 77 | } 78 | -------------------------------------------------------------------------------- /doctests/lpush_lrange_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: lpush_and_lrange 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | func ExampleClient_LPush_and_lrange() { 14 | ctx := context.Background() 15 | 16 | rdb := redis.NewClient(&redis.Options{ 17 | Addr: "localhost:6379", 18 | Password: "", // no password docs 19 | DB: 0, // use default DB 20 | }) 21 | 22 | // HIDE_END 23 | 24 | // REMOVE_START 25 | errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test 26 | if errFlush != nil { 27 | panic(errFlush) 28 | } 29 | // REMOVE_END 30 | 31 | listSize, err := rdb.LPush(ctx, "my_bikes", "bike:1", "bike:2").Result() 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(listSize) 37 | time.Sleep(10 * time.Millisecond) // Simulate some delay 38 | 39 | value, err := rdb.LRange(ctx, "my_bikes", 0, -1).Result() 40 | if err != nil { 41 | panic(err) 42 | } 43 | fmt.Println(value) 44 | // HIDE_START 45 | 46 | // Output: 2 47 | // [bike:2 bike:1] 48 | } 49 | 50 | // HIDE_END 51 | -------------------------------------------------------------------------------- /doctests/main_test.go: -------------------------------------------------------------------------------- 1 | package example_commands_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var RedisVersion float64 11 | 12 | func init() { 13 | // read REDIS_VERSION from env 14 | RedisVersion, _ = strconv.ParseFloat(strings.Trim(os.Getenv("REDIS_VERSION"), "\""), 64) 15 | fmt.Printf("REDIS_VERSION: %.1f\n", RedisVersion) 16 | } 17 | -------------------------------------------------------------------------------- /doctests/set_get_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: set_and_get 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | func ExampleClient_Set_and_get() { 13 | ctx := context.Background() 14 | 15 | rdb := redis.NewClient(&redis.Options{ 16 | Addr: "localhost:6379", 17 | Password: "", // no password docs 18 | DB: 0, // use default DB 19 | }) 20 | 21 | // HIDE_END 22 | 23 | // REMOVE_START 24 | // start with fresh database 25 | rdb.FlushDB(ctx) 26 | errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test 27 | if errFlush != nil { 28 | panic(errFlush) 29 | } 30 | // REMOVE_END 31 | 32 | err := rdb.Set(ctx, "bike:1", "Process 134", 0).Err() 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Println("OK") 38 | 39 | value, err := rdb.Get(ctx, "bike:1").Result() 40 | if err != nil { 41 | panic(err) 42 | } 43 | fmt.Printf("The name of the bike is %s", value) 44 | // HIDE_START 45 | 46 | // Output: OK 47 | // The name of the bike is Process 134 48 | } 49 | 50 | // HIDE_END 51 | -------------------------------------------------------------------------------- /doctests/topk_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: topk_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_topk() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // start with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:keywords") 27 | // REMOVE_END 28 | 29 | // STEP_START topk 30 | res1, err := rdb.TopKReserve(ctx, "bikes:keywords", 5).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.TopKAdd(ctx, "bikes:keywords", 39 | "store", 40 | "seat", 41 | "handlebars", 42 | "handles", 43 | "pedals", 44 | "tires", 45 | "store", 46 | "seat", 47 | ).Result() 48 | 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | fmt.Println(res2) // >>> [ handlebars ] 54 | 55 | res3, err := rdb.TopKList(ctx, "bikes:keywords").Result() 56 | 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | fmt.Println(res3) // [store seat pedals tires handles] 62 | 63 | res4, err := rdb.TopKQuery(ctx, "bikes:keywords", "store", "handlebars").Result() 64 | 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | fmt.Println(res4) // [true false] 70 | // STEP_END 71 | 72 | // Output: 73 | // OK 74 | // [ handlebars ] 75 | // [store seat pedals tires handles] 76 | // [true false] 77 | } 78 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | . "github.com/bsm/ginkgo/v2" 9 | . "github.com/bsm/gomega" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | type testTimeout struct { 14 | timeout bool 15 | } 16 | 17 | func (t testTimeout) Timeout() bool { 18 | return t.timeout 19 | } 20 | 21 | func (t testTimeout) Error() string { 22 | return "test timeout" 23 | } 24 | 25 | var _ = Describe("error", func() { 26 | BeforeEach(func() { 27 | 28 | }) 29 | 30 | AfterEach(func() { 31 | 32 | }) 33 | 34 | It("should retry", func() { 35 | data := map[error]bool{ 36 | io.EOF: true, 37 | io.ErrUnexpectedEOF: true, 38 | nil: false, 39 | context.Canceled: false, 40 | context.DeadlineExceeded: false, 41 | redis.ErrPoolTimeout: true, 42 | errors.New("ERR max number of clients reached"): true, 43 | errors.New("LOADING Redis is loading the dataset in memory"): true, 44 | errors.New("READONLY You can't write against a read only replica"): true, 45 | errors.New("CLUSTERDOWN The cluster is down"): true, 46 | errors.New("TRYAGAIN Command cannot be processed, please try again"): true, 47 | errors.New("other"): false, 48 | } 49 | 50 | for err, expected := range data { 51 | Expect(redis.ShouldRetry(err, false)).To(Equal(expected)) 52 | Expect(redis.ShouldRetry(err, true)).To(Equal(expected)) 53 | } 54 | }) 55 | 56 | It("should retry timeout", func() { 57 | t1 := testTimeout{timeout: true} 58 | Expect(redis.ShouldRetry(t1, true)).To(Equal(true)) 59 | Expect(redis.ShouldRetry(t1, false)).To(Equal(false)) 60 | 61 | t2 := testTimeout{timeout: false} 62 | Expect(redis.ShouldRetry(t2, true)).To(Equal(true)) 63 | Expect(redis.ShouldRetry(t2, false)).To(Equal(true)) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/README.md: -------------------------------------------------------------------------------- 1 | # Delete keys without a ttl 2 | 3 | This example demonstrates how to use `SCAN` and pipelines to efficiently delete keys without a TTL. 4 | 5 | To run this example: 6 | 7 | ```shell 8 | go run . 9 | ``` 10 | 11 | See [documentation](https://redis.uptrace.dev/guide/get-all-keys.html) for more details. 12 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/del-keys-without-ttl 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/redis/go-redis/v9 v9.10.0 9 | go.uber.org/zap v1.24.0 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | go.uber.org/atomic v1.10.0 // indirect 16 | go.uber.org/multierr v1.9.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 5 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 12 | go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 13 | go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 14 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 15 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 16 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 17 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 18 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "go.uber.org/zap" 10 | 11 | "github.com/redis/go-redis/v9" 12 | ) 13 | 14 | func main() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: ":6379", 19 | }) 20 | 21 | _ = rdb.Set(ctx, "key_with_ttl", "bar", time.Minute).Err() 22 | _ = rdb.Set(ctx, "key_without_ttl_1", "", 0).Err() 23 | _ = rdb.Set(ctx, "key_without_ttl_2", "", 0).Err() 24 | 25 | checker := NewKeyChecker(rdb, 100) 26 | 27 | start := time.Now() 28 | checker.Start(ctx) 29 | 30 | iter := rdb.Scan(ctx, 0, "", 0).Iterator() 31 | for iter.Next(ctx) { 32 | checker.Add(iter.Val()) 33 | } 34 | if err := iter.Err(); err != nil { 35 | panic(err) 36 | } 37 | 38 | deleted := checker.Stop() 39 | fmt.Println("deleted", deleted, "keys", "in", time.Since(start)) 40 | } 41 | 42 | type KeyChecker struct { 43 | rdb *redis.Client 44 | batchSize int 45 | ch chan string 46 | delCh chan string 47 | wg sync.WaitGroup 48 | deleted int 49 | logger *zap.Logger 50 | } 51 | 52 | func NewKeyChecker(rdb *redis.Client, batchSize int) *KeyChecker { 53 | return &KeyChecker{ 54 | rdb: rdb, 55 | batchSize: batchSize, 56 | ch: make(chan string, batchSize), 57 | delCh: make(chan string, batchSize), 58 | logger: zap.L(), 59 | } 60 | } 61 | 62 | func (c *KeyChecker) Add(key string) { 63 | c.ch <- key 64 | } 65 | 66 | func (c *KeyChecker) Start(ctx context.Context) { 67 | c.wg.Add(1) 68 | go func() { 69 | defer c.wg.Done() 70 | if err := c.del(ctx); err != nil { 71 | panic(err) 72 | } 73 | }() 74 | 75 | c.wg.Add(1) 76 | go func() { 77 | defer c.wg.Done() 78 | defer close(c.delCh) 79 | 80 | keys := make([]string, 0, c.batchSize) 81 | 82 | for key := range c.ch { 83 | keys = append(keys, key) 84 | if len(keys) < cap(keys) { 85 | continue 86 | } 87 | 88 | if err := c.checkKeys(ctx, keys); err != nil { 89 | c.logger.Error("checkKeys failed", zap.Error(err)) 90 | } 91 | keys = keys[:0] 92 | } 93 | 94 | if len(keys) > 0 { 95 | if err := c.checkKeys(ctx, keys); err != nil { 96 | c.logger.Error("checkKeys failed", zap.Error(err)) 97 | } 98 | keys = nil 99 | } 100 | }() 101 | } 102 | 103 | func (c *KeyChecker) Stop() int { 104 | close(c.ch) 105 | c.wg.Wait() 106 | return c.deleted 107 | } 108 | 109 | func (c *KeyChecker) checkKeys(ctx context.Context, keys []string) error { 110 | cmds, err := c.rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { 111 | for _, key := range keys { 112 | pipe.TTL(ctx, key) 113 | } 114 | return nil 115 | }) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | for i, cmd := range cmds { 121 | d, err := cmd.(*redis.DurationCmd).Result() 122 | if err != nil { 123 | return err 124 | } 125 | if d == -1 { 126 | c.delCh <- keys[i] 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func (c *KeyChecker) del(ctx context.Context) error { 134 | pipe := c.rdb.Pipeline() 135 | 136 | for key := range c.delCh { 137 | fmt.Printf("deleting %s...\n", key) 138 | pipe.Del(ctx, key) 139 | c.deleted++ 140 | 141 | if pipe.Len() < c.batchSize { 142 | continue 143 | } 144 | 145 | if _, err := pipe.Exec(ctx); err != nil { 146 | return err 147 | } 148 | } 149 | 150 | if _, err := pipe.Exec(ctx); err != nil { 151 | return err 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /example/hll/README.md: -------------------------------------------------------------------------------- 1 | # Redis HyperLogLog example 2 | 3 | To run this example: 4 | 5 | ```shell 6 | go run . 7 | ``` 8 | 9 | See [Using HyperLogLog command with go-redis](https://redis.uptrace.dev/guide/go-redis-hll.html) for 10 | details. 11 | -------------------------------------------------------------------------------- /example/hll/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/hll 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.10.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /example/hll/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | -------------------------------------------------------------------------------- /example/hll/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | 13 | rdb := redis.NewClient(&redis.Options{ 14 | Addr: ":6379", 15 | }) 16 | _ = rdb.FlushDB(ctx).Err() 17 | 18 | for i := 0; i < 10; i++ { 19 | if err := rdb.PFAdd(ctx, "myset", fmt.Sprint(i)).Err(); err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | card, err := rdb.PFCount(ctx, "myset").Result() 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | fmt.Println("set cardinality", card) 30 | } 31 | -------------------------------------------------------------------------------- /example/hset-struct/README.md: -------------------------------------------------------------------------------- 1 | # Example for setting struct fields as hash fields 2 | 3 | To run this example: 4 | 5 | ```shell 6 | go run . 7 | ``` 8 | -------------------------------------------------------------------------------- /example/hset-struct/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/scan-struct 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/redis/go-redis/v9 v9.10.0 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /example/hset-struct/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | -------------------------------------------------------------------------------- /example/hset-struct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | type Model struct { 13 | Str1 string `redis:"str1"` 14 | Str2 string `redis:"str2"` 15 | Str3 *string `redis:"str3"` 16 | Str4 *string `redis:"str4"` 17 | Bytes []byte `redis:"bytes"` 18 | Int int `redis:"int"` 19 | Int2 *int `redis:"int2"` 20 | Int3 *int `redis:"int3"` 21 | Bool bool `redis:"bool"` 22 | Bool2 *bool `redis:"bool2"` 23 | Bool3 *bool `redis:"bool3"` 24 | Bool4 *bool `redis:"bool4,omitempty"` 25 | Time time.Time `redis:"time"` 26 | Time2 *time.Time `redis:"time2"` 27 | Time3 *time.Time `redis:"time3"` 28 | Ignored struct{} `redis:"-"` 29 | } 30 | 31 | func main() { 32 | ctx := context.Background() 33 | 34 | rdb := redis.NewClient(&redis.Options{ 35 | Addr: ":6379", 36 | }) 37 | 38 | _ = rdb.FlushDB(ctx).Err() 39 | 40 | t := time.Date(2025, 02, 8, 0, 0, 0, 0, time.UTC) 41 | 42 | data := Model{ 43 | Str1: "hello", 44 | Str2: "world", 45 | Str3: ToPtr("hello"), 46 | Str4: nil, 47 | Bytes: []byte("this is bytes !"), 48 | Int: 123, 49 | Int2: ToPtr(0), 50 | Int3: nil, 51 | Bool: true, 52 | Bool2: ToPtr(false), 53 | Bool3: nil, 54 | Time: t, 55 | Time2: ToPtr(t), 56 | Time3: nil, 57 | Ignored: struct{}{}, 58 | } 59 | 60 | // Set some fields. 61 | if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { 62 | rdb.HMSet(ctx, "key", data) 63 | return nil 64 | }); err != nil { 65 | panic(err) 66 | } 67 | 68 | var model1, model2 Model 69 | 70 | // Scan all fields into the model. 71 | if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil { 72 | panic(err) 73 | } 74 | 75 | // Or scan a subset of the fields. 76 | if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil { 77 | panic(err) 78 | } 79 | 80 | spew.Dump(model1) 81 | // Output: 82 | // (main.Model) { 83 | // Str1: (string) (len=5) "hello", 84 | // Str2: (string) (len=5) "world", 85 | // Str3: (*string)(0xc000016970)((len=5) "hello"), 86 | // Str4: (*string)(0xc000016980)(""), 87 | // Bytes: ([]uint8) (len=15 cap=16) { 88 | // 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !| 89 | // }, 90 | // Int: (int) 123, 91 | // Int2: (*int)(0xc000014568)(0), 92 | // Int3: (*int)(0xc000014560)(0), 93 | // Bool: (bool) true, 94 | // Bool2: (*bool)(0xc000014570)(false), 95 | // Bool3: (*bool)(0xc000014548)(false), 96 | // Bool4: (*bool)(), 97 | // Time: (time.Time) 2025-02-08 00:00:00 +0000 UTC, 98 | // Time2: (*time.Time)(0xc0000122a0)(2025-02-08 00:00:00 +0000 UTC), 99 | // Time3: (*time.Time)(0xc000012288)(0001-01-01 00:00:00 +0000 UTC), 100 | // Ignored: (struct {}) { 101 | // } 102 | // } 103 | 104 | spew.Dump(model2) 105 | // Output: 106 | // (main.Model) { 107 | // Str1: (string) (len=5) "hello", 108 | // Str2: (string) "", 109 | // Str3: (*string)(), 110 | // Str4: (*string)(), 111 | // Bytes: ([]uint8) , 112 | // Int: (int) 123, 113 | // Int2: (*int)(), 114 | // Int3: (*int)(), 115 | // Bool: (bool) false, 116 | // Bool2: (*bool)(), 117 | // Bool3: (*bool)(), 118 | // Bool4: (*bool)(), 119 | // Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, 120 | // Time2: (*time.Time)(), 121 | // Time3: (*time.Time)(), 122 | // Ignored: (struct {}) { 123 | // } 124 | // } 125 | } 126 | 127 | func ToPtr[T any](v T) *T { 128 | return &v 129 | } 130 | -------------------------------------------------------------------------------- /example/lua-scripting/README.md: -------------------------------------------------------------------------------- 1 | # Redis Lua scripting example 2 | 3 | This is an example for [Redis Lua scripting](https://redis.uptrace.dev/guide/lua-scripting.html) 4 | article. To run it: 5 | 6 | ```shell 7 | go run . 8 | ``` 9 | -------------------------------------------------------------------------------- /example/lua-scripting/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/lua-scripting 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.10.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /example/lua-scripting/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | -------------------------------------------------------------------------------- /example/lua-scripting/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | 13 | rdb := redis.NewClient(&redis.Options{ 14 | Addr: ":6379", 15 | }) 16 | _ = rdb.FlushDB(ctx).Err() 17 | 18 | fmt.Printf("# INCR BY\n") 19 | for _, change := range []int{+1, +5, 0} { 20 | num, err := incrBy.Run(ctx, rdb, []string{"my_counter"}, change).Int() 21 | if err != nil { 22 | panic(err) 23 | } 24 | fmt.Printf("incr by %d: %d\n", change, num) 25 | } 26 | 27 | fmt.Printf("\n# SUM\n") 28 | sum, err := sum.Run(ctx, rdb, []string{"my_sum"}, 1, 2, 3).Int() 29 | if err != nil { 30 | panic(err) 31 | } 32 | fmt.Printf("sum is: %d\n", sum) 33 | } 34 | 35 | var incrBy = redis.NewScript(` 36 | local key = KEYS[1] 37 | local change = ARGV[1] 38 | 39 | local value = redis.call("GET", key) 40 | if not value then 41 | value = 0 42 | end 43 | 44 | value = value + change 45 | redis.call("SET", key, value) 46 | 47 | return value 48 | `) 49 | 50 | var sum = redis.NewScript(` 51 | local key = KEYS[1] 52 | 53 | local sum = redis.call("GET", key) 54 | if not sum then 55 | sum = 0 56 | end 57 | 58 | local num_arg = #ARGV 59 | for i = 1, num_arg do 60 | sum = sum + ARGV[i] 61 | end 62 | 63 | redis.call("SET", key, sum) 64 | 65 | return sum 66 | `) 67 | -------------------------------------------------------------------------------- /example/otel/README.md: -------------------------------------------------------------------------------- 1 | # Example for go-redis OpenTelemetry instrumentation 2 | 3 | This example demonstrates how to monitor Redis using OpenTelemetry and 4 | [Uptrace](https://github.com/uptrace/uptrace). It requires Docker to start Redis Server and Uptrace. 5 | 6 | See 7 | [Monitoring Go Redis Performance and Errors](https://redis.uptrace.dev/guide/go-redis-monitoring.html) 8 | for details. 9 | 10 | **Step 1**. Download the example using Git: 11 | 12 | ```shell 13 | git clone https://github.com/redis/go-redis.git 14 | cd example/otel 15 | ``` 16 | 17 | **Step 2**. Start the services using Docker: 18 | 19 | ```shell 20 | docker-compose up -d 21 | ``` 22 | 23 | **Step 3**. Make sure Uptrace is running: 24 | 25 | ```shell 26 | docker-compose logs uptrace 27 | ``` 28 | 29 | **Step 4**. Run the Redis client example and Follow the link to view the trace: 30 | 31 | ```shell 32 | go run client.go 33 | trace: http://localhost:14318/traces/ee029d8782242c8ed38b16d961093b35 34 | ``` 35 | 36 | ![Redis trace](./image/redis-trace.png) 37 | 38 | You can also open Uptrace UI at [http://localhost:14318](http://localhost:14318) to view available 39 | spans, logs, and metrics. 40 | 41 | ## Redis monitoring 42 | 43 | You can also [monitor Redis performance](https://uptrace.dev/opentelemetry/redis-monitoring.html) 44 | metrics By installing OpenTelemetry Collector. 45 | 46 | [OpenTelemetry Collector](https://uptrace.dev/opentelemetry/collector.html) is an agent that pulls 47 | telemetry data from systems you want to monitor and sends it to APM tools using the OpenTelemetry 48 | protocol (OTLP). 49 | 50 | When telemetry data reaches Uptrace, it automatically generates a Redis dashboard from a pre-defined 51 | template. 52 | 53 | ![Redis dashboard](./image/metrics.png) 54 | 55 | ## Links 56 | 57 | - [Uptrace open-source APM](https://uptrace.dev/get/open-source-apm.html) 58 | - [OpenTelemetry Go instrumentations](https://uptrace.dev/opentelemetry/instrumentations/?lang=go) 59 | - [OpenTelemetry Go Tracing API](https://uptrace.dev/opentelemetry/go-tracing.html) 60 | -------------------------------------------------------------------------------- /example/otel/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "github.com/uptrace/uptrace-go/uptrace" 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/codes" 13 | 14 | "github.com/redis/go-redis/extra/redisotel/v9" 15 | "github.com/redis/go-redis/v9" 16 | ) 17 | 18 | var tracer = otel.Tracer("github.com/redis/go-redis/example/otel") 19 | 20 | func main() { 21 | ctx := context.Background() 22 | 23 | uptrace.ConfigureOpentelemetry( 24 | // copy your project DSN here or use UPTRACE_DSN env var 25 | uptrace.WithDSN("http://project2_secret_token@localhost:14317/2"), 26 | 27 | uptrace.WithServiceName("myservice"), 28 | uptrace.WithServiceVersion("v1.0.0"), 29 | ) 30 | defer uptrace.Shutdown(ctx) 31 | 32 | rdb := redis.NewClient(&redis.Options{ 33 | Addr: ":6379", 34 | }) 35 | if err := redisotel.InstrumentTracing(rdb); err != nil { 36 | panic(err) 37 | } 38 | if err := redisotel.InstrumentMetrics(rdb); err != nil { 39 | panic(err) 40 | } 41 | 42 | for i := 0; i < 1e6; i++ { 43 | ctx, rootSpan := tracer.Start(ctx, "handleRequest") 44 | 45 | if err := handleRequest(ctx, rdb); err != nil { 46 | rootSpan.RecordError(err) 47 | rootSpan.SetStatus(codes.Error, err.Error()) 48 | } 49 | 50 | rootSpan.End() 51 | 52 | if i == 0 { 53 | fmt.Printf("view trace: %s\n", uptrace.TraceURL(rootSpan)) 54 | } 55 | 56 | time.Sleep(time.Second) 57 | } 58 | } 59 | 60 | func handleRequest(ctx context.Context, rdb *redis.Client) error { 61 | if err := rdb.Set(ctx, "First value", "value_1", 0).Err(); err != nil { 62 | return err 63 | } 64 | if err := rdb.Set(ctx, "Second value", "value_2", 0).Err(); err != nil { 65 | return err 66 | } 67 | 68 | var group sync.WaitGroup 69 | 70 | for i := 0; i < 20; i++ { 71 | group.Add(1) 72 | go func() { 73 | defer group.Done() 74 | val := rdb.Get(ctx, "Second value").Val() 75 | if val != "value_2" { 76 | log.Printf("%q != %q", val, "value_2") 77 | } 78 | }() 79 | } 80 | 81 | group.Wait() 82 | 83 | if err := rdb.Del(ctx, "First value").Err(); err != nil { 84 | return err 85 | } 86 | if err := rdb.Del(ctx, "Second value").Err(); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /example/otel/config/otel-collector.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | health_check: 3 | pprof: 4 | endpoint: 0.0.0.0:1777 5 | zpages: 6 | endpoint: 0.0.0.0:55679 7 | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | http: 13 | hostmetrics: 14 | collection_interval: 10s 15 | scrapers: 16 | cpu: 17 | disk: 18 | load: 19 | filesystem: 20 | memory: 21 | network: 22 | paging: 23 | redis: 24 | endpoint: 'redis-server:6379' 25 | collection_interval: 10s 26 | jaeger: 27 | protocols: 28 | grpc: 29 | 30 | processors: 31 | resourcedetection: 32 | detectors: ['system'] 33 | cumulativetodelta: 34 | batch: 35 | send_batch_size: 10000 36 | timeout: 10s 37 | 38 | exporters: 39 | otlp/uptrace: 40 | endpoint: http://uptrace:14317 41 | tls: 42 | insecure: true 43 | headers: { 'uptrace-dsn': 'http://project2_secret_token@localhost:14317/2' } 44 | debug: 45 | 46 | service: 47 | # telemetry: 48 | # logs: 49 | # level: DEBUG 50 | pipelines: 51 | traces: 52 | receivers: [otlp, jaeger] 53 | processors: [batch] 54 | exporters: [otlp/uptrace] 55 | metrics: 56 | receivers: [otlp] 57 | processors: [cumulativetodelta, batch] 58 | exporters: [otlp/uptrace] 59 | metrics/hostmetrics: 60 | receivers: [hostmetrics, redis] 61 | processors: [cumulativetodelta, batch, resourcedetection] 62 | exporters: [otlp/uptrace] 63 | logs: 64 | receivers: [otlp] 65 | processors: [batch] 66 | exporters: [otlp/uptrace] 67 | 68 | extensions: [health_check, pprof, zpages] 69 | -------------------------------------------------------------------------------- /example/otel/config/vector.toml: -------------------------------------------------------------------------------- 1 | [sources.syslog_logs] 2 | type = "demo_logs" 3 | format = "syslog" 4 | 5 | [sources.apache_common_logs] 6 | type = "demo_logs" 7 | format = "apache_common" 8 | 9 | [sources.apache_error_logs] 10 | type = "demo_logs" 11 | format = "apache_error" 12 | 13 | [sources.json_logs] 14 | type = "demo_logs" 15 | format = "json" 16 | 17 | # Parse Syslog logs 18 | # See the Vector Remap Language reference for more info: https://vrl.dev 19 | [transforms.parse_logs] 20 | type = "remap" 21 | inputs = ["syslog_logs"] 22 | source = ''' 23 | . = parse_syslog!(string!(.message)) 24 | ''' 25 | 26 | # Export data to Uptrace. 27 | [sinks.uptrace] 28 | type = "http" 29 | inputs = ["parse_logs", "apache_common_logs", "apache_error_logs", "json_logs"] 30 | encoding.codec = "json" 31 | framing.method = "newline_delimited" 32 | compression = "gzip" 33 | uri = "http://uptrace:14318/api/v1/vector/logs" 34 | #uri = "https://api.uptrace.dev/api/v1/vector/logs" 35 | headers.uptrace-dsn = "http://project2_secret_token@localhost:14317/2" 36 | -------------------------------------------------------------------------------- /example/otel/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | clickhouse: 5 | image: clickhouse/clickhouse-server:23.7 6 | restart: on-failure 7 | environment: 8 | CLICKHOUSE_DB: uptrace 9 | healthcheck: 10 | test: ['CMD', 'wget', '--spider', '-q', 'localhost:8123/ping'] 11 | interval: 1s 12 | timeout: 1s 13 | retries: 30 14 | volumes: 15 | - ch_data2:/var/lib/clickhouse 16 | ports: 17 | - '8123:8123' 18 | - '9000:9000' 19 | 20 | postgres: 21 | image: postgres:15-alpine 22 | restart: on-failure 23 | environment: 24 | PGDATA: /var/lib/postgresql/data/pgdata 25 | POSTGRES_USER: uptrace 26 | POSTGRES_PASSWORD: uptrace 27 | POSTGRES_DB: uptrace 28 | healthcheck: 29 | test: ['CMD-SHELL', 'pg_isready', '-U', 'uptrace', '-d', 'uptrace'] 30 | interval: 1s 31 | timeout: 1s 32 | retries: 30 33 | volumes: 34 | - 'pg_data2:/var/lib/postgresql/data/pgdata' 35 | ports: 36 | - '5432:5432' 37 | 38 | uptrace: 39 | image: 'uptrace/uptrace:1.6.2' 40 | #image: 'uptrace/uptrace-dev:latest' 41 | restart: on-failure 42 | volumes: 43 | - ./uptrace.yml:/etc/uptrace/uptrace.yml 44 | #environment: 45 | # - DEBUG=2 46 | ports: 47 | - '14317:14317' 48 | - '14318:14318' 49 | depends_on: 50 | clickhouse: 51 | condition: service_healthy 52 | 53 | otelcol: 54 | image: otel/opentelemetry-collector-contrib:0.91.0 55 | restart: on-failure 56 | volumes: 57 | - ./config/otel-collector.yaml:/etc/otelcol-contrib/config.yaml 58 | ports: 59 | - '4317:4317' 60 | - '4318:4318' 61 | 62 | vector: 63 | image: timberio/vector:0.28.X-alpine 64 | volumes: 65 | - ./config/vector.toml:/etc/vector/vector.toml:ro 66 | 67 | mailhog: 68 | image: mailhog/mailhog:v1.0.1 69 | restart: on-failure 70 | ports: 71 | - '8025:8025' 72 | 73 | redis-server: 74 | image: redis 75 | ports: 76 | - '6379:6379' 77 | redis-cli: 78 | image: redis 79 | 80 | volumes: 81 | ch_data2: 82 | pg_data2: 83 | -------------------------------------------------------------------------------- /example/otel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/otel 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | replace github.com/redis/go-redis/v9 => ../.. 8 | 9 | replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel 10 | 11 | replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd 12 | 13 | require ( 14 | github.com/redis/go-redis/extra/redisotel/v9 v9.10.0 15 | github.com/redis/go-redis/v9 v9.10.0 16 | github.com/uptrace/uptrace-go v1.21.0 17 | go.opentelemetry.io/otel v1.22.0 18 | ) 19 | 20 | require ( 21 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 24 | github.com/go-logr/logr v1.4.1 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/golang/protobuf v1.5.3 // indirect 27 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect 28 | github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 // indirect 29 | go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect 30 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect 31 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect 32 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect 33 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect 34 | go.opentelemetry.io/otel/metric v1.22.0 // indirect 35 | go.opentelemetry.io/otel/sdk v1.22.0 // indirect 36 | go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect 37 | go.opentelemetry.io/otel/trace v1.22.0 // indirect 38 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 39 | golang.org/x/net v0.36.0 // indirect 40 | golang.org/x/sys v0.30.0 // indirect 41 | golang.org/x/text v0.22.0 // indirect 42 | google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect 43 | google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect 44 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect 45 | google.golang.org/grpc v1.60.1 // indirect 46 | google.golang.org/protobuf v1.33.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /example/otel/image/metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis/go-redis/75e8370a6f08f55b5337524040de9c2b95687582/example/otel/image/metrics.png -------------------------------------------------------------------------------- /example/otel/image/redis-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis/go-redis/75e8370a6f08f55b5337524040de9c2b95687582/example/otel/image/redis-trace.png -------------------------------------------------------------------------------- /example/redis-bloom/README.md: -------------------------------------------------------------------------------- 1 | # RedisBloom example for go-redis 2 | 3 | This is an example for 4 | [Bloom, Cuckoo, Count-Min, Top-K](https://redis.uptrace.dev/guide/bloom-cuckoo-count-min-top-k.html) 5 | article. 6 | 7 | To run it, you need to compile and install 8 | [RedisBloom](https://oss.redis.com/redisbloom/Quick_Start/#building) module: 9 | 10 | ```shell 11 | go run . 12 | ``` 13 | -------------------------------------------------------------------------------- /example/redis-bloom/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/redis-bloom 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.10.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /example/redis-bloom/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | -------------------------------------------------------------------------------- /example/scan-struct/README.md: -------------------------------------------------------------------------------- 1 | # Example for scanning hash fields into a struct 2 | 3 | To run this example: 4 | 5 | ```shell 6 | go run . 7 | ``` 8 | 9 | See 10 | [Redis: Scanning hash fields into a struct](https://redis.uptrace.dev/guide/scanning-hash-fields.html) 11 | for details. 12 | -------------------------------------------------------------------------------- /example/scan-struct/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/scan-struct 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/redis/go-redis/v9 v9.10.0 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /example/scan-struct/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | -------------------------------------------------------------------------------- /example/scan-struct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type Model struct { 12 | Str1 string `redis:"str1"` 13 | Str2 string `redis:"str2"` 14 | Str3 *string `redis:"str3"` 15 | Bytes []byte `redis:"bytes"` 16 | Int int `redis:"int"` 17 | Int2 *int `redis:"int2"` 18 | Bool bool `redis:"bool"` 19 | Bool2 *bool `redis:"bool2"` 20 | Ignored struct{} `redis:"-"` 21 | } 22 | 23 | func main() { 24 | ctx := context.Background() 25 | 26 | rdb := redis.NewClient(&redis.Options{ 27 | Addr: ":6379", 28 | }) 29 | _ = rdb.FlushDB(ctx).Err() 30 | 31 | // Set some fields. 32 | if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { 33 | rdb.HSet(ctx, "key", "str1", "hello") 34 | rdb.HSet(ctx, "key", "str2", "world") 35 | rdb.HSet(ctx, "key", "str3", "") 36 | rdb.HSet(ctx, "key", "int", 123) 37 | rdb.HSet(ctx, "key", "int2", 0) 38 | rdb.HSet(ctx, "key", "bool", 1) 39 | rdb.HSet(ctx, "key", "bool2", 0) 40 | rdb.HSet(ctx, "key", "bytes", []byte("this is bytes !")) 41 | return nil 42 | }); err != nil { 43 | panic(err) 44 | } 45 | 46 | var model1, model2 Model 47 | 48 | // Scan all fields into the model. 49 | if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil { 50 | panic(err) 51 | } 52 | 53 | // Or scan a subset of the fields. 54 | if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil { 55 | panic(err) 56 | } 57 | 58 | spew.Dump(model1) 59 | // Output: 60 | // (main.Model) { 61 | // Str1: (string) (len=5) "hello", 62 | // Str2: (string) (len=5) "world", 63 | // Bytes: ([]uint8) (len=15 cap=16) { 64 | // 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !| 65 | // }, 66 | // Int: (int) 123, 67 | // Bool: (bool) true, 68 | // Ignored: (struct {}) { 69 | // } 70 | // } 71 | 72 | spew.Dump(model2) 73 | // Output: 74 | // (main.Model) { 75 | // Str1: (string) (len=5) "hello", 76 | // Str2: (string) "", 77 | // Bytes: ([]uint8) , 78 | // Int: (int) 123, 79 | // Bool: (bool) false, 80 | // Ignored: (struct {}) { 81 | // } 82 | // } 83 | } 84 | -------------------------------------------------------------------------------- /example_instrumentation_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type redisHook struct{} 12 | 13 | var _ redis.Hook = redisHook{} 14 | 15 | func (redisHook) DialHook(hook redis.DialHook) redis.DialHook { 16 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 17 | fmt.Printf("dialing %s %s\n", network, addr) 18 | conn, err := hook(ctx, network, addr) 19 | fmt.Printf("finished dialing %s %s\n", network, addr) 20 | return conn, err 21 | } 22 | } 23 | 24 | func (redisHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook { 25 | return func(ctx context.Context, cmd redis.Cmder) error { 26 | fmt.Printf("starting processing: <%v>\n", cmd.Args()) 27 | err := hook(ctx, cmd) 28 | fmt.Printf("finished processing: <%v>\n", cmd.Args()) 29 | return err 30 | } 31 | } 32 | 33 | func (redisHook) ProcessPipelineHook(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook { 34 | return func(ctx context.Context, cmds []redis.Cmder) error { 35 | names := make([]string, 0, len(cmds)) 36 | for _, cmd := range cmds { 37 | names = append(names, fmt.Sprintf("%v", cmd.Args())) 38 | } 39 | fmt.Printf("pipeline starting processing: %v\n", names) 40 | err := hook(ctx, cmds) 41 | fmt.Printf("pipeline finished processing: %v\n", names) 42 | return err 43 | } 44 | } 45 | 46 | func Example_instrumentation() { 47 | rdb := redis.NewClient(&redis.Options{ 48 | Addr: ":6379", 49 | DisableIdentity: true, 50 | }) 51 | rdb.AddHook(redisHook{}) 52 | 53 | rdb.Ping(ctx) 54 | // Output: 55 | // starting processing: <[ping]> 56 | // dialing tcp :6379 57 | // finished dialing tcp :6379 58 | // starting processing: <[hello 3]> 59 | // finished processing: <[hello 3]> 60 | // finished processing: <[ping]> 61 | } 62 | 63 | func ExamplePipeline_instrumentation() { 64 | rdb := redis.NewClient(&redis.Options{ 65 | Addr: ":6379", 66 | DisableIdentity: true, 67 | }) 68 | rdb.AddHook(redisHook{}) 69 | 70 | rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { 71 | pipe.Ping(ctx) 72 | pipe.Ping(ctx) 73 | return nil 74 | }) 75 | // Output: 76 | // pipeline starting processing: [[ping] [ping]] 77 | // dialing tcp :6379 78 | // finished dialing tcp :6379 79 | // starting processing: <[hello 3]> 80 | // finished processing: <[hello 3]> 81 | // pipeline finished processing: [[ping] [ping]] 82 | } 83 | 84 | func ExampleClient_Watch_instrumentation() { 85 | rdb := redis.NewClient(&redis.Options{ 86 | Addr: ":6379", 87 | DisableIdentity: true, 88 | }) 89 | rdb.AddHook(redisHook{}) 90 | 91 | rdb.Watch(ctx, func(tx *redis.Tx) error { 92 | tx.Ping(ctx) 93 | tx.Ping(ctx) 94 | return nil 95 | }, "foo") 96 | // Output: 97 | // starting processing: <[watch foo]> 98 | // dialing tcp :6379 99 | // finished dialing tcp :6379 100 | // starting processing: <[hello 3]> 101 | // finished processing: <[hello 3]> 102 | // finished processing: <[watch foo]> 103 | // starting processing: <[ping]> 104 | // finished processing: <[ping]> 105 | // starting processing: <[ping]> 106 | // finished processing: <[ping]> 107 | // starting processing: <[unwatch]> 108 | // finished processing: <[unwatch]> 109 | } 110 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/redis/go-redis/v9/internal" 10 | "github.com/redis/go-redis/v9/internal/hashtag" 11 | "github.com/redis/go-redis/v9/internal/pool" 12 | ) 13 | 14 | func (c *baseClient) Pool() pool.Pooler { 15 | return c.connPool 16 | } 17 | 18 | func (c *PubSub) SetNetConn(netConn net.Conn) { 19 | c.cn = pool.NewConn(netConn) 20 | } 21 | 22 | func (c *ClusterClient) LoadState(ctx context.Context) (*clusterState, error) { 23 | // return c.state.Reload(ctx) 24 | return c.loadState(ctx) 25 | } 26 | 27 | func (c *ClusterClient) SlotAddrs(ctx context.Context, slot int) []string { 28 | state, err := c.state.Get(ctx) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | var addrs []string 34 | for _, n := range state.slotNodes(slot) { 35 | addrs = append(addrs, n.Client.getAddr()) 36 | } 37 | return addrs 38 | } 39 | 40 | func (c *ClusterClient) Nodes(ctx context.Context, key string) ([]*clusterNode, error) { 41 | state, err := c.state.Reload(ctx) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | slot := hashtag.Slot(key) 47 | nodes := state.slotNodes(slot) 48 | if len(nodes) != 2 { 49 | return nil, fmt.Errorf("slot=%d does not have enough nodes: %v", slot, nodes) 50 | } 51 | return nodes, nil 52 | } 53 | 54 | func (c *ClusterClient) SwapNodes(ctx context.Context, key string) error { 55 | nodes, err := c.Nodes(ctx, key) 56 | if err != nil { 57 | return err 58 | } 59 | nodes[0], nodes[1] = nodes[1], nodes[0] 60 | return nil 61 | } 62 | 63 | func (c *clusterState) IsConsistent(ctx context.Context) bool { 64 | if len(c.Masters) < 3 { 65 | return false 66 | } 67 | for _, master := range c.Masters { 68 | s := master.Client.Info(ctx, "replication").Val() 69 | if !strings.Contains(s, "role:master") { 70 | return false 71 | } 72 | } 73 | 74 | if len(c.Slaves) < 3 { 75 | return false 76 | } 77 | for _, slave := range c.Slaves { 78 | s := slave.Client.Info(ctx, "replication").Val() 79 | if !strings.Contains(s, "role:slave") { 80 | return false 81 | } 82 | } 83 | 84 | return true 85 | } 86 | 87 | func GetSlavesAddrByName(ctx context.Context, c *SentinelClient, name string) []string { 88 | addrs, err := c.Replicas(ctx, name).Result() 89 | if err != nil { 90 | internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s", 91 | name, err) 92 | return []string{} 93 | } 94 | return parseReplicaAddrs(addrs, false) 95 | } 96 | 97 | func (c *Ring) ShardByName(name string) *ringShard { 98 | shard, _ := c.sharding.GetByName(name) 99 | return shard 100 | } 101 | 102 | func (c *ModuleLoadexConfig) ToArgs() []interface{} { 103 | return c.toArgs() 104 | } 105 | 106 | func ShouldRetry(err error, retryTimeout bool) bool { 107 | return shouldRetry(err, retryTimeout) 108 | } 109 | -------------------------------------------------------------------------------- /extra/rediscensus/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/rediscensus/v9 2 | 3 | go 1.19 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd 8 | 9 | require ( 10 | github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 11 | github.com/redis/go-redis/v9 v9.10.0 12 | go.opencensus.io v0.24.0 13 | ) 14 | 15 | require ( 16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 18 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 19 | ) 20 | 21 | retract ( 22 | v9.7.2 // This version was accidentally released. 23 | v9.5.3 // This version was accidentally released. 24 | ) 25 | -------------------------------------------------------------------------------- /extra/rediscensus/rediscensus.go: -------------------------------------------------------------------------------- 1 | package rediscensus 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "go.opencensus.io/trace" 8 | 9 | "github.com/redis/go-redis/extra/rediscmd/v9" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | type TracingHook struct{} 14 | 15 | var _ redis.Hook = (*TracingHook)(nil) 16 | 17 | func NewTracingHook() *TracingHook { 18 | return new(TracingHook) 19 | } 20 | 21 | func (TracingHook) DialHook(next redis.DialHook) redis.DialHook { 22 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 23 | ctx, span := trace.StartSpan(ctx, "dial") 24 | defer span.End() 25 | 26 | span.AddAttributes( 27 | trace.StringAttribute("db.system", "redis"), 28 | trace.StringAttribute("network", network), 29 | trace.StringAttribute("addr", addr), 30 | ) 31 | 32 | conn, err := next(ctx, network, addr) 33 | if err != nil { 34 | recordErrorOnOCSpan(ctx, span, err) 35 | 36 | return nil, err 37 | } 38 | 39 | return conn, nil 40 | } 41 | } 42 | 43 | func (TracingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 44 | return func(ctx context.Context, cmd redis.Cmder) error { 45 | ctx, span := trace.StartSpan(ctx, cmd.FullName()) 46 | defer span.End() 47 | 48 | span.AddAttributes( 49 | trace.StringAttribute("db.system", "redis"), 50 | trace.StringAttribute("redis.cmd", rediscmd.CmdString(cmd)), 51 | ) 52 | 53 | err := next(ctx, cmd) 54 | if err != nil { 55 | recordErrorOnOCSpan(ctx, span, err) 56 | return err 57 | } 58 | 59 | if err = cmd.Err(); err != nil { 60 | recordErrorOnOCSpan(ctx, span, err) 61 | } 62 | 63 | return nil 64 | } 65 | } 66 | 67 | func (TracingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 68 | return next 69 | } 70 | 71 | func recordErrorOnOCSpan(ctx context.Context, span *trace.Span, err error) { 72 | if err != redis.Nil { 73 | span.AddAttributes(trace.BoolAttribute("error", true)) 74 | span.Annotate([]trace.Attribute{trace.StringAttribute("Error", "redis error")}, err.Error()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /extra/rediscmd/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/rediscmd/v9 2 | 3 | go 1.19 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/bsm/ginkgo/v2 v2.12.0 9 | github.com/bsm/gomega v1.27.10 10 | github.com/redis/go-redis/v9 v9.10.0 11 | ) 12 | 13 | require ( 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | ) 17 | 18 | retract ( 19 | v9.7.2 // This version was accidentally released. 20 | v9.5.3 // This version was accidentally released. 21 | ) 22 | -------------------------------------------------------------------------------- /extra/rediscmd/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | -------------------------------------------------------------------------------- /extra/rediscmd/rediscmd.go: -------------------------------------------------------------------------------- 1 | package rediscmd 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | func CmdString(cmd redis.Cmder) string { 14 | b := make([]byte, 0, 32) 15 | b = AppendCmd(b, cmd) 16 | return String(b) 17 | } 18 | 19 | func CmdsString(cmds []redis.Cmder) (string, string) { 20 | const numNameLimit = 10 21 | 22 | seen := make(map[string]struct{}, numNameLimit) 23 | unqNames := make([]string, 0, numNameLimit) 24 | 25 | b := make([]byte, 0, 32*len(cmds)) 26 | 27 | for i, cmd := range cmds { 28 | if i > 0 { 29 | b = append(b, '\n') 30 | } 31 | b = AppendCmd(b, cmd) 32 | 33 | if len(unqNames) >= numNameLimit { 34 | continue 35 | } 36 | 37 | name := cmd.FullName() 38 | if _, ok := seen[name]; !ok { 39 | seen[name] = struct{}{} 40 | unqNames = append(unqNames, name) 41 | } 42 | } 43 | 44 | summary := strings.Join(unqNames, " ") 45 | return summary, String(b) 46 | } 47 | 48 | func AppendCmd(b []byte, cmd redis.Cmder) []byte { 49 | for i, arg := range cmd.Args() { 50 | if i > 0 { 51 | b = append(b, ' ') 52 | } 53 | b = appendArg(b, arg) 54 | } 55 | 56 | if err := cmd.Err(); err != nil { 57 | b = append(b, ": "...) 58 | b = append(b, err.Error()...) 59 | } 60 | 61 | return b 62 | } 63 | 64 | func appendArg(b []byte, v interface{}) []byte { 65 | switch v := v.(type) { 66 | case nil: 67 | return append(b, ""...) 68 | case string: 69 | return appendUTF8String(b, Bytes(v)) 70 | case []byte: 71 | return appendUTF8String(b, v) 72 | case int: 73 | return strconv.AppendInt(b, int64(v), 10) 74 | case int8: 75 | return strconv.AppendInt(b, int64(v), 10) 76 | case int16: 77 | return strconv.AppendInt(b, int64(v), 10) 78 | case int32: 79 | return strconv.AppendInt(b, int64(v), 10) 80 | case int64: 81 | return strconv.AppendInt(b, v, 10) 82 | case uint: 83 | return strconv.AppendUint(b, uint64(v), 10) 84 | case uint8: 85 | return strconv.AppendUint(b, uint64(v), 10) 86 | case uint16: 87 | return strconv.AppendUint(b, uint64(v), 10) 88 | case uint32: 89 | return strconv.AppendUint(b, uint64(v), 10) 90 | case uint64: 91 | return strconv.AppendUint(b, v, 10) 92 | case float32: 93 | return strconv.AppendFloat(b, float64(v), 'f', -1, 64) 94 | case float64: 95 | return strconv.AppendFloat(b, v, 'f', -1, 64) 96 | case bool: 97 | if v { 98 | return append(b, "true"...) 99 | } 100 | return append(b, "false"...) 101 | case time.Time: 102 | return v.AppendFormat(b, time.RFC3339Nano) 103 | default: 104 | return append(b, fmt.Sprint(v)...) 105 | } 106 | } 107 | 108 | func appendUTF8String(dst []byte, src []byte) []byte { 109 | if isSimple(src) { 110 | dst = append(dst, src...) 111 | return dst 112 | } 113 | 114 | s := len(dst) 115 | dst = append(dst, make([]byte, hex.EncodedLen(len(src)))...) 116 | hex.Encode(dst[s:], src) 117 | return dst 118 | } 119 | 120 | func isSimple(b []byte) bool { 121 | for _, c := range b { 122 | if !isSimpleByte(c) { 123 | return false 124 | } 125 | } 126 | return true 127 | } 128 | 129 | func isSimpleByte(c byte) bool { 130 | return c >= 0x21 && c <= 0x7e 131 | } 132 | -------------------------------------------------------------------------------- /extra/rediscmd/rediscmd_test.go: -------------------------------------------------------------------------------- 1 | package rediscmd 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | ) 9 | 10 | func TestGinkgo(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "redisext") 13 | } 14 | 15 | var _ = Describe("AppendArg", func() { 16 | DescribeTable("...", 17 | func(src string, wanted string) { 18 | b := appendArg(nil, src) 19 | Expect(string(b)).To(Equal(wanted)) 20 | }, 21 | 22 | Entry("", "-inf", "-inf"), 23 | Entry("", "+inf", "+inf"), 24 | Entry("", "foo.bar", "foo.bar"), 25 | Entry("", "foo:bar", "foo:bar"), 26 | Entry("", "foo{bar}", "foo{bar}"), 27 | Entry("", "foo-123_BAR", "foo-123_BAR"), 28 | Entry("", "foo\nbar", "666f6f0a626172"), 29 | Entry("", "\000", "00"), 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /extra/rediscmd/safe.go: -------------------------------------------------------------------------------- 1 | //go:build appengine 2 | // +build appengine 3 | 4 | package rediscmd 5 | 6 | func String(b []byte) string { 7 | return string(b) 8 | } 9 | 10 | func Bytes(s string) []byte { 11 | return []byte(s) 12 | } 13 | -------------------------------------------------------------------------------- /extra/rediscmd/unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !appengine 2 | // +build !appengine 3 | 4 | package rediscmd 5 | 6 | import "unsafe" 7 | 8 | // String converts byte slice to string. 9 | func String(b []byte) string { 10 | return *(*string)(unsafe.Pointer(&b)) 11 | } 12 | 13 | // Bytes converts string to byte slice. 14 | func Bytes(s string) []byte { 15 | return *(*[]byte)(unsafe.Pointer( 16 | &struct { 17 | string 18 | Cap int 19 | }{s, len(s)}, 20 | )) 21 | } 22 | -------------------------------------------------------------------------------- /extra/redisotel/README.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry instrumentation for go-redis 2 | 3 | ## Installation 4 | 5 | ```bash 6 | go get github.com/redis/go-redis/extra/redisotel/v9 7 | ``` 8 | 9 | ## Usage 10 | 11 | Tracing is enabled by adding a hook: 12 | 13 | ```go 14 | import ( 15 | "github.com/redis/go-redis/v9" 16 | "github.com/redis/go-redis/extra/redisotel/v9" 17 | ) 18 | 19 | rdb := rdb.NewClient(&rdb.Options{...}) 20 | 21 | // Enable tracing instrumentation. 22 | if err := redisotel.InstrumentTracing(rdb); err != nil { 23 | panic(err) 24 | } 25 | 26 | // Enable metrics instrumentation. 27 | if err := redisotel.InstrumentMetrics(rdb); err != nil { 28 | panic(err) 29 | } 30 | ``` 31 | 32 | See [example](../../example/otel) and 33 | [Monitoring Go Redis Performance and Errors](https://redis.uptrace.dev/guide/go-redis-monitoring.html) 34 | for details. 35 | -------------------------------------------------------------------------------- /extra/redisotel/config.go: -------------------------------------------------------------------------------- 1 | package redisotel 2 | 3 | import ( 4 | "go.opentelemetry.io/otel" 5 | "go.opentelemetry.io/otel/attribute" 6 | "go.opentelemetry.io/otel/metric" 7 | semconv "go.opentelemetry.io/otel/semconv/v1.24.0" 8 | "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | type config struct { 12 | // Common options. 13 | 14 | dbSystem string 15 | attrs []attribute.KeyValue 16 | 17 | // Tracing options. 18 | 19 | tp trace.TracerProvider 20 | tracer trace.Tracer 21 | 22 | dbStmtEnabled bool 23 | 24 | // Metrics options. 25 | 26 | mp metric.MeterProvider 27 | meter metric.Meter 28 | 29 | poolName string 30 | } 31 | 32 | type baseOption interface { 33 | apply(conf *config) 34 | } 35 | 36 | type Option interface { 37 | baseOption 38 | tracing() 39 | metrics() 40 | } 41 | 42 | type option func(conf *config) 43 | 44 | func (fn option) apply(conf *config) { 45 | fn(conf) 46 | } 47 | 48 | func (fn option) tracing() {} 49 | 50 | func (fn option) metrics() {} 51 | 52 | func newConfig(opts ...baseOption) *config { 53 | conf := &config{ 54 | dbSystem: "redis", 55 | attrs: []attribute.KeyValue{}, 56 | 57 | tp: otel.GetTracerProvider(), 58 | mp: otel.GetMeterProvider(), 59 | dbStmtEnabled: true, 60 | } 61 | 62 | for _, opt := range opts { 63 | opt.apply(conf) 64 | } 65 | 66 | conf.attrs = append(conf.attrs, semconv.DBSystemKey.String(conf.dbSystem)) 67 | 68 | return conf 69 | } 70 | 71 | func WithDBSystem(dbSystem string) Option { 72 | return option(func(conf *config) { 73 | conf.dbSystem = dbSystem 74 | }) 75 | } 76 | 77 | // WithAttributes specifies additional attributes to be added to the span. 78 | func WithAttributes(attrs ...attribute.KeyValue) Option { 79 | return option(func(conf *config) { 80 | conf.attrs = append(conf.attrs, attrs...) 81 | }) 82 | } 83 | 84 | //------------------------------------------------------------------------------ 85 | 86 | type TracingOption interface { 87 | baseOption 88 | tracing() 89 | } 90 | 91 | type tracingOption func(conf *config) 92 | 93 | var _ TracingOption = (*tracingOption)(nil) 94 | 95 | func (fn tracingOption) apply(conf *config) { 96 | fn(conf) 97 | } 98 | 99 | func (fn tracingOption) tracing() {} 100 | 101 | // WithTracerProvider specifies a tracer provider to use for creating a tracer. 102 | // If none is specified, the global provider is used. 103 | func WithTracerProvider(provider trace.TracerProvider) TracingOption { 104 | return tracingOption(func(conf *config) { 105 | conf.tp = provider 106 | }) 107 | } 108 | 109 | // WithDBStatement tells the tracing hook not to log raw redis commands. 110 | func WithDBStatement(on bool) TracingOption { 111 | return tracingOption(func(conf *config) { 112 | conf.dbStmtEnabled = on 113 | }) 114 | } 115 | 116 | //------------------------------------------------------------------------------ 117 | 118 | type MetricsOption interface { 119 | baseOption 120 | metrics() 121 | } 122 | 123 | type metricsOption func(conf *config) 124 | 125 | var _ MetricsOption = (*metricsOption)(nil) 126 | 127 | func (fn metricsOption) apply(conf *config) { 128 | fn(conf) 129 | } 130 | 131 | func (fn metricsOption) metrics() {} 132 | 133 | // WithMeterProvider configures a metric.Meter used to create instruments. 134 | func WithMeterProvider(mp metric.MeterProvider) MetricsOption { 135 | return metricsOption(func(conf *config) { 136 | conf.mp = mp 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /extra/redisotel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/redisotel/v9 2 | 3 | go 1.19 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd 8 | 9 | require ( 10 | github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 11 | github.com/redis/go-redis/v9 v9.10.0 12 | go.opentelemetry.io/otel v1.22.0 13 | go.opentelemetry.io/otel/metric v1.22.0 14 | go.opentelemetry.io/otel/sdk v1.22.0 15 | go.opentelemetry.io/otel/trace v1.22.0 16 | ) 17 | 18 | require ( 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 21 | github.com/go-logr/logr v1.4.1 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | golang.org/x/sys v0.16.0 // indirect 24 | ) 25 | 26 | retract ( 27 | v9.7.2 // This version was accidentally released. 28 | v9.5.3 // This version was accidentally released. 29 | ) 30 | -------------------------------------------------------------------------------- /extra/redisotel/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 8 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 9 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 10 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 11 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 12 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 16 | go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= 17 | go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= 18 | go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= 19 | go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= 20 | go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= 21 | go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= 22 | go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= 23 | go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= 24 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 25 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | -------------------------------------------------------------------------------- /extra/redisprometheus/README.md: -------------------------------------------------------------------------------- 1 | # Prometheus Metric Collector 2 | 3 | This package implements a [`prometheus.Collector`](https://pkg.go.dev/github.com/prometheus/client_golang@v1.12.2/prometheus#Collector) 4 | for collecting metrics about the connection pool used by the various redis clients. 5 | Supported clients are `redis.Client`, `redis.ClusterClient`, `redis.Ring` and `redis.UniversalClient`. 6 | 7 | ### Example 8 | 9 | ```go 10 | client := redis.NewClient(options) 11 | collector := redisprometheus.NewCollector(namespace, subsystem, client) 12 | prometheus.MustRegister(collector) 13 | ``` 14 | 15 | ### Metrics 16 | 17 | | Name | Type | Description | 18 | |---------------------------|----------------|-----------------------------------------------------------------------------| 19 | | `pool_hit_total` | Counter metric | number of times a connection was found in the pool | 20 | | `pool_miss_total` | Counter metric | number of times a connection was not found in the pool | 21 | | `pool_timeout_total` | Counter metric | number of times a timeout occurred when getting a connection from the pool | 22 | | `pool_conn_total_current` | Gauge metric | current number of connections in the pool | 23 | | `pool_conn_idle_current` | Gauge metric | current number of idle connections in the pool | 24 | | `pool_conn_stale_total` | Counter metric | number of times a connection was removed from the pool because it was stale | 25 | 26 | 27 | -------------------------------------------------------------------------------- /extra/redisprometheus/collector.go: -------------------------------------------------------------------------------- 1 | package redisprometheus 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | // StatGetter provides a method to get pool statistics. 10 | type StatGetter interface { 11 | PoolStats() *redis.PoolStats 12 | } 13 | 14 | // Collector collects statistics from a redis client. 15 | // It implements the prometheus.Collector interface. 16 | type Collector struct { 17 | getter StatGetter 18 | hitDesc *prometheus.Desc 19 | missDesc *prometheus.Desc 20 | timeoutDesc *prometheus.Desc 21 | totalDesc *prometheus.Desc 22 | idleDesc *prometheus.Desc 23 | staleDesc *prometheus.Desc 24 | } 25 | 26 | var _ prometheus.Collector = (*Collector)(nil) 27 | 28 | // NewCollector returns a new Collector based on the provided StatGetter. 29 | // The given namespace and subsystem are used to build the fully qualified metric name, 30 | // i.e. "{namespace}_{subsystem}_{metric}". 31 | // The provided metrics are: 32 | // - pool_hit_total 33 | // - pool_miss_total 34 | // - pool_timeout_total 35 | // - pool_conn_total_current 36 | // - pool_conn_idle_current 37 | // - pool_conn_stale_total 38 | func NewCollector(namespace, subsystem string, getter StatGetter) *Collector { 39 | return &Collector{ 40 | getter: getter, 41 | hitDesc: prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, subsystem, "pool_hit_total"), 43 | "Number of times a connection was found in the pool", 44 | nil, nil, 45 | ), 46 | missDesc: prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, subsystem, "pool_miss_total"), 48 | "Number of times a connection was not found in the pool", 49 | nil, nil, 50 | ), 51 | timeoutDesc: prometheus.NewDesc( 52 | prometheus.BuildFQName(namespace, subsystem, "pool_timeout_total"), 53 | "Number of times a timeout occurred when looking for a connection in the pool", 54 | nil, nil, 55 | ), 56 | totalDesc: prometheus.NewDesc( 57 | prometheus.BuildFQName(namespace, subsystem, "pool_conn_total_current"), 58 | "Current number of connections in the pool", 59 | nil, nil, 60 | ), 61 | idleDesc: prometheus.NewDesc( 62 | prometheus.BuildFQName(namespace, subsystem, "pool_conn_idle_current"), 63 | "Current number of idle connections in the pool", 64 | nil, nil, 65 | ), 66 | staleDesc: prometheus.NewDesc( 67 | prometheus.BuildFQName(namespace, subsystem, "pool_conn_stale_total"), 68 | "Number of times a connection was removed from the pool because it was stale", 69 | nil, nil, 70 | ), 71 | } 72 | } 73 | 74 | // Describe implements the prometheus.Collector interface. 75 | func (s *Collector) Describe(descs chan<- *prometheus.Desc) { 76 | descs <- s.hitDesc 77 | descs <- s.missDesc 78 | descs <- s.timeoutDesc 79 | descs <- s.totalDesc 80 | descs <- s.idleDesc 81 | descs <- s.staleDesc 82 | } 83 | 84 | // Collect implements the prometheus.Collector interface. 85 | func (s *Collector) Collect(metrics chan<- prometheus.Metric) { 86 | stats := s.getter.PoolStats() 87 | metrics <- prometheus.MustNewConstMetric( 88 | s.hitDesc, 89 | prometheus.CounterValue, 90 | float64(stats.Hits), 91 | ) 92 | metrics <- prometheus.MustNewConstMetric( 93 | s.missDesc, 94 | prometheus.CounterValue, 95 | float64(stats.Misses), 96 | ) 97 | metrics <- prometheus.MustNewConstMetric( 98 | s.timeoutDesc, 99 | prometheus.CounterValue, 100 | float64(stats.Timeouts), 101 | ) 102 | metrics <- prometheus.MustNewConstMetric( 103 | s.totalDesc, 104 | prometheus.GaugeValue, 105 | float64(stats.TotalConns), 106 | ) 107 | metrics <- prometheus.MustNewConstMetric( 108 | s.idleDesc, 109 | prometheus.GaugeValue, 110 | float64(stats.IdleConns), 111 | ) 112 | metrics <- prometheus.MustNewConstMetric( 113 | s.staleDesc, 114 | prometheus.CounterValue, 115 | float64(stats.StaleConns), 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /extra/redisprometheus/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/redisprometheus/v9 2 | 3 | go 1.19 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/prometheus/client_golang v1.14.0 9 | github.com/redis/go-redis/v9 v9.10.0 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | github.com/golang/protobuf v1.5.2 // indirect 17 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 18 | github.com/prometheus/client_model v0.3.0 // indirect 19 | github.com/prometheus/common v0.39.0 // indirect 20 | github.com/prometheus/procfs v0.9.0 // indirect 21 | golang.org/x/sys v0.4.0 // indirect 22 | google.golang.org/protobuf v1.33.0 // indirect 23 | ) 24 | 25 | retract ( 26 | v9.7.2 // This version was accidentally released. 27 | v9.5.3 // This version was accidentally released. 28 | ) 29 | -------------------------------------------------------------------------------- /extra/redisprometheus/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 4 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 10 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 11 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 12 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 13 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 14 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 15 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 16 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 17 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 18 | github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= 19 | github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= 20 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 21 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 22 | github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= 23 | github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= 24 | github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 25 | github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= 26 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 28 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 30 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 31 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 32 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 33 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 34 | -------------------------------------------------------------------------------- /fuzz/fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package fuzz 5 | 6 | import ( 7 | "context" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | var ( 14 | ctx = context.Background() 15 | rdb *redis.Client 16 | ) 17 | 18 | func init() { 19 | rdb = redis.NewClient(&redis.Options{ 20 | Addr: ":6379", 21 | DialTimeout: 10 * time.Second, 22 | ReadTimeout: 10 * time.Second, 23 | WriteTimeout: 10 * time.Second, 24 | PoolSize: 10, 25 | PoolTimeout: 10 * time.Second, 26 | }) 27 | } 28 | 29 | func Fuzz(data []byte) int { 30 | arrayLen := len(data) 31 | if arrayLen < 4 { 32 | return -1 33 | } 34 | maxIter := int(uint(data[0])) 35 | for i := 0; i < maxIter && i < arrayLen; i++ { 36 | n := i % arrayLen 37 | if n == 0 { 38 | _ = rdb.Set(ctx, string(data[i:]), string(data[i:]), 0).Err() 39 | } else if n == 1 { 40 | _, _ = rdb.Get(ctx, string(data[i:])).Result() 41 | } else if n == 2 { 42 | _, _ = rdb.Incr(ctx, string(data[i:])).Result() 43 | } else if n == 3 { 44 | var cursor uint64 45 | _, _, _ = rdb.Scan(ctx, cursor, string(data[i:]), 10).Result() 46 | } 47 | } 48 | return 1 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/v9 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bsm/ginkgo/v2 v2.12.0 7 | github.com/bsm/gomega v1.27.10 8 | github.com/cespare/xxhash/v2 v2.3.0 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f 10 | ) 11 | 12 | retract ( 13 | v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. 14 | v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead. 15 | v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | -------------------------------------------------------------------------------- /helper/helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "github.com/redis/go-redis/v9/internal/util" 4 | 5 | func ParseFloat(s string) (float64, error) { 6 | return util.ParseStringToFloat(s) 7 | } 8 | 9 | func MustParseFloat(s string) float64 { 10 | return util.MustParseFloat(s) 11 | } 12 | -------------------------------------------------------------------------------- /hyperloglog_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "context" 4 | 5 | type HyperLogLogCmdable interface { 6 | PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd 7 | PFCount(ctx context.Context, keys ...string) *IntCmd 8 | PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd 9 | } 10 | 11 | func (c cmdable) PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd { 12 | args := make([]interface{}, 2, 2+len(els)) 13 | args[0] = "pfadd" 14 | args[1] = key 15 | args = appendArgs(args, els) 16 | cmd := NewIntCmd(ctx, args...) 17 | _ = c(ctx, cmd) 18 | return cmd 19 | } 20 | 21 | func (c cmdable) PFCount(ctx context.Context, keys ...string) *IntCmd { 22 | args := make([]interface{}, 1+len(keys)) 23 | args[0] = "pfcount" 24 | for i, key := range keys { 25 | args[1+i] = key 26 | } 27 | cmd := NewIntCmd(ctx, args...) 28 | _ = c(ctx, cmd) 29 | return cmd 30 | } 31 | 32 | func (c cmdable) PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd { 33 | args := make([]interface{}, 2+len(keys)) 34 | args[0] = "pfmerge" 35 | args[1] = dest 36 | for i, key := range keys { 37 | args[2+i] = key 38 | } 39 | cmd := NewStatusCmd(ctx, args...) 40 | _ = c(ctx, cmd) 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /internal/arg.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9/internal/util" 9 | ) 10 | 11 | func AppendArg(b []byte, v interface{}) []byte { 12 | switch v := v.(type) { 13 | case nil: 14 | return append(b, ""...) 15 | case string: 16 | return appendUTF8String(b, util.StringToBytes(v)) 17 | case []byte: 18 | return appendUTF8String(b, v) 19 | case int: 20 | return strconv.AppendInt(b, int64(v), 10) 21 | case int8: 22 | return strconv.AppendInt(b, int64(v), 10) 23 | case int16: 24 | return strconv.AppendInt(b, int64(v), 10) 25 | case int32: 26 | return strconv.AppendInt(b, int64(v), 10) 27 | case int64: 28 | return strconv.AppendInt(b, v, 10) 29 | case uint: 30 | return strconv.AppendUint(b, uint64(v), 10) 31 | case uint8: 32 | return strconv.AppendUint(b, uint64(v), 10) 33 | case uint16: 34 | return strconv.AppendUint(b, uint64(v), 10) 35 | case uint32: 36 | return strconv.AppendUint(b, uint64(v), 10) 37 | case uint64: 38 | return strconv.AppendUint(b, v, 10) 39 | case float32: 40 | return strconv.AppendFloat(b, float64(v), 'f', -1, 64) 41 | case float64: 42 | return strconv.AppendFloat(b, v, 'f', -1, 64) 43 | case bool: 44 | if v { 45 | return append(b, "true"...) 46 | } 47 | return append(b, "false"...) 48 | case time.Time: 49 | return v.AppendFormat(b, time.RFC3339Nano) 50 | default: 51 | return append(b, fmt.Sprint(v)...) 52 | } 53 | } 54 | 55 | func appendUTF8String(dst []byte, src []byte) []byte { 56 | dst = append(dst, src...) 57 | return dst 58 | } 59 | -------------------------------------------------------------------------------- /internal/customvet/.gitignore: -------------------------------------------------------------------------------- 1 | /customvet 2 | -------------------------------------------------------------------------------- /internal/customvet/checks/setval/setval.go: -------------------------------------------------------------------------------- 1 | package setval 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "golang.org/x/tools/go/analysis" 9 | ) 10 | 11 | var Analyzer = &analysis.Analyzer{ 12 | Name: "setval", 13 | Doc: "find Cmder types that are missing a SetVal method", 14 | 15 | Run: func(pass *analysis.Pass) (interface{}, error) { 16 | cmderTypes := make(map[string]token.Pos) 17 | typesWithSetValMethod := make(map[string]bool) 18 | 19 | for _, file := range pass.Files { 20 | for _, decl := range file.Decls { 21 | funcName, receiverType := parseFuncDecl(decl, pass.TypesInfo) 22 | 23 | switch funcName { 24 | case "Result": 25 | cmderTypes[receiverType] = decl.Pos() 26 | case "SetVal": 27 | typesWithSetValMethod[receiverType] = true 28 | } 29 | } 30 | } 31 | 32 | for cmder, pos := range cmderTypes { 33 | if !typesWithSetValMethod[cmder] { 34 | pass.Reportf(pos, "%s is missing a SetVal method", cmder) 35 | } 36 | } 37 | 38 | return nil, nil 39 | }, 40 | } 41 | 42 | func parseFuncDecl(decl ast.Decl, typesInfo *types.Info) (funcName, receiverType string) { 43 | funcDecl, ok := decl.(*ast.FuncDecl) 44 | if !ok { 45 | return "", "" // Not a function declaration. 46 | } 47 | 48 | if funcDecl.Recv == nil { 49 | return "", "" // Not a method. 50 | } 51 | 52 | if len(funcDecl.Recv.List) != 1 { 53 | return "", "" // Unexpected number of receiver arguments. (Can this happen?) 54 | } 55 | 56 | receiverTypeObj := typesInfo.TypeOf(funcDecl.Recv.List[0].Type) 57 | if receiverTypeObj == nil { 58 | return "", "" // Unable to determine the receiver type. 59 | } 60 | 61 | return funcDecl.Name.Name, receiverTypeObj.String() 62 | } 63 | -------------------------------------------------------------------------------- /internal/customvet/checks/setval/setval_test.go: -------------------------------------------------------------------------------- 1 | package setval_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/tools/go/analysis/analysistest" 7 | 8 | "github.com/redis/go-redis/internal/customvet/checks/setval" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | testdata := analysistest.TestData() 13 | analysistest.Run(t, testdata, setval.Analyzer, "a") 14 | } 15 | -------------------------------------------------------------------------------- /internal/customvet/checks/setval/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type GoodCmd struct { 4 | val int 5 | } 6 | 7 | func (c *GoodCmd) SetVal(val int) { 8 | c.val = val 9 | } 10 | 11 | func (c *GoodCmd) Result() (int, error) { 12 | return c.val, nil 13 | } 14 | 15 | type BadCmd struct { 16 | val int 17 | } 18 | 19 | func (c *BadCmd) Result() (int, error) { // want "\\*a.BadCmd is missing a SetVal method" 20 | return c.val, nil 21 | } 22 | 23 | type NotACmd struct { 24 | val int 25 | } 26 | 27 | func (c *NotACmd) Val() int { 28 | return c.val 29 | } 30 | -------------------------------------------------------------------------------- /internal/customvet/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/internal/customvet 2 | 3 | go 1.17 4 | 5 | require golang.org/x/tools v0.5.0 6 | 7 | require ( 8 | golang.org/x/mod v0.7.0 // indirect 9 | golang.org/x/sys v0.4.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /internal/customvet/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 2 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 3 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 4 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 5 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 6 | golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= 7 | golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= 8 | -------------------------------------------------------------------------------- /internal/customvet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/tools/go/analysis/multichecker" 5 | 6 | "github.com/redis/go-redis/internal/customvet/checks/setval" 7 | ) 8 | 9 | func main() { 10 | multichecker.Main( 11 | setval.Analyzer, 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /internal/hashtag/hashtag.go: -------------------------------------------------------------------------------- 1 | package hashtag 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/redis/go-redis/v9/internal/rand" 7 | ) 8 | 9 | const slotNumber = 16384 10 | 11 | // CRC16 implementation according to CCITT standards. 12 | // Copyright 2001-2010 Georges Menie (www.menie.org) 13 | // Copyright 2013 The Go Authors. All rights reserved. 14 | // http://redis.io/topics/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c 15 | var crc16tab = [256]uint16{ 16 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 17 | 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 18 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 19 | 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 20 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 21 | 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 22 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 23 | 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 24 | 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 25 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 26 | 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 27 | 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 28 | 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 29 | 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 30 | 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 31 | 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 32 | 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 33 | 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 34 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 35 | 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 36 | 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 37 | 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 38 | 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 39 | 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 40 | 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 41 | 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 42 | 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 43 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 44 | 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 45 | 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 46 | 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 47 | 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 48 | } 49 | 50 | func Key(key string) string { 51 | if s := strings.IndexByte(key, '{'); s > -1 { 52 | if e := strings.IndexByte(key[s+1:], '}'); e > 0 { 53 | return key[s+1 : s+e+1] 54 | } 55 | } 56 | return key 57 | } 58 | 59 | func RandomSlot() int { 60 | return rand.Intn(slotNumber) 61 | } 62 | 63 | // Slot returns a consistent slot number between 0 and 16383 64 | // for any given string key. 65 | func Slot(key string) int { 66 | if key == "" { 67 | return RandomSlot() 68 | } 69 | key = Key(key) 70 | return int(crc16sum(key)) % slotNumber 71 | } 72 | 73 | func crc16sum(key string) (crc uint16) { 74 | for i := 0; i < len(key); i++ { 75 | crc = (crc << 8) ^ crc16tab[(byte(crc>>8)^key[i])&0x00ff] 76 | } 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /internal/hashtag/hashtag_test.go: -------------------------------------------------------------------------------- 1 | package hashtag 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | 9 | "github.com/redis/go-redis/v9/internal/rand" 10 | ) 11 | 12 | func TestGinkgoSuite(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "hashtag") 15 | } 16 | 17 | var _ = Describe("CRC16", func() { 18 | // http://redis.io/topics/cluster-spec#keys-distribution-model 19 | It("should calculate CRC16", func() { 20 | tests := []struct { 21 | s string 22 | n uint16 23 | }{ 24 | {"123456789", 0x31C3}, 25 | {string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), 21847}, 26 | } 27 | 28 | for _, test := range tests { 29 | Expect(crc16sum(test.s)).To(Equal(test.n), "for %s", test.s) 30 | } 31 | }) 32 | }) 33 | 34 | var _ = Describe("HashSlot", func() { 35 | It("should calculate hash slots", func() { 36 | tests := []struct { 37 | key string 38 | slot int 39 | }{ 40 | {"123456789", 12739}, 41 | {"{}foo", 9500}, 42 | {"foo{}", 5542}, 43 | {"foo{}{bar}", 8363}, 44 | {"", 10503}, 45 | {"", 5176}, 46 | {string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), 5463}, 47 | } 48 | // Empty keys receive random slot. 49 | rand.Seed(100) 50 | 51 | for _, test := range tests { 52 | Expect(Slot(test.key)).To(Equal(test.slot), "for %s", test.key) 53 | } 54 | }) 55 | 56 | It("should extract keys from tags", func() { 57 | tests := []struct { 58 | one, two string 59 | }{ 60 | {"foo{bar}", "bar"}, 61 | {"{foo}bar", "foo"}, 62 | {"{user1000}.following", "{user1000}.followers"}, 63 | {"foo{{bar}}zap", "{bar"}, 64 | {"foo{bar}{zap}", "bar"}, 65 | } 66 | 67 | for _, test := range tests { 68 | Expect(Slot(test.one)).To(Equal(Slot(test.two)), "for %s <-> %s", test.one, test.two) 69 | } 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /internal/hscan/structmap.go: -------------------------------------------------------------------------------- 1 | package hscan 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/redis/go-redis/v9/internal/util" 11 | ) 12 | 13 | // structMap contains the map of struct fields for target structs 14 | // indexed by the struct type. 15 | type structMap struct { 16 | m sync.Map 17 | } 18 | 19 | func newStructMap() *structMap { 20 | return new(structMap) 21 | } 22 | 23 | func (s *structMap) get(t reflect.Type) *structSpec { 24 | if v, ok := s.m.Load(t); ok { 25 | return v.(*structSpec) 26 | } 27 | 28 | spec := newStructSpec(t, "redis") 29 | s.m.Store(t, spec) 30 | return spec 31 | } 32 | 33 | //------------------------------------------------------------------------------ 34 | 35 | // structSpec contains the list of all fields in a target struct. 36 | type structSpec struct { 37 | m map[string]*structField 38 | } 39 | 40 | func (s *structSpec) set(tag string, sf *structField) { 41 | s.m[tag] = sf 42 | } 43 | 44 | func newStructSpec(t reflect.Type, fieldTag string) *structSpec { 45 | numField := t.NumField() 46 | out := &structSpec{ 47 | m: make(map[string]*structField, numField), 48 | } 49 | 50 | for i := 0; i < numField; i++ { 51 | f := t.Field(i) 52 | 53 | tag := f.Tag.Get(fieldTag) 54 | if tag == "" || tag == "-" { 55 | continue 56 | } 57 | 58 | tag = strings.Split(tag, ",")[0] 59 | if tag == "" { 60 | continue 61 | } 62 | 63 | // Use the built-in decoder. 64 | kind := f.Type.Kind() 65 | if kind == reflect.Pointer { 66 | kind = f.Type.Elem().Kind() 67 | } 68 | out.set(tag, &structField{index: i, fn: decoders[kind]}) 69 | } 70 | 71 | return out 72 | } 73 | 74 | //------------------------------------------------------------------------------ 75 | 76 | // structField represents a single field in a target struct. 77 | type structField struct { 78 | index int 79 | fn decoderFunc 80 | } 81 | 82 | //------------------------------------------------------------------------------ 83 | 84 | type StructValue struct { 85 | spec *structSpec 86 | value reflect.Value 87 | } 88 | 89 | func (s StructValue) Scan(key string, value string) error { 90 | field, ok := s.spec.m[key] 91 | if !ok { 92 | return nil 93 | } 94 | 95 | v := s.value.Field(field.index) 96 | isPtr := v.Kind() == reflect.Ptr 97 | 98 | if isPtr && v.IsNil() { 99 | v.Set(reflect.New(v.Type().Elem())) 100 | } 101 | if !isPtr && v.Type().Name() != "" && v.CanAddr() { 102 | v = v.Addr() 103 | isPtr = true 104 | } 105 | 106 | if isPtr && v.Type().NumMethod() > 0 && v.CanInterface() { 107 | switch scan := v.Interface().(type) { 108 | case Scanner: 109 | return scan.ScanRedis(value) 110 | case encoding.TextUnmarshaler: 111 | return scan.UnmarshalText(util.StringToBytes(value)) 112 | } 113 | } 114 | 115 | if isPtr { 116 | v = v.Elem() 117 | } 118 | 119 | if err := field.fn(v, value); err != nil { 120 | t := s.value.Type() 121 | return fmt.Errorf("cannot scan redis.result %s into struct field %s.%s of type %s, error-%s", 122 | value, t.Name(), t.Field(field.index).Name, t.Field(field.index).Type, err.Error()) 123 | } 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/redis/go-redis/v9/internal/rand" 7 | ) 8 | 9 | func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration { 10 | if retry < 0 { 11 | panic("not reached") 12 | } 13 | if minBackoff == 0 { 14 | return 0 15 | } 16 | 17 | d := minBackoff << uint(retry) 18 | if d < minBackoff { 19 | return maxBackoff 20 | } 21 | 22 | d = minBackoff + time.Duration(rand.Int63n(int64(d))) 23 | 24 | if d > maxBackoff || d < minBackoff { 25 | d = maxBackoff 26 | } 27 | 28 | return d 29 | } 30 | -------------------------------------------------------------------------------- /internal/internal_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | . "github.com/bsm/gomega" 8 | ) 9 | 10 | func TestRetryBackoff(t *testing.T) { 11 | RegisterTestingT(t) 12 | 13 | for i := 0; i <= 16; i++ { 14 | backoff := RetryBackoff(i, time.Millisecond, 512*time.Millisecond) 15 | Expect(backoff >= 0).To(BeTrue()) 16 | Expect(backoff <= 512*time.Millisecond).To(BeTrue()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/log.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | type Logging interface { 11 | Printf(ctx context.Context, format string, v ...interface{}) 12 | } 13 | 14 | type logger struct { 15 | log *log.Logger 16 | } 17 | 18 | func (l *logger) Printf(ctx context.Context, format string, v ...interface{}) { 19 | _ = l.log.Output(2, fmt.Sprintf(format, v...)) 20 | } 21 | 22 | // Logger calls Output to print to the stderr. 23 | // Arguments are handled in the manner of fmt.Print. 24 | var Logger Logging = &logger{ 25 | log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile), 26 | } 27 | -------------------------------------------------------------------------------- /internal/once.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Camlistore Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "sync" 21 | "sync/atomic" 22 | ) 23 | 24 | // A Once will perform a successful action exactly once. 25 | // 26 | // Unlike a sync.Once, this Once's func returns an error 27 | // and is re-armed on failure. 28 | type Once struct { 29 | m sync.Mutex 30 | done uint32 31 | } 32 | 33 | // Do calls the function f if and only if Do has not been invoked 34 | // without error for this instance of Once. In other words, given 35 | // 36 | // var once Once 37 | // 38 | // if once.Do(f) is called multiple times, only the first call will 39 | // invoke f, even if f has a different value in each invocation unless 40 | // f returns an error. A new instance of Once is required for each 41 | // function to execute. 42 | // 43 | // Do is intended for initialization that must be run exactly once. Since f 44 | // is niladic, it may be necessary to use a function literal to capture the 45 | // arguments to a function to be invoked by Do: 46 | // 47 | // err := config.once.Do(func() error { return config.init(filename) }) 48 | func (o *Once) Do(f func() error) error { 49 | if atomic.LoadUint32(&o.done) == 1 { 50 | return nil 51 | } 52 | // Slow-path. 53 | o.m.Lock() 54 | defer o.m.Unlock() 55 | var err error 56 | if o.done == 0 { 57 | err = f() 58 | if err == nil { 59 | atomic.StoreUint32(&o.done, 1) 60 | } 61 | } 62 | return err 63 | } 64 | -------------------------------------------------------------------------------- /internal/pool/bench_test.go: -------------------------------------------------------------------------------- 1 | package pool_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/redis/go-redis/v9/internal/pool" 10 | ) 11 | 12 | type poolGetPutBenchmark struct { 13 | poolSize int 14 | } 15 | 16 | func (bm poolGetPutBenchmark) String() string { 17 | return fmt.Sprintf("pool=%d", bm.poolSize) 18 | } 19 | 20 | func BenchmarkPoolGetPut(b *testing.B) { 21 | ctx := context.Background() 22 | benchmarks := []poolGetPutBenchmark{ 23 | {1}, 24 | {2}, 25 | {8}, 26 | {32}, 27 | {64}, 28 | {128}, 29 | } 30 | for _, bm := range benchmarks { 31 | b.Run(bm.String(), func(b *testing.B) { 32 | connPool := pool.NewConnPool(&pool.Options{ 33 | Dialer: dummyDialer, 34 | PoolSize: bm.poolSize, 35 | PoolTimeout: time.Second, 36 | DialTimeout: 1 * time.Second, 37 | ConnMaxIdleTime: time.Hour, 38 | }) 39 | 40 | b.ResetTimer() 41 | 42 | b.RunParallel(func(pb *testing.PB) { 43 | for pb.Next() { 44 | cn, err := connPool.Get(ctx) 45 | if err != nil { 46 | b.Fatal(err) 47 | } 48 | connPool.Put(ctx, cn) 49 | } 50 | }) 51 | }) 52 | } 53 | } 54 | 55 | type poolGetRemoveBenchmark struct { 56 | poolSize int 57 | } 58 | 59 | func (bm poolGetRemoveBenchmark) String() string { 60 | return fmt.Sprintf("pool=%d", bm.poolSize) 61 | } 62 | 63 | func BenchmarkPoolGetRemove(b *testing.B) { 64 | ctx := context.Background() 65 | benchmarks := []poolGetRemoveBenchmark{ 66 | {1}, 67 | {2}, 68 | {8}, 69 | {32}, 70 | {64}, 71 | {128}, 72 | } 73 | 74 | for _, bm := range benchmarks { 75 | b.Run(bm.String(), func(b *testing.B) { 76 | connPool := pool.NewConnPool(&pool.Options{ 77 | Dialer: dummyDialer, 78 | PoolSize: bm.poolSize, 79 | PoolTimeout: time.Second, 80 | DialTimeout: 1 * time.Second, 81 | ConnMaxIdleTime: time.Hour, 82 | }) 83 | 84 | b.ResetTimer() 85 | 86 | b.RunParallel(func(pb *testing.PB) { 87 | for pb.Next() { 88 | cn, err := connPool.Get(ctx) 89 | if err != nil { 90 | b.Fatal(err) 91 | } 92 | connPool.Remove(ctx, cn, nil) 93 | } 94 | }) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/pool/conn.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "net" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9/internal/proto" 11 | ) 12 | 13 | var noDeadline = time.Time{} 14 | 15 | type Conn struct { 16 | usedAt int64 // atomic 17 | netConn net.Conn 18 | 19 | rd *proto.Reader 20 | bw *bufio.Writer 21 | wr *proto.Writer 22 | 23 | Inited bool 24 | pooled bool 25 | createdAt time.Time 26 | 27 | onClose func() error 28 | } 29 | 30 | func NewConn(netConn net.Conn) *Conn { 31 | cn := &Conn{ 32 | netConn: netConn, 33 | createdAt: time.Now(), 34 | } 35 | cn.rd = proto.NewReader(netConn) 36 | cn.bw = bufio.NewWriter(netConn) 37 | cn.wr = proto.NewWriter(cn.bw) 38 | cn.SetUsedAt(time.Now()) 39 | return cn 40 | } 41 | 42 | func (cn *Conn) UsedAt() time.Time { 43 | unix := atomic.LoadInt64(&cn.usedAt) 44 | return time.Unix(unix, 0) 45 | } 46 | 47 | func (cn *Conn) SetUsedAt(tm time.Time) { 48 | atomic.StoreInt64(&cn.usedAt, tm.Unix()) 49 | } 50 | 51 | func (cn *Conn) SetOnClose(fn func() error) { 52 | cn.onClose = fn 53 | } 54 | 55 | func (cn *Conn) SetNetConn(netConn net.Conn) { 56 | cn.netConn = netConn 57 | cn.rd.Reset(netConn) 58 | cn.bw.Reset(netConn) 59 | } 60 | 61 | func (cn *Conn) Write(b []byte) (int, error) { 62 | return cn.netConn.Write(b) 63 | } 64 | 65 | func (cn *Conn) RemoteAddr() net.Addr { 66 | if cn.netConn != nil { 67 | return cn.netConn.RemoteAddr() 68 | } 69 | return nil 70 | } 71 | 72 | func (cn *Conn) WithReader( 73 | ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error, 74 | ) error { 75 | if timeout >= 0 { 76 | if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil { 77 | return err 78 | } 79 | } 80 | return fn(cn.rd) 81 | } 82 | 83 | func (cn *Conn) WithWriter( 84 | ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error, 85 | ) error { 86 | if timeout >= 0 { 87 | if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil { 88 | return err 89 | } 90 | } 91 | 92 | if cn.bw.Buffered() > 0 { 93 | cn.bw.Reset(cn.netConn) 94 | } 95 | 96 | if err := fn(cn.wr); err != nil { 97 | return err 98 | } 99 | 100 | return cn.bw.Flush() 101 | } 102 | 103 | func (cn *Conn) Close() error { 104 | if cn.onClose != nil { 105 | // ignore error 106 | _ = cn.onClose() 107 | } 108 | return cn.netConn.Close() 109 | } 110 | 111 | func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time { 112 | tm := time.Now() 113 | cn.SetUsedAt(tm) 114 | 115 | if timeout > 0 { 116 | tm = tm.Add(timeout) 117 | } 118 | 119 | if ctx != nil { 120 | deadline, ok := ctx.Deadline() 121 | if ok { 122 | if timeout == 0 { 123 | return deadline 124 | } 125 | if deadline.Before(tm) { 126 | return deadline 127 | } 128 | return tm 129 | } 130 | } 131 | 132 | if timeout > 0 { 133 | return tm 134 | } 135 | 136 | return noDeadline 137 | } 138 | -------------------------------------------------------------------------------- /internal/pool/conn_check.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos 2 | 3 | package pool 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "net" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | var errUnexpectedRead = errors.New("unexpected read from socket") 14 | 15 | func connCheck(conn net.Conn) error { 16 | // Reset previous timeout. 17 | _ = conn.SetDeadline(time.Time{}) 18 | 19 | sysConn, ok := conn.(syscall.Conn) 20 | if !ok { 21 | return nil 22 | } 23 | rawConn, err := sysConn.SyscallConn() 24 | if err != nil { 25 | return err 26 | } 27 | 28 | var sysErr error 29 | 30 | if err := rawConn.Read(func(fd uintptr) bool { 31 | var buf [1]byte 32 | n, err := syscall.Read(int(fd), buf[:]) 33 | switch { 34 | case n == 0 && err == nil: 35 | sysErr = io.EOF 36 | case n > 0: 37 | sysErr = errUnexpectedRead 38 | case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK: 39 | sysErr = nil 40 | default: 41 | sysErr = err 42 | } 43 | return true 44 | }); err != nil { 45 | return err 46 | } 47 | 48 | return sysErr 49 | } 50 | -------------------------------------------------------------------------------- /internal/pool/conn_check_dummy.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos 2 | 3 | package pool 4 | 5 | import "net" 6 | 7 | func connCheck(conn net.Conn) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /internal/pool/conn_check_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos 2 | 3 | package pool 4 | 5 | import ( 6 | "net" 7 | "net/http/httptest" 8 | "time" 9 | 10 | . "github.com/bsm/ginkgo/v2" 11 | . "github.com/bsm/gomega" 12 | ) 13 | 14 | var _ = Describe("tests conn_check with real conns", func() { 15 | var ts *httptest.Server 16 | var conn net.Conn 17 | var err error 18 | 19 | BeforeEach(func() { 20 | ts = httptest.NewServer(nil) 21 | conn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second) 22 | Expect(err).NotTo(HaveOccurred()) 23 | }) 24 | 25 | AfterEach(func() { 26 | ts.Close() 27 | }) 28 | 29 | It("good conn check", func() { 30 | Expect(connCheck(conn)).NotTo(HaveOccurred()) 31 | 32 | Expect(conn.Close()).NotTo(HaveOccurred()) 33 | Expect(connCheck(conn)).To(HaveOccurred()) 34 | }) 35 | 36 | It("bad conn check", func() { 37 | Expect(conn.Close()).NotTo(HaveOccurred()) 38 | Expect(connCheck(conn)).To(HaveOccurred()) 39 | }) 40 | 41 | It("check conn deadline", func() { 42 | Expect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred()) 43 | time.Sleep(time.Millisecond * 10) 44 | Expect(connCheck(conn)).NotTo(HaveOccurred()) 45 | Expect(conn.Close()).NotTo(HaveOccurred()) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /internal/pool/export_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | func (cn *Conn) SetCreatedAt(tm time.Time) { 9 | cn.createdAt = tm 10 | } 11 | 12 | func (cn *Conn) NetConn() net.Conn { 13 | return cn.netConn 14 | } 15 | -------------------------------------------------------------------------------- /internal/pool/main_test.go: -------------------------------------------------------------------------------- 1 | package pool_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "syscall" 9 | "testing" 10 | "time" 11 | 12 | . "github.com/bsm/ginkgo/v2" 13 | . "github.com/bsm/gomega" 14 | ) 15 | 16 | func TestGinkgoSuite(t *testing.T) { 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "pool") 19 | } 20 | 21 | func perform(n int, cbs ...func(int)) { 22 | var wg sync.WaitGroup 23 | for _, cb := range cbs { 24 | for i := 0; i < n; i++ { 25 | wg.Add(1) 26 | go func(cb func(int), i int) { 27 | defer GinkgoRecover() 28 | defer wg.Done() 29 | 30 | cb(i) 31 | }(cb, i) 32 | } 33 | } 34 | wg.Wait() 35 | } 36 | 37 | func dummyDialer(context.Context) (net.Conn, error) { 38 | return newDummyConn(), nil 39 | } 40 | 41 | func newDummyConn() net.Conn { 42 | return &dummyConn{ 43 | rawConn: new(dummyRawConn), 44 | } 45 | } 46 | 47 | var ( 48 | _ net.Conn = (*dummyConn)(nil) 49 | _ syscall.Conn = (*dummyConn)(nil) 50 | ) 51 | 52 | type dummyConn struct { 53 | rawConn *dummyRawConn 54 | } 55 | 56 | func (d *dummyConn) SyscallConn() (syscall.RawConn, error) { 57 | return d.rawConn, nil 58 | } 59 | 60 | var errDummy = fmt.Errorf("dummyConn err") 61 | 62 | func (d *dummyConn) Read(b []byte) (n int, err error) { 63 | return 0, errDummy 64 | } 65 | 66 | func (d *dummyConn) Write(b []byte) (n int, err error) { 67 | return 0, errDummy 68 | } 69 | 70 | func (d *dummyConn) Close() error { 71 | d.rawConn.Close() 72 | return nil 73 | } 74 | 75 | func (d *dummyConn) LocalAddr() net.Addr { 76 | return &net.TCPAddr{} 77 | } 78 | 79 | func (d *dummyConn) RemoteAddr() net.Addr { 80 | return &net.TCPAddr{} 81 | } 82 | 83 | func (d *dummyConn) SetDeadline(t time.Time) error { 84 | return nil 85 | } 86 | 87 | func (d *dummyConn) SetReadDeadline(t time.Time) error { 88 | return nil 89 | } 90 | 91 | func (d *dummyConn) SetWriteDeadline(t time.Time) error { 92 | return nil 93 | } 94 | 95 | var _ syscall.RawConn = (*dummyRawConn)(nil) 96 | 97 | type dummyRawConn struct { 98 | mu sync.Mutex 99 | closed bool 100 | } 101 | 102 | func (d *dummyRawConn) Control(f func(fd uintptr)) error { 103 | return nil 104 | } 105 | 106 | func (d *dummyRawConn) Read(f func(fd uintptr) (done bool)) error { 107 | d.mu.Lock() 108 | defer d.mu.Unlock() 109 | if d.closed { 110 | return fmt.Errorf("dummyRawConn closed") 111 | } 112 | return nil 113 | } 114 | 115 | func (d *dummyRawConn) Write(f func(fd uintptr) (done bool)) error { 116 | return nil 117 | } 118 | 119 | func (d *dummyRawConn) Close() { 120 | d.mu.Lock() 121 | d.closed = true 122 | d.mu.Unlock() 123 | } 124 | -------------------------------------------------------------------------------- /internal/pool/pool_single.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import "context" 4 | 5 | type SingleConnPool struct { 6 | pool Pooler 7 | cn *Conn 8 | stickyErr error 9 | } 10 | 11 | var _ Pooler = (*SingleConnPool)(nil) 12 | 13 | func NewSingleConnPool(pool Pooler, cn *Conn) *SingleConnPool { 14 | return &SingleConnPool{ 15 | pool: pool, 16 | cn: cn, 17 | } 18 | } 19 | 20 | func (p *SingleConnPool) NewConn(ctx context.Context) (*Conn, error) { 21 | return p.pool.NewConn(ctx) 22 | } 23 | 24 | func (p *SingleConnPool) CloseConn(cn *Conn) error { 25 | return p.pool.CloseConn(cn) 26 | } 27 | 28 | func (p *SingleConnPool) Get(ctx context.Context) (*Conn, error) { 29 | if p.stickyErr != nil { 30 | return nil, p.stickyErr 31 | } 32 | return p.cn, nil 33 | } 34 | 35 | func (p *SingleConnPool) Put(ctx context.Context, cn *Conn) {} 36 | 37 | func (p *SingleConnPool) Remove(ctx context.Context, cn *Conn, reason error) { 38 | p.cn = nil 39 | p.stickyErr = reason 40 | } 41 | 42 | func (p *SingleConnPool) Close() error { 43 | p.cn = nil 44 | p.stickyErr = ErrClosed 45 | return nil 46 | } 47 | 48 | func (p *SingleConnPool) Len() int { 49 | return 0 50 | } 51 | 52 | func (p *SingleConnPool) IdleLen() int { 53 | return 0 54 | } 55 | 56 | func (p *SingleConnPool) Stats() *Stats { 57 | return &Stats{} 58 | } 59 | -------------------------------------------------------------------------------- /internal/proto/proto_test.go: -------------------------------------------------------------------------------- 1 | package proto_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | ) 9 | 10 | func TestGinkgoSuite(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "proto") 13 | } 14 | -------------------------------------------------------------------------------- /internal/proto/reader_test.go: -------------------------------------------------------------------------------- 1 | package proto_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/redis/go-redis/v9/internal/proto" 9 | ) 10 | 11 | func BenchmarkReader_ParseReply_Status(b *testing.B) { 12 | benchmarkParseReply(b, "+OK\r\n", false) 13 | } 14 | 15 | func BenchmarkReader_ParseReply_Int(b *testing.B) { 16 | benchmarkParseReply(b, ":1\r\n", false) 17 | } 18 | 19 | func BenchmarkReader_ParseReply_Float(b *testing.B) { 20 | benchmarkParseReply(b, ",123.456\r\n", false) 21 | } 22 | 23 | func BenchmarkReader_ParseReply_Bool(b *testing.B) { 24 | benchmarkParseReply(b, "#t\r\n", false) 25 | } 26 | 27 | func BenchmarkReader_ParseReply_BigInt(b *testing.B) { 28 | benchmarkParseReply(b, "(3492890328409238509324850943850943825024385\r\n", false) 29 | } 30 | 31 | func BenchmarkReader_ParseReply_Error(b *testing.B) { 32 | benchmarkParseReply(b, "-Error message\r\n", true) 33 | } 34 | 35 | func BenchmarkReader_ParseReply_Nil(b *testing.B) { 36 | benchmarkParseReply(b, "_\r\n", true) 37 | } 38 | 39 | func BenchmarkReader_ParseReply_BlobError(b *testing.B) { 40 | benchmarkParseReply(b, "!21\r\nSYNTAX invalid syntax", true) 41 | } 42 | 43 | func BenchmarkReader_ParseReply_String(b *testing.B) { 44 | benchmarkParseReply(b, "$5\r\nhello\r\n", false) 45 | } 46 | 47 | func BenchmarkReader_ParseReply_Verb(b *testing.B) { 48 | benchmarkParseReply(b, "$9\r\ntxt:hello\r\n", false) 49 | } 50 | 51 | func BenchmarkReader_ParseReply_Slice(b *testing.B) { 52 | benchmarkParseReply(b, "*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n", false) 53 | } 54 | 55 | func BenchmarkReader_ParseReply_Set(b *testing.B) { 56 | benchmarkParseReply(b, "~2\r\n$5\r\nhello\r\n$5\r\nworld\r\n", false) 57 | } 58 | 59 | func BenchmarkReader_ParseReply_Push(b *testing.B) { 60 | benchmarkParseReply(b, ">2\r\n$5\r\nhello\r\n$5\r\nworld\r\n", false) 61 | } 62 | 63 | func BenchmarkReader_ParseReply_Map(b *testing.B) { 64 | benchmarkParseReply(b, "%2\r\n$5\r\nhello\r\n$5\r\nworld\r\n+key\r\n+value\r\n", false) 65 | } 66 | 67 | func BenchmarkReader_ParseReply_Attr(b *testing.B) { 68 | benchmarkParseReply(b, "%1\r\n+key\r\n+value\r\n+hello\r\n", false) 69 | } 70 | 71 | func TestReader_ReadLine(t *testing.T) { 72 | original := bytes.Repeat([]byte("a"), 8192) 73 | original[len(original)-2] = '\r' 74 | original[len(original)-1] = '\n' 75 | r := proto.NewReader(bytes.NewReader(original)) 76 | read, err := r.ReadLine() 77 | if err != nil && err != io.EOF { 78 | t.Errorf("Should be able to read the full buffer: %v", err) 79 | } 80 | 81 | if !bytes.Equal(read, original[:len(original)-2]) { 82 | t.Errorf("Values must be equal: %d expected %d", len(read), len(original[:len(original)-2])) 83 | } 84 | } 85 | 86 | func benchmarkParseReply(b *testing.B, reply string, wanterr bool) { 87 | buf := new(bytes.Buffer) 88 | for i := 0; i < b.N; i++ { 89 | buf.WriteString(reply) 90 | } 91 | p := proto.NewReader(buf) 92 | b.ResetTimer() 93 | 94 | for i := 0; i < b.N; i++ { 95 | _, err := p.ReadReply() 96 | if !wanterr && err != nil { 97 | b.Fatal(err) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/proto/scan_test.go: -------------------------------------------------------------------------------- 1 | package proto_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | 9 | "github.com/redis/go-redis/v9/internal/proto" 10 | ) 11 | 12 | type testScanSliceStruct struct { 13 | ID int 14 | Name string 15 | } 16 | 17 | func (s *testScanSliceStruct) MarshalBinary() ([]byte, error) { 18 | return json.Marshal(s) 19 | } 20 | 21 | func (s *testScanSliceStruct) UnmarshalBinary(b []byte) error { 22 | return json.Unmarshal(b, s) 23 | } 24 | 25 | var _ = Describe("ScanSlice", func() { 26 | data := []string{ 27 | `{"ID":-1,"Name":"Back Yu"}`, 28 | `{"ID":1,"Name":"szyhf"}`, 29 | } 30 | 31 | It("[]testScanSliceStruct", func() { 32 | var slice []testScanSliceStruct 33 | err := proto.ScanSlice(data, &slice) 34 | Expect(err).NotTo(HaveOccurred()) 35 | Expect(slice).To(Equal([]testScanSliceStruct{ 36 | {-1, "Back Yu"}, 37 | {1, "szyhf"}, 38 | })) 39 | }) 40 | 41 | It("var testContainer []*testScanSliceStruct", func() { 42 | var slice []*testScanSliceStruct 43 | err := proto.ScanSlice(data, &slice) 44 | Expect(err).NotTo(HaveOccurred()) 45 | Expect(slice).To(Equal([]*testScanSliceStruct{ 46 | {-1, "Back Yu"}, 47 | {1, "szyhf"}, 48 | })) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /internal/rand/rand.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | ) 7 | 8 | // Int returns a non-negative pseudo-random int. 9 | func Int() int { return pseudo.Int() } 10 | 11 | // Intn returns, as an int, a non-negative pseudo-random number in [0,n). 12 | // It panics if n <= 0. 13 | func Intn(n int) int { return pseudo.Intn(n) } 14 | 15 | // Int63n returns, as an int64, a non-negative pseudo-random number in [0,n). 16 | // It panics if n <= 0. 17 | func Int63n(n int64) int64 { return pseudo.Int63n(n) } 18 | 19 | // Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n). 20 | func Perm(n int) []int { return pseudo.Perm(n) } 21 | 22 | // Seed uses the provided seed value to initialize the default Source to a 23 | // deterministic state. If Seed is not called, the generator behaves as if 24 | // seeded by Seed(1). 25 | func Seed(n int64) { pseudo.Seed(n) } 26 | 27 | var pseudo = rand.New(&source{src: rand.NewSource(1)}) 28 | 29 | type source struct { 30 | src rand.Source 31 | mu sync.Mutex 32 | } 33 | 34 | func (s *source) Int63() int64 { 35 | s.mu.Lock() 36 | n := s.src.Int63() 37 | s.mu.Unlock() 38 | return n 39 | } 40 | 41 | func (s *source) Seed(seed int64) { 42 | s.mu.Lock() 43 | s.src.Seed(seed) 44 | s.mu.Unlock() 45 | } 46 | 47 | // Shuffle pseudo-randomizes the order of elements. 48 | // n is the number of elements. 49 | // swap swaps the elements with indexes i and j. 50 | func Shuffle(n int, swap func(i, j int)) { pseudo.Shuffle(n, swap) } 51 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9/internal/util" 11 | ) 12 | 13 | func Sleep(ctx context.Context, dur time.Duration) error { 14 | t := time.NewTimer(dur) 15 | defer t.Stop() 16 | 17 | select { 18 | case <-t.C: 19 | return nil 20 | case <-ctx.Done(): 21 | return ctx.Err() 22 | } 23 | } 24 | 25 | func ToLower(s string) string { 26 | if isLower(s) { 27 | return s 28 | } 29 | 30 | b := make([]byte, len(s)) 31 | for i := range b { 32 | c := s[i] 33 | if c >= 'A' && c <= 'Z' { 34 | c += 'a' - 'A' 35 | } 36 | b[i] = c 37 | } 38 | return util.BytesToString(b) 39 | } 40 | 41 | func isLower(s string) bool { 42 | for i := 0; i < len(s); i++ { 43 | c := s[i] 44 | if c >= 'A' && c <= 'Z' { 45 | return false 46 | } 47 | } 48 | return true 49 | } 50 | 51 | func ReplaceSpaces(s string) string { 52 | return strings.ReplaceAll(s, " ", "-") 53 | } 54 | 55 | func GetAddr(addr string) string { 56 | ind := strings.LastIndexByte(addr, ':') 57 | if ind == -1 { 58 | return "" 59 | } 60 | 61 | if strings.IndexByte(addr, '.') != -1 { 62 | return addr 63 | } 64 | 65 | if addr[0] == '[' { 66 | return addr 67 | } 68 | return net.JoinHostPort(addr[:ind], addr[ind+1:]) 69 | } 70 | 71 | func ToInteger(val interface{}) int { 72 | switch v := val.(type) { 73 | case int: 74 | return v 75 | case int64: 76 | return int(v) 77 | case string: 78 | i, _ := strconv.Atoi(v) 79 | return i 80 | default: 81 | return 0 82 | } 83 | } 84 | 85 | func ToFloat(val interface{}) float64 { 86 | switch v := val.(type) { 87 | case float64: 88 | return v 89 | case string: 90 | f, _ := strconv.ParseFloat(v, 64) 91 | return f 92 | default: 93 | return 0.0 94 | } 95 | } 96 | 97 | func ToString(val interface{}) string { 98 | if str, ok := val.(string); ok { 99 | return str 100 | } 101 | return "" 102 | } 103 | 104 | func ToStringSlice(val interface{}) []string { 105 | if arr, ok := val.([]interface{}); ok { 106 | result := make([]string, len(arr)) 107 | for i, v := range arr { 108 | result[i] = ToString(v) 109 | } 110 | return result 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /internal/util/convert.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | ) 8 | 9 | // ParseFloat parses a Redis RESP3 float reply into a Go float64, 10 | // handling "inf", "-inf", "nan" per Redis conventions. 11 | func ParseStringToFloat(s string) (float64, error) { 12 | switch s { 13 | case "inf": 14 | return math.Inf(1), nil 15 | case "-inf": 16 | return math.Inf(-1), nil 17 | case "nan", "-nan": 18 | return math.NaN(), nil 19 | } 20 | return strconv.ParseFloat(s, 64) 21 | } 22 | 23 | // MustParseFloat is like ParseFloat but panics on parse errors. 24 | func MustParseFloat(s string) float64 { 25 | f, err := ParseStringToFloat(s) 26 | if err != nil { 27 | panic(fmt.Sprintf("redis: failed to parse float %q: %v", s, err)) 28 | } 29 | return f 30 | } 31 | -------------------------------------------------------------------------------- /internal/util/convert_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestParseStringToFloat(t *testing.T) { 9 | tests := []struct { 10 | in string 11 | want float64 12 | ok bool 13 | }{ 14 | {"1.23", 1.23, true}, 15 | {"inf", math.Inf(1), true}, 16 | {"-inf", math.Inf(-1), true}, 17 | {"nan", math.NaN(), true}, 18 | {"oops", 0, false}, 19 | } 20 | 21 | for _, tc := range tests { 22 | got, err := ParseStringToFloat(tc.in) 23 | if tc.ok { 24 | if err != nil { 25 | t.Fatalf("ParseFloat(%q) error: %v", tc.in, err) 26 | } 27 | if math.IsNaN(tc.want) { 28 | if !math.IsNaN(got) { 29 | t.Errorf("ParseFloat(%q) = %v; want NaN", tc.in, got) 30 | } 31 | } else if got != tc.want { 32 | t.Errorf("ParseFloat(%q) = %v; want %v", tc.in, got, tc.want) 33 | } 34 | } else { 35 | if err == nil { 36 | t.Errorf("ParseFloat(%q) expected error, got nil", tc.in) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/util/safe.go: -------------------------------------------------------------------------------- 1 | //go:build appengine 2 | 3 | package util 4 | 5 | func BytesToString(b []byte) string { 6 | return string(b) 7 | } 8 | 9 | func StringToBytes(s string) []byte { 10 | return []byte(s) 11 | } 12 | -------------------------------------------------------------------------------- /internal/util/strconv.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strconv" 4 | 5 | func Atoi(b []byte) (int, error) { 6 | return strconv.Atoi(BytesToString(b)) 7 | } 8 | 9 | func ParseInt(b []byte, base int, bitSize int) (int64, error) { 10 | return strconv.ParseInt(BytesToString(b), base, bitSize) 11 | } 12 | 13 | func ParseUint(b []byte, base int, bitSize int) (uint64, error) { 14 | return strconv.ParseUint(BytesToString(b), base, bitSize) 15 | } 16 | 17 | func ParseFloat(b []byte, bitSize int) (float64, error) { 18 | return strconv.ParseFloat(BytesToString(b), bitSize) 19 | } 20 | -------------------------------------------------------------------------------- /internal/util/strconv_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestAtoi(t *testing.T) { 9 | tests := []struct { 10 | input []byte 11 | expected int 12 | wantErr bool 13 | }{ 14 | {[]byte("123"), 123, false}, 15 | {[]byte("-456"), -456, false}, 16 | {[]byte("abc"), 0, true}, 17 | } 18 | 19 | for _, tt := range tests { 20 | result, err := Atoi(tt.input) 21 | if (err != nil) != tt.wantErr { 22 | t.Errorf("Atoi(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) 23 | } 24 | if result != tt.expected && !tt.wantErr { 25 | t.Errorf("Atoi(%q) = %d, want %d", tt.input, result, tt.expected) 26 | } 27 | } 28 | } 29 | 30 | func TestParseInt(t *testing.T) { 31 | tests := []struct { 32 | input []byte 33 | base int 34 | bitSize int 35 | expected int64 36 | wantErr bool 37 | }{ 38 | {[]byte("123"), 10, 64, 123, false}, 39 | {[]byte("-7F"), 16, 64, -127, false}, 40 | {[]byte("zzz"), 36, 64, 46655, false}, 41 | {[]byte("invalid"), 10, 64, 0, true}, 42 | } 43 | 44 | for _, tt := range tests { 45 | result, err := ParseInt(tt.input, tt.base, tt.bitSize) 46 | if (err != nil) != tt.wantErr { 47 | t.Errorf("ParseInt(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr) 48 | } 49 | if result != tt.expected && !tt.wantErr { 50 | t.Errorf("ParseInt(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected) 51 | } 52 | } 53 | } 54 | 55 | func TestParseUint(t *testing.T) { 56 | tests := []struct { 57 | input []byte 58 | base int 59 | bitSize int 60 | expected uint64 61 | wantErr bool 62 | }{ 63 | {[]byte("255"), 10, 8, 255, false}, 64 | {[]byte("FF"), 16, 16, 255, false}, 65 | {[]byte("-1"), 10, 8, 0, true}, // negative should error for unsigned 66 | } 67 | 68 | for _, tt := range tests { 69 | result, err := ParseUint(tt.input, tt.base, tt.bitSize) 70 | if (err != nil) != tt.wantErr { 71 | t.Errorf("ParseUint(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr) 72 | } 73 | if result != tt.expected && !tt.wantErr { 74 | t.Errorf("ParseUint(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected) 75 | } 76 | } 77 | } 78 | 79 | func TestParseFloat(t *testing.T) { 80 | tests := []struct { 81 | input []byte 82 | bitSize int 83 | expected float64 84 | wantErr bool 85 | }{ 86 | {[]byte("3.14"), 64, 3.14, false}, 87 | {[]byte("-2.71"), 64, -2.71, false}, 88 | {[]byte("NaN"), 64, math.NaN(), false}, 89 | {[]byte("invalid"), 64, 0, true}, 90 | } 91 | 92 | for _, tt := range tests { 93 | result, err := ParseFloat(tt.input, tt.bitSize) 94 | if (err != nil) != tt.wantErr { 95 | t.Errorf("ParseFloat(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) 96 | } 97 | if !tt.wantErr && !(math.IsNaN(tt.expected) && math.IsNaN(result)) && result != tt.expected { 98 | t.Errorf("ParseFloat(%q) = %v, want %v", tt.input, result, tt.expected) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/util/type.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func ToPtr[T any](v T) *T { 4 | return &v 5 | } 6 | -------------------------------------------------------------------------------- /internal/util/unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !appengine 2 | 3 | package util 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | // BytesToString converts byte slice to string. 10 | func BytesToString(b []byte) string { 11 | return *(*string)(unsafe.Pointer(&b)) 12 | } 13 | 14 | // StringToBytes converts string to byte slice. 15 | func StringToBytes(s string) []byte { 16 | return *(*[]byte)(unsafe.Pointer( 17 | &struct { 18 | string 19 | Cap int 20 | }{s, len(s)}, 21 | )) 22 | } 23 | -------------------------------------------------------------------------------- /internal/util_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "testing" 7 | 8 | . "github.com/bsm/ginkgo/v2" 9 | . "github.com/bsm/gomega" 10 | ) 11 | 12 | func BenchmarkToLowerStd(b *testing.B) { 13 | str := "AaBbCcDdEeFfGgHhIiJjKk" 14 | for i := 0; i < b.N; i++ { 15 | _ = strings.ToLower(str) 16 | } 17 | } 18 | 19 | // util.ToLower is 3x faster than strings.ToLower. 20 | func BenchmarkToLowerInternal(b *testing.B) { 21 | str := "AaBbCcDdEeFfGgHhIiJjKk" 22 | for i := 0; i < b.N; i++ { 23 | _ = ToLower(str) 24 | } 25 | } 26 | 27 | func TestToLower(t *testing.T) { 28 | It("toLower", func() { 29 | str := "AaBbCcDdEeFfGg" 30 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 31 | 32 | str = "ABCDE" 33 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 34 | 35 | str = "ABCDE" 36 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 37 | 38 | str = "abced" 39 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 40 | }) 41 | } 42 | 43 | func TestIsLower(t *testing.T) { 44 | It("isLower", func() { 45 | str := "AaBbCcDdEeFfGg" 46 | Expect(isLower(str)).To(BeFalse()) 47 | 48 | str = "ABCDE" 49 | Expect(isLower(str)).To(BeFalse()) 50 | 51 | str = "abcdefg" 52 | Expect(isLower(str)).To(BeTrue()) 53 | }) 54 | } 55 | 56 | func TestGetAddr(t *testing.T) { 57 | It("getAddr", func() { 58 | str := "127.0.0.1:1234" 59 | Expect(GetAddr(str)).To(Equal(str)) 60 | 61 | str = "[::1]:1234" 62 | Expect(GetAddr(str)).To(Equal(str)) 63 | 64 | str = "[fd01:abcd::7d03]:6379" 65 | Expect(GetAddr(str)).To(Equal(str)) 66 | 67 | Expect(GetAddr("::1:1234")).To(Equal("[::1]:1234")) 68 | 69 | Expect(GetAddr("fd01:abcd::7d03:6379")).To(Equal("[fd01:abcd::7d03]:6379")) 70 | 71 | Expect(GetAddr("127.0.0.1")).To(Equal("")) 72 | 73 | Expect(GetAddr("127")).To(Equal("")) 74 | }) 75 | } 76 | 77 | func BenchmarkReplaceSpaces(b *testing.B) { 78 | version := runtime.Version() 79 | for i := 0; i < b.N; i++ { 80 | _ = ReplaceSpaces(version) 81 | } 82 | } 83 | 84 | func ReplaceSpacesUseBuilder(s string) string { 85 | // Pre-allocate a builder with the same length as s to minimize allocations. 86 | // This is a basic optimization; adjust the initial size based on your use case. 87 | var builder strings.Builder 88 | builder.Grow(len(s)) 89 | 90 | for _, char := range s { 91 | if char == ' ' { 92 | // Replace space with a hyphen. 93 | builder.WriteRune('-') 94 | } else { 95 | // Copy the character as-is. 96 | builder.WriteRune(char) 97 | } 98 | } 99 | 100 | return builder.String() 101 | } 102 | 103 | func BenchmarkReplaceSpacesUseBuilder(b *testing.B) { 104 | version := runtime.Version() 105 | for i := 0; i < b.N; i++ { 106 | _ = ReplaceSpacesUseBuilder(version) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // ScanIterator is used to incrementally iterate over a collection of elements. 8 | type ScanIterator struct { 9 | cmd *ScanCmd 10 | pos int 11 | } 12 | 13 | // Err returns the last iterator error, if any. 14 | func (it *ScanIterator) Err() error { 15 | return it.cmd.Err() 16 | } 17 | 18 | // Next advances the cursor and returns true if more values can be read. 19 | func (it *ScanIterator) Next(ctx context.Context) bool { 20 | // Instantly return on errors. 21 | if it.cmd.Err() != nil { 22 | return false 23 | } 24 | 25 | // Advance cursor, check if we are still within range. 26 | if it.pos < len(it.cmd.page) { 27 | it.pos++ 28 | return true 29 | } 30 | 31 | for { 32 | // Return if there is no more data to fetch. 33 | if it.cmd.cursor == 0 { 34 | return false 35 | } 36 | 37 | // Fetch next page. 38 | switch it.cmd.args[0] { 39 | case "scan", "qscan": 40 | it.cmd.args[1] = it.cmd.cursor 41 | default: 42 | it.cmd.args[2] = it.cmd.cursor 43 | } 44 | 45 | err := it.cmd.process(ctx, it.cmd) 46 | if err != nil { 47 | return false 48 | } 49 | 50 | it.pos = 1 51 | 52 | // Redis can occasionally return empty page. 53 | if len(it.cmd.page) > 0 { 54 | return true 55 | } 56 | } 57 | } 58 | 59 | // Val returns the key/field at the current cursor position. 60 | func (it *ScanIterator) Val() string { 61 | var v string 62 | if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) { 63 | v = it.cmd.page[it.pos-1] 64 | } 65 | return v 66 | } 67 | -------------------------------------------------------------------------------- /monitor_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | . "github.com/bsm/ginkgo/v2" 11 | . "github.com/bsm/gomega" 12 | 13 | "github.com/redis/go-redis/v9" 14 | ) 15 | 16 | // This test is for manual use and is not part of the CI of Go-Redis. 17 | var _ = Describe("Monitor command", Label("monitor"), func() { 18 | ctx := context.TODO() 19 | var client *redis.Client 20 | 21 | BeforeEach(func() { 22 | if os.Getenv("RUN_MONITOR_TEST") != "true" { 23 | Skip("Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.") 24 | } 25 | client = redis.NewClient(&redis.Options{Addr: redisPort}) 26 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 27 | 28 | }) 29 | 30 | AfterEach(func() { 31 | Expect(client.Close()).NotTo(HaveOccurred()) 32 | }) 33 | 34 | It("should monitor", Label("monitor"), func() { 35 | ress := make(chan string) 36 | client1 := redis.NewClient(&redis.Options{Addr: redisPort}) 37 | mn := client1.Monitor(ctx, ress) 38 | mn.Start() 39 | // Wait for the Redis server to be in monitoring mode. 40 | time.Sleep(100 * time.Millisecond) 41 | client.Set(ctx, "foo", "bar", 0) 42 | client.Set(ctx, "bar", "baz", 0) 43 | client.Set(ctx, "bap", 8, 0) 44 | client.Get(ctx, "bap") 45 | lst := []string{} 46 | for i := 0; i < 5; i++ { 47 | s := <-ress 48 | lst = append(lst, s) 49 | } 50 | mn.Stop() 51 | Expect(lst[0]).To(ContainSubstring("OK")) 52 | Expect(lst[1]).To(ContainSubstring(`"set" "foo" "bar"`)) 53 | Expect(lst[2]).To(ContainSubstring(`"set" "bar" "baz"`)) 54 | Expect(lst[3]).To(ContainSubstring(`"set" "bap" "8"`)) 55 | }) 56 | }) 57 | 58 | func TestMonitorCommand(t *testing.T) { 59 | if os.Getenv("RUN_MONITOR_TEST") != "true" { 60 | t.Skip("Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.") 61 | } 62 | 63 | ctx := context.TODO() 64 | client := redis.NewClient(&redis.Options{Addr: redisPort}) 65 | if err := client.FlushDB(ctx).Err(); err != nil { 66 | t.Fatalf("FlushDB failed: %v", err) 67 | } 68 | 69 | defer func() { 70 | if err := client.Close(); err != nil { 71 | t.Fatalf("Close failed: %v", err) 72 | } 73 | }() 74 | 75 | ress := make(chan string, 10) // Buffer to prevent blocking 76 | client1 := redis.NewClient(&redis.Options{Addr: redisPort}) // Adjust the Addr field as necessary 77 | mn := client1.Monitor(ctx, ress) 78 | mn.Start() 79 | // Wait for the Redis server to be in monitoring mode. 80 | time.Sleep(100 * time.Millisecond) 81 | client.Set(ctx, "foo", "bar", 0) 82 | client.Set(ctx, "bar", "baz", 0) 83 | client.Set(ctx, "bap", 8, 0) 84 | client.Get(ctx, "bap") 85 | mn.Stop() 86 | var lst []string 87 | for i := 0; i < 5; i++ { 88 | s := <-ress 89 | lst = append(lst, s) 90 | } 91 | 92 | // Assertions 93 | if !containsSubstring(lst[0], "OK") { 94 | t.Errorf("Expected lst[0] to contain 'OK', got %s", lst[0]) 95 | } 96 | if !containsSubstring(lst[1], `"set" "foo" "bar"`) { 97 | t.Errorf(`Expected lst[1] to contain '"set" "foo" "bar"', got %s`, lst[1]) 98 | } 99 | if !containsSubstring(lst[2], `"set" "bar" "baz"`) { 100 | t.Errorf(`Expected lst[2] to contain '"set" "bar" "baz"', got %s`, lst[2]) 101 | } 102 | if !containsSubstring(lst[3], `"set" "bap" "8"`) { 103 | t.Errorf(`Expected lst[3] to contain '"set" "bap" "8"', got %s`, lst[3]) 104 | } 105 | } 106 | 107 | func containsSubstring(s, substr string) bool { 108 | return strings.Contains(s, substr) 109 | } 110 | -------------------------------------------------------------------------------- /osscluster_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd { 10 | cmd := NewIntCmd(ctx, "dbsize") 11 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 12 | var size int64 13 | err := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error { 14 | n, err := master.DBSize(ctx).Result() 15 | if err != nil { 16 | return err 17 | } 18 | atomic.AddInt64(&size, n) 19 | return nil 20 | }) 21 | if err != nil { 22 | cmd.SetErr(err) 23 | } else { 24 | cmd.val = size 25 | } 26 | return nil 27 | }) 28 | return cmd 29 | } 30 | 31 | func (c *ClusterClient) ScriptLoad(ctx context.Context, script string) *StringCmd { 32 | cmd := NewStringCmd(ctx, "script", "load", script) 33 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 34 | var mu sync.Mutex 35 | err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error { 36 | val, err := shard.ScriptLoad(ctx, script).Result() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | mu.Lock() 42 | if cmd.Val() == "" { 43 | cmd.val = val 44 | } 45 | mu.Unlock() 46 | 47 | return nil 48 | }) 49 | if err != nil { 50 | cmd.SetErr(err) 51 | } 52 | return nil 53 | }) 54 | return cmd 55 | } 56 | 57 | func (c *ClusterClient) ScriptFlush(ctx context.Context) *StatusCmd { 58 | cmd := NewStatusCmd(ctx, "script", "flush") 59 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 60 | err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error { 61 | return shard.ScriptFlush(ctx).Err() 62 | }) 63 | if err != nil { 64 | cmd.SetErr(err) 65 | } 66 | return nil 67 | }) 68 | return cmd 69 | } 70 | 71 | func (c *ClusterClient) ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd { 72 | args := make([]interface{}, 2+len(hashes)) 73 | args[0] = "script" 74 | args[1] = "exists" 75 | for i, hash := range hashes { 76 | args[2+i] = hash 77 | } 78 | cmd := NewBoolSliceCmd(ctx, args...) 79 | 80 | result := make([]bool, len(hashes)) 81 | for i := range result { 82 | result[i] = true 83 | } 84 | 85 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 86 | var mu sync.Mutex 87 | err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error { 88 | val, err := shard.ScriptExists(ctx, hashes...).Result() 89 | if err != nil { 90 | return err 91 | } 92 | 93 | mu.Lock() 94 | for i, v := range val { 95 | result[i] = result[i] && v 96 | } 97 | mu.Unlock() 98 | 99 | return nil 100 | }) 101 | if err != nil { 102 | cmd.SetErr(err) 103 | } else { 104 | cmd.val = result 105 | } 106 | return nil 107 | }) 108 | return cmd 109 | } 110 | -------------------------------------------------------------------------------- /pipeline.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | type pipelineExecer func(context.Context, []Cmder) error 9 | 10 | // Pipeliner is an mechanism to realise Redis Pipeline technique. 11 | // 12 | // Pipelining is a technique to extremely speed up processing by packing 13 | // operations to batches, send them at once to Redis and read a replies in a 14 | // single step. 15 | // See https://redis.io/topics/pipelining 16 | // 17 | // Pay attention, that Pipeline is not a transaction, so you can get unexpected 18 | // results in case of big pipelines and small read/write timeouts. 19 | // Redis client has retransmission logic in case of timeouts, pipeline 20 | // can be retransmitted and commands can be executed more then once. 21 | // To avoid this: it is good idea to use reasonable bigger read/write timeouts 22 | // depends of your batch size and/or use TxPipeline. 23 | type Pipeliner interface { 24 | StatefulCmdable 25 | 26 | // Len is to obtain the number of commands in the pipeline that have not yet been executed. 27 | Len() int 28 | 29 | // Do is an API for executing any command. 30 | // If a certain Redis command is not yet supported, you can use Do to execute it. 31 | Do(ctx context.Context, args ...interface{}) *Cmd 32 | 33 | // Process is to put the commands to be executed into the pipeline buffer. 34 | Process(ctx context.Context, cmd Cmder) error 35 | 36 | // Discard is to discard all commands in the cache that have not yet been executed. 37 | Discard() 38 | 39 | // Exec is to send all the commands buffered in the pipeline to the redis-server. 40 | Exec(ctx context.Context) ([]Cmder, error) 41 | } 42 | 43 | var _ Pipeliner = (*Pipeline)(nil) 44 | 45 | // Pipeline implements pipelining as described in 46 | // http://redis.io/topics/pipelining. 47 | // Please note: it is not safe for concurrent use by multiple goroutines. 48 | type Pipeline struct { 49 | cmdable 50 | statefulCmdable 51 | 52 | exec pipelineExecer 53 | cmds []Cmder 54 | } 55 | 56 | func (c *Pipeline) init() { 57 | c.cmdable = c.Process 58 | c.statefulCmdable = c.Process 59 | } 60 | 61 | // Len returns the number of queued commands. 62 | func (c *Pipeline) Len() int { 63 | return len(c.cmds) 64 | } 65 | 66 | // Do queues the custom command for later execution. 67 | func (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd { 68 | cmd := NewCmd(ctx, args...) 69 | if len(args) == 0 { 70 | cmd.SetErr(errors.New("redis: please enter the command to be executed")) 71 | return cmd 72 | } 73 | _ = c.Process(ctx, cmd) 74 | return cmd 75 | } 76 | 77 | // Process queues the cmd for later execution. 78 | func (c *Pipeline) Process(ctx context.Context, cmd Cmder) error { 79 | c.cmds = append(c.cmds, cmd) 80 | return nil 81 | } 82 | 83 | // Discard resets the pipeline and discards queued commands. 84 | func (c *Pipeline) Discard() { 85 | c.cmds = c.cmds[:0] 86 | } 87 | 88 | // Exec executes all previously queued commands using one 89 | // client-server roundtrip. 90 | // 91 | // Exec always returns list of commands and error of the first failed 92 | // command if any. 93 | func (c *Pipeline) Exec(ctx context.Context) ([]Cmder, error) { 94 | if len(c.cmds) == 0 { 95 | return nil, nil 96 | } 97 | 98 | cmds := c.cmds 99 | c.cmds = nil 100 | 101 | return cmds, c.exec(ctx, cmds) 102 | } 103 | 104 | func (c *Pipeline) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 105 | if err := fn(c); err != nil { 106 | return nil, err 107 | } 108 | return c.Exec(ctx) 109 | } 110 | 111 | func (c *Pipeline) Pipeline() Pipeliner { 112 | return c 113 | } 114 | 115 | func (c *Pipeline) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 116 | return c.Pipelined(ctx, fn) 117 | } 118 | 119 | func (c *Pipeline) TxPipeline() Pipeliner { 120 | return c 121 | } 122 | -------------------------------------------------------------------------------- /pipeline_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | 7 | . "github.com/bsm/ginkgo/v2" 8 | . "github.com/bsm/gomega" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | var _ = Describe("pipelining", func() { 14 | var client *redis.Client 15 | var pipe *redis.Pipeline 16 | 17 | BeforeEach(func() { 18 | client = redis.NewClient(redisOptions()) 19 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 20 | }) 21 | 22 | AfterEach(func() { 23 | Expect(client.Close()).NotTo(HaveOccurred()) 24 | }) 25 | 26 | It("supports block style", func() { 27 | var get *redis.StringCmd 28 | cmds, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error { 29 | get = pipe.Get(ctx, "foo") 30 | return nil 31 | }) 32 | Expect(err).To(Equal(redis.Nil)) 33 | Expect(cmds).To(HaveLen(1)) 34 | Expect(cmds[0]).To(Equal(get)) 35 | Expect(get.Err()).To(Equal(redis.Nil)) 36 | Expect(get.Val()).To(Equal("")) 37 | }) 38 | 39 | assertPipeline := func() { 40 | It("returns no errors when there are no commands", func() { 41 | _, err := pipe.Exec(ctx) 42 | Expect(err).NotTo(HaveOccurred()) 43 | }) 44 | 45 | It("discards queued commands", func() { 46 | pipe.Get(ctx, "key") 47 | pipe.Discard() 48 | cmds, err := pipe.Exec(ctx) 49 | Expect(err).NotTo(HaveOccurred()) 50 | Expect(cmds).To(BeNil()) 51 | }) 52 | 53 | It("handles val/err", func() { 54 | err := client.Set(ctx, "key", "value", 0).Err() 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | get := pipe.Get(ctx, "key") 58 | cmds, err := pipe.Exec(ctx) 59 | Expect(err).NotTo(HaveOccurred()) 60 | Expect(cmds).To(HaveLen(1)) 61 | 62 | val, err := get.Result() 63 | Expect(err).NotTo(HaveOccurred()) 64 | Expect(val).To(Equal("value")) 65 | }) 66 | 67 | It("supports custom command", func() { 68 | pipe.Do(ctx, "ping") 69 | cmds, err := pipe.Exec(ctx) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(cmds).To(HaveLen(1)) 72 | }) 73 | 74 | It("handles large pipelines", Label("NonRedisEnterprise"), func() { 75 | for callCount := 1; callCount < 16; callCount++ { 76 | for i := 1; i <= callCount; i++ { 77 | pipe.SetNX(ctx, strconv.Itoa(i)+"_key", strconv.Itoa(i)+"_value", 0) 78 | } 79 | 80 | cmds, err := pipe.Exec(ctx) 81 | Expect(err).NotTo(HaveOccurred()) 82 | Expect(cmds).To(HaveLen(callCount)) 83 | for _, cmd := range cmds { 84 | Expect(cmd).To(BeAssignableToTypeOf(&redis.BoolCmd{})) 85 | } 86 | } 87 | }) 88 | 89 | It("should Exec, not Do", func() { 90 | err := pipe.Do(ctx).Err() 91 | Expect(err).To(Equal(errors.New("redis: please enter the command to be executed"))) 92 | }) 93 | } 94 | 95 | Describe("Pipeline", func() { 96 | BeforeEach(func() { 97 | pipe = client.Pipeline().(*redis.Pipeline) 98 | }) 99 | 100 | assertPipeline() 101 | }) 102 | 103 | Describe("TxPipeline", func() { 104 | BeforeEach(func() { 105 | pipe = client.TxPipeline().(*redis.Pipeline) 106 | }) 107 | 108 | assertPipeline() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | . "github.com/bsm/ginkgo/v2" 8 | . "github.com/bsm/gomega" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | var _ = Describe("pool", func() { 14 | var client *redis.Client 15 | 16 | BeforeEach(func() { 17 | opt := redisOptions() 18 | opt.MinIdleConns = 0 19 | opt.ConnMaxLifetime = 0 20 | opt.ConnMaxIdleTime = time.Second 21 | client = redis.NewClient(opt) 22 | }) 23 | 24 | AfterEach(func() { 25 | Expect(client.Close()).NotTo(HaveOccurred()) 26 | }) 27 | 28 | It("respects max size", func() { 29 | perform(1000, func(id int) { 30 | val, err := client.Ping(ctx).Result() 31 | Expect(err).NotTo(HaveOccurred()) 32 | Expect(val).To(Equal("PONG")) 33 | }) 34 | 35 | pool := client.Pool() 36 | Expect(pool.Len()).To(BeNumerically("<=", 10)) 37 | Expect(pool.IdleLen()).To(BeNumerically("<=", 10)) 38 | Expect(pool.Len()).To(Equal(pool.IdleLen())) 39 | }) 40 | 41 | It("respects max size on multi", func() { 42 | perform(1000, func(id int) { 43 | var ping *redis.StatusCmd 44 | 45 | err := client.Watch(ctx, func(tx *redis.Tx) error { 46 | cmds, err := tx.Pipelined(ctx, func(pipe redis.Pipeliner) error { 47 | ping = pipe.Ping(ctx) 48 | return nil 49 | }) 50 | Expect(err).NotTo(HaveOccurred()) 51 | Expect(cmds).To(HaveLen(1)) 52 | return err 53 | }) 54 | Expect(err).NotTo(HaveOccurred()) 55 | 56 | Expect(ping.Err()).NotTo(HaveOccurred()) 57 | Expect(ping.Val()).To(Equal("PONG")) 58 | }) 59 | 60 | pool := client.Pool() 61 | Expect(pool.Len()).To(BeNumerically("<=", 10)) 62 | Expect(pool.IdleLen()).To(BeNumerically("<=", 10)) 63 | Expect(pool.Len()).To(Equal(pool.IdleLen())) 64 | }) 65 | 66 | It("respects max size on pipelines", func() { 67 | perform(1000, func(id int) { 68 | pipe := client.Pipeline() 69 | ping := pipe.Ping(ctx) 70 | cmds, err := pipe.Exec(ctx) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(cmds).To(HaveLen(1)) 73 | Expect(ping.Err()).NotTo(HaveOccurred()) 74 | Expect(ping.Val()).To(Equal("PONG")) 75 | }) 76 | 77 | pool := client.Pool() 78 | Expect(pool.Len()).To(BeNumerically("<=", 10)) 79 | Expect(pool.IdleLen()).To(BeNumerically("<=", 10)) 80 | Expect(pool.Len()).To(Equal(pool.IdleLen())) 81 | }) 82 | 83 | It("removes broken connections", func() { 84 | cn, err := client.Pool().Get(context.Background()) 85 | Expect(err).NotTo(HaveOccurred()) 86 | cn.SetNetConn(&badConn{}) 87 | client.Pool().Put(ctx, cn) 88 | 89 | val, err := client.Ping(ctx).Result() 90 | Expect(err).NotTo(HaveOccurred()) 91 | Expect(val).To(Equal("PONG")) 92 | 93 | val, err = client.Ping(ctx).Result() 94 | Expect(err).NotTo(HaveOccurred()) 95 | Expect(val).To(Equal("PONG")) 96 | 97 | pool := client.Pool() 98 | Expect(pool.Len()).To(Equal(1)) 99 | Expect(pool.IdleLen()).To(Equal(1)) 100 | 101 | stats := pool.Stats() 102 | Expect(stats.Hits).To(Equal(uint32(1))) 103 | Expect(stats.Misses).To(Equal(uint32(2))) 104 | Expect(stats.Timeouts).To(Equal(uint32(0))) 105 | }) 106 | 107 | It("reuses connections", func() { 108 | // explain: https://github.com/redis/go-redis/pull/1675 109 | opt := redisOptions() 110 | opt.MinIdleConns = 0 111 | opt.ConnMaxLifetime = 0 112 | opt.ConnMaxIdleTime = 10 * time.Second 113 | client = redis.NewClient(opt) 114 | 115 | for i := 0; i < 100; i++ { 116 | val, err := client.Ping(ctx).Result() 117 | Expect(err).NotTo(HaveOccurred()) 118 | Expect(val).To(Equal("PONG")) 119 | } 120 | 121 | pool := client.Pool() 122 | Expect(pool.Len()).To(Equal(1)) 123 | Expect(pool.IdleLen()).To(Equal(1)) 124 | 125 | stats := pool.Stats() 126 | Expect(stats.Hits).To(Equal(uint32(99))) 127 | Expect(stats.Misses).To(Equal(uint32(1))) 128 | Expect(stats.Timeouts).To(Equal(uint32(0))) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /pubsub_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "context" 4 | 5 | type PubSubCmdable interface { 6 | Publish(ctx context.Context, channel string, message interface{}) *IntCmd 7 | SPublish(ctx context.Context, channel string, message interface{}) *IntCmd 8 | PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd 9 | PubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd 10 | PubSubNumPat(ctx context.Context) *IntCmd 11 | PubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd 12 | PubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd 13 | } 14 | 15 | // Publish posts the message to the channel. 16 | func (c cmdable) Publish(ctx context.Context, channel string, message interface{}) *IntCmd { 17 | cmd := NewIntCmd(ctx, "publish", channel, message) 18 | _ = c(ctx, cmd) 19 | return cmd 20 | } 21 | 22 | func (c cmdable) SPublish(ctx context.Context, channel string, message interface{}) *IntCmd { 23 | cmd := NewIntCmd(ctx, "spublish", channel, message) 24 | _ = c(ctx, cmd) 25 | return cmd 26 | } 27 | 28 | func (c cmdable) PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd { 29 | args := []interface{}{"pubsub", "channels"} 30 | if pattern != "*" { 31 | args = append(args, pattern) 32 | } 33 | cmd := NewStringSliceCmd(ctx, args...) 34 | _ = c(ctx, cmd) 35 | return cmd 36 | } 37 | 38 | func (c cmdable) PubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd { 39 | args := make([]interface{}, 2+len(channels)) 40 | args[0] = "pubsub" 41 | args[1] = "numsub" 42 | for i, channel := range channels { 43 | args[2+i] = channel 44 | } 45 | cmd := NewMapStringIntCmd(ctx, args...) 46 | _ = c(ctx, cmd) 47 | return cmd 48 | } 49 | 50 | func (c cmdable) PubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd { 51 | args := []interface{}{"pubsub", "shardchannels"} 52 | if pattern != "*" { 53 | args = append(args, pattern) 54 | } 55 | cmd := NewStringSliceCmd(ctx, args...) 56 | _ = c(ctx, cmd) 57 | return cmd 58 | } 59 | 60 | func (c cmdable) PubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd { 61 | args := make([]interface{}, 2+len(channels)) 62 | args[0] = "pubsub" 63 | args[1] = "shardnumsub" 64 | for i, channel := range channels { 65 | args[2+i] = channel 66 | } 67 | cmd := NewMapStringIntCmd(ctx, args...) 68 | _ = c(ctx, cmd) 69 | return cmd 70 | } 71 | 72 | func (c cmdable) PubSubNumPat(ctx context.Context) *IntCmd { 73 | cmd := NewIntCmd(ctx, "pubsub", "numpat") 74 | _ = c(ctx, cmd) 75 | return cmd 76 | } 77 | -------------------------------------------------------------------------------- /script.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "io" 8 | ) 9 | 10 | type Scripter interface { 11 | Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd 12 | EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd 13 | EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd 14 | EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd 15 | ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd 16 | ScriptLoad(ctx context.Context, script string) *StringCmd 17 | } 18 | 19 | var ( 20 | _ Scripter = (*Client)(nil) 21 | _ Scripter = (*Ring)(nil) 22 | _ Scripter = (*ClusterClient)(nil) 23 | ) 24 | 25 | type Script struct { 26 | src, hash string 27 | } 28 | 29 | func NewScript(src string) *Script { 30 | h := sha1.New() 31 | _, _ = io.WriteString(h, src) 32 | return &Script{ 33 | src: src, 34 | hash: hex.EncodeToString(h.Sum(nil)), 35 | } 36 | } 37 | 38 | func (s *Script) Hash() string { 39 | return s.hash 40 | } 41 | 42 | func (s *Script) Load(ctx context.Context, c Scripter) *StringCmd { 43 | return c.ScriptLoad(ctx, s.src) 44 | } 45 | 46 | func (s *Script) Exists(ctx context.Context, c Scripter) *BoolSliceCmd { 47 | return c.ScriptExists(ctx, s.hash) 48 | } 49 | 50 | func (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 51 | return c.Eval(ctx, s.src, keys, args...) 52 | } 53 | 54 | func (s *Script) EvalRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 55 | return c.EvalRO(ctx, s.src, keys, args...) 56 | } 57 | 58 | func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 59 | return c.EvalSha(ctx, s.hash, keys, args...) 60 | } 61 | 62 | func (s *Script) EvalShaRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 63 | return c.EvalShaRO(ctx, s.hash, keys, args...) 64 | } 65 | 66 | // Run optimistically uses EVALSHA to run the script. If script does not exist 67 | // it is retried using EVAL. 68 | func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 69 | r := s.EvalSha(ctx, c, keys, args...) 70 | if HasErrorPrefix(r.Err(), "NOSCRIPT") { 71 | return s.Eval(ctx, c, keys, args...) 72 | } 73 | return r 74 | } 75 | 76 | // RunRO optimistically uses EVALSHA_RO to run the script. If script does not exist 77 | // it is retried using EVAL_RO. 78 | func (s *Script) RunRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 79 | r := s.EvalShaRO(ctx, c, keys, args...) 80 | if HasErrorPrefix(r.Err(), "NOSCRIPT") { 81 | return s.EvalRO(ctx, c, keys, args...) 82 | } 83 | return r 84 | } 85 | -------------------------------------------------------------------------------- /scripts/bump_deps.sh: -------------------------------------------------------------------------------- 1 | PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ 2 | | sed 's/^\.\///' \ 3 | | sort) 4 | 5 | for dir in $PACKAGE_DIRS 6 | do 7 | printf "${dir}: go get -d && go mod tidy\n" 8 | (cd ./${dir} && go get -d && go mod tidy) 9 | done 10 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | help() { 6 | cat <<- EOF 7 | Usage: TAG=tag $0 8 | 9 | Updates version in go.mod files and pushes a new brash to GitHub. 10 | 11 | VARIABLES: 12 | TAG git tag, for example, v1.0.0 13 | EOF 14 | exit 0 15 | } 16 | 17 | if [ -z "$TAG" ] 18 | then 19 | printf "TAG is required\n\n" 20 | help 21 | fi 22 | 23 | TAG_REGEX="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" 24 | if ! [[ "${TAG}" =~ ${TAG_REGEX} ]]; then 25 | printf "TAG is not valid: ${TAG}\n\n" 26 | exit 1 27 | fi 28 | 29 | TAG_FOUND=`git tag --list ${TAG}` 30 | if [[ ${TAG_FOUND} = ${TAG} ]] ; then 31 | printf "tag ${TAG} already exists\n\n" 32 | exit 1 33 | fi 34 | 35 | if ! git diff --quiet 36 | then 37 | printf "working tree is not clean\n\n" 38 | git status 39 | exit 1 40 | fi 41 | 42 | git checkout master 43 | 44 | PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ 45 | | sed 's/^\.\///' \ 46 | | sort) 47 | 48 | for dir in $PACKAGE_DIRS 49 | do 50 | printf "${dir}: go get -u && go mod tidy\n" 51 | #(cd ./${dir} && go get -u && go mod tidy -compat=1.18) 52 | done 53 | 54 | for dir in $PACKAGE_DIRS 55 | do 56 | sed --in-place \ 57 | "s/redis\/go-redis\([^ ]*\) v.*/redis\/go-redis\1 ${TAG}/" "${dir}/go.mod" 58 | #(cd ./${dir} && go get -u && go mod tidy -compat=1.18) 59 | (cd ./${dir} && go mod tidy -compat=1.18) 60 | done 61 | 62 | sed --in-place "s/\(return \)\"[^\"]*\"/\1\"${TAG#v}\"/" ./version.go 63 | 64 | git checkout -b release/${TAG} master 65 | git add -u 66 | git commit -m "chore: release $TAG (release.sh)" 67 | git push origin release/${TAG} 68 | -------------------------------------------------------------------------------- /scripts/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | help() { 6 | cat <<- EOF 7 | Usage: TAG=tag $0 8 | 9 | Creates git tags for public Go packages. 10 | 11 | VARIABLES: 12 | TAG git tag, for example, v1.0.0 13 | EOF 14 | exit 0 15 | } 16 | 17 | if [ -z "$TAG" ] 18 | then 19 | printf "TAG env var is required\n\n"; 20 | help 21 | fi 22 | 23 | if ! grep -Fq "\"${TAG#v}\"" version.go 24 | then 25 | printf "version.go does not contain ${TAG#v}\n" 26 | exit 1 27 | fi 28 | 29 | PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ 30 | | grep -E -v "example|internal" \ 31 | | sed 's/^\.\///' \ 32 | | sort) 33 | 34 | git tag ${TAG} 35 | git push origin ${TAG} 36 | 37 | for dir in $PACKAGE_DIRS 38 | do 39 | printf "tagging ${dir}/${TAG}\n" 40 | git tag ${dir}/${TAG} 41 | git push origin ${dir}/${TAG} 42 | done 43 | -------------------------------------------------------------------------------- /unit_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // mockCmdable is a mock implementation of cmdable that records the last command. 8 | // This is used for unit testing command construction without requiring a Redis server. 9 | type mockCmdable struct { 10 | lastCmd Cmder 11 | returnErr error 12 | } 13 | 14 | func (m *mockCmdable) call(ctx context.Context, cmd Cmder) error { 15 | m.lastCmd = cmd 16 | if m.returnErr != nil { 17 | cmd.SetErr(m.returnErr) 18 | } 19 | return m.returnErr 20 | } 21 | 22 | func (m *mockCmdable) asCmdable() cmdable { 23 | return func(ctx context.Context, cmd Cmder) error { 24 | return m.call(ctx, cmd) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | // Version is the current release version. 4 | func Version() string { 5 | return "9.10.0" 6 | } 7 | --------------------------------------------------------------------------------