[
  {
    "path": ".github/CODEOWNERS",
    "content": "doctests/* @dmaier-redislabs\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: ['https://uptrace.dev/sponsor']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\nIssue tracker is used for reporting bugs and discussing new features. Please use\n[stackoverflow](https://stackoverflow.com) for supporting issues.\n\n<!--- Provide a general summary of the issue in the Title above -->\n\n## Expected Behavior\n\n<!--- Tell us what should happen -->\n\n## Current Behavior\n\n<!--- Tell us what happens instead of the expected behavior -->\n\n## Possible Solution\n\n<!--- Not obligatory, but suggest a fix/reason for the bug, -->\n\n## Steps to Reproduce\n\n<!--- Provide a link to a live example, or an unambiguous set of steps to -->\n<!--- reproduce this bug. Include code to reproduce, if relevant -->\n\n1.\n2.\n3.\n4.\n\n## Context (Environment)\n\n<!--- How has this issue affected you? What are you trying to accomplish? -->\n<!--- Providing context helps us come up with a solution that is most useful in the real world -->\n\n<!--- Provide a general summary of the issue in the Title above -->\n\n## Detailed Description\n\n<!--- Provide a detailed description of the change or addition you are proposing -->\n\n## Possible Implementation\n\n<!--- Not obligatory, but suggest an idea for implementing addition or change -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Discussions\n    url: https://github.com/go-redis/redis/discussions\n    about: Ask a question via GitHub Discussions\n"
  },
  {
    "path": ".github/RELEASE_NOTES_TEMPLATE.md",
    "content": "# Release Notes Template for go-redis\n\nThis template provides a structured format for creating release notes for go-redis releases.\n\n## Format Structure\n\n```markdown\n# X.Y.Z (YYYY-MM-DD)\n\n## 🚀 Highlights\n\n### [Category Name]\nBrief description of the major feature/change with context and impact.\n- Key points\n- Performance metrics if applicable\n- Links to documentation\n\n### [Another Category]\n...\n\n## ✨ New Features\n\n- Feature description ([#XXXX](https://github.com/redis/go-redis/pull/XXXX)) by [@username](https://github.com/username)\n- ...\n\n## 🐛 Bug Fixes\n\n- Fix description ([#XXXX](https://github.com/redis/go-redis/pull/XXXX)) by [@username](https://github.com/username)\n- ...\n\n## ⚡ Performance\n\n- Performance improvement description ([#XXXX](https://github.com/redis/go-redis/pull/XXXX)) by [@username](https://github.com/username)\n- ...\n\n## 🧪 Testing & Infrastructure\n\n- Testing/CI improvement ([#XXXX](https://github.com/redis/go-redis/pull/XXXX)) by [@username](https://github.com/username)\n- ...\n\n## 👥 Contributors\n\nWe'd like to thank all the contributors who worked on this release!\n\n[@username1](https://github.com/username1), [@username2](https://github.com/username2), ...\n\n---\n\n**Full Changelog**: https://github.com/redis/go-redis/compare/vX.Y-1.Z...vX.Y.Z\n```\n\n## Guidelines\n\n### Highlights Section\nThe Highlights section should contain the **most important** user-facing changes. Common categories include:\n\n- **Typed Errors** - Error handling improvements\n- **New Commands** - New Redis commands support (especially for new Redis versions)\n- **Search & Vector** - RediSearch and vector-related features\n- **Connection Pool** - Pool improvements and performance\n- **Metrics & Observability** - Monitoring and instrumentation\n- **Breaking Changes** - Any breaking changes (should be prominent)\n\nEach highlight should:\n- Have a descriptive title\n- Include context about why it matters\n- Link to relevant PRs\n- Include performance metrics if applicable\n\n### New Features Section\n- List all new features with PR links and contributor attribution\n- Use descriptive text, not just PR titles\n- Group related features together if it makes sense\n\n### Bug Fixes Section\n- Only include actual bug fixes\n- Be specific about what was broken and how it's fixed\n- Include issue links if the PR references an issue\n\n### Performance Section\n- Separate from New Features to highlight performance work\n- Include metrics when available (e.g., \"47-67% faster\", \"33% less memory\")\n- Explain the impact on users\n\n### Testing & Infrastructure Section\n- Include only important testing/CI changes\n- **Exclude** dependency bumps (e.g., dependabot PRs for actions)\n- **Exclude** minor CI tweaks unless they're significant\n- Include major Redis version updates in CI\n\n### What to Exclude\n- Dependency bumps (dependabot PRs)\n- Minor documentation typo fixes\n- Internal refactoring that doesn't affect users\n- Duplicate entries (same PR in multiple sections)\n- `dependabot[bot]` from contributors list\n\n### Formatting Rules\n1. **PR Links**: Use `([#XXXX](https://github.com/redis/go-redis/pull/XXXX))` format\n2. **Contributor Links**: Use `[@username](https://github.com/username)` format\n3. **Issue Links**: Use `([#XXXX](https://github.com/redis/go-redis/issues/XXXX))` format\n4. **Full Changelog**: Always include at the bottom with correct version comparison\n\n### Getting PR Information\nUse GitHub API to fetch PR details:\n```bash\n# Get recent merged PRs\ngh pr list --state merged --limit 50 --json number,title,author,mergedAt,url\n```\n\nOr use the GitHub web interface to review merged PRs between releases.\n\n### Example Workflow\n1. Gather all merged PRs since last release\n2. Categorize PRs by type (feature, bug fix, performance, etc.)\n3. Identify the 3-5 most important changes for Highlights\n4. Remove duplicates and dependency bumps\n5. Add PR and contributor links\n6. Review for clarity and completeness\n7. Add Full Changelog link with correct version tags\n\n## Example (v9.17.0)\n\nSee the v9.17.0 release notes in `RELEASE-NOTES.md` for a complete example following this template.\n\n"
  },
  {
    "path": ".github/actions/run-tests/action.yml",
    "content": "name: 'Run go-redis tests'\ndescription: 'Runs go-redis tests against different Redis versions and configurations'\ninputs:\n  go-version:\n    description: 'Go version to use for running tests'\n    default: '1.24'\n  redis-version:\n    description: 'Redis version to test against'\n    required: true\nruns:\n  using: \"composite\"\n  steps:\n    - name: Set up ${{ inputs.go-version }}\n      uses: actions/setup-go@v6\n      with:\n        go-version: ${{ inputs.go-version }}\n\n    - name: Setup Test environment\n      env:\n        REDIS_VERSION: ${{ inputs.redis-version }}\n      run: |\n        set -e\n        redis_version_np=$(echo \"$REDIS_VERSION\" | grep -oP '^\\d+.\\d+')\n\n        # Mapping of redis version to redis testing containers\n        declare -A redis_version_mapping=(\n          [\"8.6.x\"]=\"custom-21860421418-debian-amd64\"\n          [\"8.4.x\"]=\"8.4.0\"\n          [\"8.2.x\"]=\"8.2.1-pre\"\n          [\"8.0.x\"]=\"8.0.2\"\n        )\n\n        if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then\n          echo \"REDIS_VERSION=${redis_version_np}\" >> $GITHUB_ENV\n          echo \"REDIS_IMAGE=redis:${REDIS_VERSION}\" >> $GITHUB_ENV\n          echo \"CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}\" >> $GITHUB_ENV\n        else\n          echo \"Version not found in the mapping.\"\n          exit 1\n        fi\n        sleep 10 # wait for redis to start\n      shell: bash\n    - name: Set up Docker Compose environment with redis ${{ inputs.redis-version }}\n      run: |\n        make docker.start\n        sleep 5\n      shell: bash\n    - name: Run tests\n      env:\n        RCE_DOCKER: \"true\"\n        RE_CLUSTER: \"false\"\n      run: |\n        make test.ci\n      shell: bash"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: gomod\n  directory: /\n  schedule:\n    interval: weekly\n- package-ecosystem: github-actions\n  directory: /\n  schedule:\n    interval: weekly\n"
  },
  {
    "path": ".github/release-drafter-config.yml",
    "content": "name-template: '$NEXT_MINOR_VERSION'\ntag-template: 'v$NEXT_MINOR_VERSION'\nautolabeler:\n  - label: 'maintenance'\n    files:\n      - '*.md'\n      - '.github/*'\n  - label: 'bug'\n    branch:\n      - '/bug-.+'\n  - label: 'maintenance'\n    branch:\n      - '/maintenance-.+'\n  - label: 'feature'\n    branch:\n      - '/feature-.+'\ncategories:\n  - title: 'Breaking Changes'\n    labels:\n      - 'breakingchange'\n  - title: '🧪 Experimental Features'\n    labels:\n      - 'experimental'\n  - title: '🚀 New Features'\n    labels:\n      - 'feature'\n      - 'enhancement'\n  - title: '🐛 Bug Fixes'\n    labels:\n      - 'fix'\n      - 'bugfix'\n      - 'bug'\n      - 'BUG'\n  - title: '🧰 Maintenance'\n    label: 'maintenance'\nchange-template: '- $TITLE (#$NUMBER)'\nexclude-labels:\n  - 'skip-changelog'\nexclude-contributors:\n  - 'dependabot'\ntemplate: |\n  # Changes\n\n  $CHANGES\n\n  ## Contributors\n  We'd like to thank all the contributors who worked on this release!\n\n  $CONTRIBUTORS\n\n"
  },
  {
    "path": ".github/spellcheck-settings.yml",
    "content": "matrix:\n- name: Markdown\n  expect_match: false\n  apsell:\n    lang: en\n    d: en_US\n    ignore-case: true\n  dictionary:\n    wordlists:\n    - .github/wordlist.txt\n    output: wordlist.dic\n  pipeline:\n  - pyspelling.filters.markdown:\n      markdown_extensions:\n      - markdown.extensions.extra:\n  - pyspelling.filters.html:\n      comments: false\n      attributes:\n      - alt\n      ignores:\n      - ':matches(code, pre)'\n      - code\n      - pre\n      - blockquote\n      - img\n  sources:\n  - 'README.md'\n  - 'FAQ.md'\n  - 'docs/**'\n"
  },
  {
    "path": ".github/wordlist.txt",
    "content": "ACLs\nAPIs\nautoload\nautoloader\nautoloading\nanalytics\nAutoloading\nbackend\nbackends\nbehaviour\nCAS\nClickHouse\nconfig\ncustomizable\nCustomizable\ndataset\nde\nDisableIdentity\nElastiCache\nextensibility\nFPM\nGolang\nIANA\nkeyspace\nkeyspaces\nKvrocks\nlocalhost\nLua\nMSSQL\nnamespace\nNoSQL\nOpenTelemetry\nORM\nPackagist\nPhpRedis\npipelining\npluggable\nPredis\nPSR\nQuickstart\nREADME\nrebalanced\nrebalancing\nredis\nRedis\nRocksDB\nruntime\nSHA\nsharding\nSETNAME\nSpellCheck\nSSL\nstruct\nstunnel\nSynDump\nTCP\nTLS\nUnstableResp\nuri\nURI\nurl\nvariadic\nRedisStack\nRedisGears\nRedisTimeseries\nRediSearch\nRawResult\nRawVal\nentra\nEntraID\nEntra\nOAuth\nAzure\nStreamingCredentialsProvider\noauth\nentraid\nMiB\nKiB\noldstable\nbackoff\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Go\n\non:\n  push:\n    branches: [master, v9, 'v9.*']\n  pull_request:\n    branches: [master, v9, v9.7, v9.8, 'ndyakov/**', 'ofekshenawa/**', 'ce/**']\n\npermissions:\n  contents: read\n\njobs:\n\n  benchmark:\n    name: benchmark\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        redis-version:\n          - \"8.6.x\" # Redis CE 8.6\n          - \"8.4.x\" # Redis CE 8.4\n          - \"8.2.x\" # Redis CE 8.2\n          - \"8.0.x\" # Redis CE 8.0\n        go-version:\n          - \"1.24.x\"\n          - oldstable\n          - stable\n\n    steps:\n      - name: Set up ${{ matrix.go-version }}\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Setup Test environment\n        env:\n          REDIS_VERSION: ${{ matrix.redis-version }}\n          CLIENT_LIBS_TEST_IMAGE: \"redislabs/client-libs-test:${{ matrix.redis-version }}\"\n        run: |\n          set -e\n          redis_version_np=$(echo \"$REDIS_VERSION\" | grep -oP '^\\d+.\\d+')\n          \n          # Mapping of redis version to redis testing containers\n          declare -A redis_version_mapping=(\n            [\"8.6.x\"]=\"custom-21860421418-debian-amd64\"\n            [\"8.4.x\"]=\"8.4.0\"\n            [\"8.2.x\"]=\"8.2.1-pre\"\n            [\"8.0.x\"]=\"8.0.2\"\n          )\n          if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then\n            echo \"REDIS_VERSION=${redis_version_np}\" >> $GITHUB_ENV\n            echo \"REDIS_IMAGE=redis:${{ matrix.redis-version }}\" >> $GITHUB_ENV\n            echo \"CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}\" >> $GITHUB_ENV\n          else\n            echo \"Version not found in the mapping.\"\n            exit 1\n          fi\n        shell: bash\n      - name: Set up Docker Compose environment with redis ${{ matrix.redis-version }}\n        run: make docker.start\n        shell: bash\n      - name: Benchmark Tests\n        env:\n          RCE_DOCKER: \"true\"\n          RE_CLUSTER: \"false\"\n        run: make bench\n        shell: bash\n\n  test-redis-ce:\n    name: test-redis-ce\n    runs-on: ubuntu-latest\n    strategy:\n        fail-fast: false\n        matrix:\n          redis-version:\n            - \"8.6.x\" # Redis CE 8.6\n            - \"8.4.x\" # Redis CE 8.4\n            - \"8.2.x\" # Redis CE 8.2\n            - \"8.0.x\" # Redis CE 8.0\n          go-version:\n            - \"1.24.x\"\n            - oldstable\n            - stable\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Run tests\n        uses: ./.github/actions/run-tests\n        with:\n          go-version: ${{matrix.go-version}}\n          redis-version: ${{ matrix.redis-version }}\n\n      - name: Upload to Codecov\n        uses: codecov/codecov-action@v5\n        with:\n          files: coverage.txt\n          token: ${{ secrets.CODECOV_TOKEN }}\n\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [master, v9, v9.7, v9.8]\n  pull_request:\n    branches: [master, v9, v9.7, v9.8]\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://git.io/codeql-language-support\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/doctests.yaml",
    "content": "name: Documentation Tests\n\non:\n  push:\n    branches: [master, examples]\n  pull_request:\n    branches: [master, examples]\n\npermissions:\n  contents: read\n\njobs:\n  doctests:\n    name: doctests\n    runs-on: ubuntu-latest\n\n    services:\n      redis-stack:\n        image: redislabs/client-libs-test:custom-21860421418-debian-amd64\n        env:\n          TLS_ENABLED: no\n          REDIS_CLUSTER: no\n          PORT: 6379\n        ports:\n          - 6379:6379\n\n    strategy:\n      fail-fast: false\n      matrix:\n        go-version: [\"1.24\"]\n\n    steps:\n      - name: Set up ${{ matrix.go-version }}\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Test doc examples\n        working-directory: ./doctests\n        run: make test\n"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "content": "name: golangci-lint\n\non:\n  push:\n    tags:\n      - v*\n    branches:\n      - master\n      - main\n      - v9\n      - v9.8\n  pull_request:\n\npermissions:\n  contents: read\n  pull-requests: read  # for golangci/golangci-lint-action to fetch pull requests\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v9.2.0\n        with:\n          verify: true \n\n"
  },
  {
    "path": ".github/workflows/release-drafter.yml",
    "content": "name: Release Drafter\n\non:\n  push:\n    # branches to consider in the event; optional, defaults to all\n    branches:\n      - master\n\npermissions: {}\njobs:\n  update_release_draft:\n    permissions:\n      pull-requests: write  #  to add label to PR (release-drafter/release-drafter)\n      contents: write  #  to create a github release (release-drafter/release-drafter)\n\n    runs-on: ubuntu-latest\n    steps:\n      # Drafts your next Release notes as Pull Requests are merged into \"master\"\n      - uses: release-drafter/release-drafter@v7\n        with:\n          # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml\n           config-name: release-drafter-config.yml\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/spellcheck.yml",
    "content": "name: spellcheck\non:\n  pull_request:\njobs:\n  check-spelling:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: Check Spelling\n        uses: rojopolis/spellcheck-github-actions@0.60.0\n        with:\n          config_path: .github/spellcheck-settings.yml\n          task_name: Markdown\n"
  },
  {
    "path": ".github/workflows/stale-issues.yml",
    "content": "name: \"Stale Issue Management\"\non:\n  schedule:\n    # Run daily at midnight UTC\n    - cron: \"0 0 * * *\"\n  workflow_dispatch: # Allow manual triggering\n\nenv:\n  # Default stale policy timeframes\n  DAYS_BEFORE_STALE: 365\n  DAYS_BEFORE_CLOSE: 30\n\n  # Accelerated timeline for needs-information issues\n  NEEDS_INFO_DAYS_BEFORE_STALE: 30\n  NEEDS_INFO_DAYS_BEFORE_CLOSE: 7\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      # First step: Handle regular issues (excluding needs-information)\n      - name: Mark regular issues as stale\n        uses: actions/stale@v10\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n\n          # Default stale policy\n          days-before-stale: ${{ env.DAYS_BEFORE_STALE }}\n          days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}\n\n          # Explicit stale label configuration\n          stale-issue-label: \"stale\"\n          stale-pr-label: \"stale\"\n\n          stale-issue-message: |\n            This issue has been automatically marked as stale due to inactivity.\n            It will be closed in 30 days if no further activity occurs.\n            If you believe this issue is still relevant, please add a comment to keep it open.\n\n          close-issue-message: |\n            This issue has been automatically closed due to inactivity.\n            If you believe this issue is still relevant, please reopen it or create a new issue with updated information.\n\n          # Exclude needs-information issues from this step\n          exempt-issue-labels: 'no-stale,needs-information'\n\n          # Remove stale label when issue/PR becomes active again\n          remove-stale-when-updated: true\n\n          # Apply to pull requests with same timeline\n          days-before-pr-stale: ${{ env.DAYS_BEFORE_STALE }}\n          days-before-pr-close: ${{ env.DAYS_BEFORE_CLOSE }}\n\n          stale-pr-message: |\n            This pull request has been automatically marked as stale due to inactivity.\n            It will be closed in 30 days if no further activity occurs.\n\n          close-pr-message: |\n            This pull request has been automatically closed due to inactivity.\n            If you would like to continue this work, please reopen the PR or create a new one.\n\n          # Only exclude no-stale PRs (needs-information PRs follow standard timeline)\n          exempt-pr-labels: 'no-stale'\n\n      # Second step: Handle needs-information issues with accelerated timeline\n      - name: Mark needs-information issues as stale\n        uses: actions/stale@v10\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n\n          # Accelerated timeline for needs-information\n          days-before-stale: ${{ env.NEEDS_INFO_DAYS_BEFORE_STALE }}\n          days-before-close: ${{ env.NEEDS_INFO_DAYS_BEFORE_CLOSE }}\n\n          # Explicit stale label configuration\n          stale-issue-label: \"stale\"\n\n          # Only target ISSUES with needs-information label (not PRs)\n          only-issue-labels: 'needs-information'\n\n          stale-issue-message: |\n            This issue has been marked as stale because it requires additional information\n            that has not been provided for 30 days. It will be closed in 7 days if the\n            requested information is not provided.\n\n          close-issue-message: |\n            This issue has been closed because the requested information was not provided within the specified timeframe.\n            If you can provide the missing information, please reopen this issue or create a new one.\n\n          # Disable PR processing for this step\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n\n          # Remove stale label when issue becomes active again\n          remove-stale-when-updated: true\n"
  },
  {
    "path": ".github/workflows/test-e2e.yml",
    "content": "name: E2E Tests\n\non:\n  push:\n    branches: [master, v9, 'v9.*']\n  pull_request:\n    branches: [master, v9, v9.7, v9.8, 'ndyakov/**', 'ofekshenawa/**', 'ce/**']\n\npermissions:\n  contents: read\n\njobs:\n  test-e2e-mock:\n    name: E2E Tests (Mock Proxy)\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        go-version:\n          - stable\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Set up Go ${{ matrix.go-version }}\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Start Docker services for E2E tests\n        run: make docker.e2e.start\n\n      - name: Wait for services to be ready\n        run: |\n          echo \"Waiting for Redis to be ready...\"\n          timeout 30 bash -c 'until docker exec redis-standalone redis-cli ping 2>/dev/null; do sleep 1; done'\n          echo \"Waiting for cae-resp-proxy to be ready...\"\n          timeout 30 bash -c 'until curl -s http://localhost:18100/stats > /dev/null; do sleep 1; done'\n          echo \"All services are ready!\"\n\n      - name: Run E2E tests with mock proxy\n        env:\n          E2E_SCENARIO_TESTS: \"true\"\n        run: |\n          go test -v ./maintnotifications/e2e/ -timeout 30m -race\n        continue-on-error: false\n\n      - name: Stop Docker services\n        if: always()\n        run: make docker.e2e.stop\n\n      - name: Show Docker logs on failure\n        if: failure()\n        run: |\n          echo \"=== Redis logs ===\"\n          docker logs redis-standalone 2>&1 | tail -100\n          echo \"=== cae-resp-proxy logs ===\"\n          docker logs cae-resp-proxy 2>&1 | tail -100\n          echo \"=== proxy-fault-injector logs ===\"\n          docker logs proxy-fault-injector 2>&1 | tail -100\n\n"
  },
  {
    "path": ".github/workflows/test-redis-enterprise.yml",
    "content": "name: RE Tests\n\non:\n  push:\n    branches: [master, v9, v9.7, v9.8]\n  pull_request:\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    name: build\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        go-version: [1.24.x]\n        re-build: [\"7.4.2-54\"]\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Clone Redis EE docker repository\n        uses: actions/checkout@v6\n        with:\n          repository: RedisLabs/redis-ee-docker\n          path: redis-ee\n\n      - name: Set up ${{ matrix.go-version }}\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Build cluster\n        working-directory: redis-ee\n        env:\n          IMAGE: \"redislabs/redis:${{ matrix.re-build }}\"\n          RE_USERNAME: test@test.com\n          RE_PASS: 12345\n          RE_CLUSTER_NAME: re-test\n          RE_USE_OSS_CLUSTER: false\n          RE_DB_PORT: 6379\n        run: ./build.sh\n\n      - name: Test\n        env:\n          RE_CLUSTER: true\n          REDIS_VERSION: \"7.4\"\n        run: |\n          go test \\\n          -skip=\"^TestTLS\" \\\n          --ginkgo.skip-file=\"ring_test.go\" \\\n          --ginkgo.skip-file=\"sentinel_test.go\" \\\n          --ginkgo.skip-file=\"osscluster_test.go\" \\\n          --ginkgo.skip-file=\"pubsub_test.go\" \\\n          --ginkgo.skip-file=\"tls_test.go\" \\\n          --ginkgo.skip-file=\"tls_cluster_test.go\" \\\n          --ginkgo.label-filter='!NonRedisEnterprise'\n"
  },
  {
    "path": ".gitignore",
    "content": "*.rdb\ntestdata/*\n.idea/\n.DS_Store\n*.tar.gz\n*.dic\nredis8tests.sh\ncoverage.txt\n**/coverage.txt\n.vscode\ntmp/*\n*.test\nextra/redisotel-native/metrics-collector-app/\n# maintenanceNotifications upgrade documentation (temporary)\nmaintenanceNotifications/docs/\n\n# Docker-generated files (TLS certificates, cluster data, etc.)\ndockers/*/tls/\ndockers/osscluster-tls/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nrun:\n  timeout: 5m\n  tests: false\nlinters:\n  settings:\n    staticcheck:\n      checks:\n        - all\n        # Incorrect or missing package comment.\n        # https://staticcheck.dev/docs/checks/#ST1000\n        - -ST1000\n        # Omit embedded fields from selector expression.\n        # https://staticcheck.dev/docs/checks/#QF1008\n        - -QF1008\n        - -ST1003\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  enable:\n    - gofmt\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".prettierrc.yml",
    "content": "semi: false\nsingleQuote: true\nproseWrap: always\nprintWidth: 100\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Introduction\n\nWe appreciate your interest in considering contributing to go-redis.\nCommunity contributions mean a lot to us.\n\n## Contributions we need\n\nYou may already know how you'd like to contribute, whether it's a fix for a bug you\nencountered, or a new feature your team wants to use.\n\nIf you don't know where to start, consider improving\ndocumentation, bug triaging, and writing tutorials are all examples of\nhelpful contributions that mean less work for you.\n\n## Your First Contribution\n\nUnsure where to begin contributing? You can start by looking through\n[help-wanted\nissues](https://github.com/redis/go-redis/issues?q=is%3Aopen+is%3Aissue+label%3ahelp-wanted).\n\nNever contributed to open source before? Here are a couple of friendly\ntutorials:\n\n-   <http://makeapullrequest.com/>\n-   <http://www.firsttimersonly.com/>\n\n## Getting Started\n\nHere's how to get started with your code contribution:\n\n1.  Create your own fork of go-redis\n2.  Do the changes in your fork\n3.  If you need a development environment, run `make docker.start`.\n \n> Note: this clones and builds the docker containers specified in `docker-compose.yml`, to understand more about\n> the infrastructure that will be started you can check the `docker-compose.yml`. You also have the possiblity\n> to specify the redis image that will be pulled with the env variable `CLIENT_LIBS_TEST_IMAGE`.\n> By default the docker image that will be pulled and started is `redislabs/client-libs-test:8.2.1-pre`.\n> If you want to test with newer Redis version, using a newer version of `redislabs/client-libs-test` should work out of the box.\n\n4.  While developing, make sure the tests pass by running `make test` (if you have the docker containers running, `make test.ci` may be sufficient).\n> Note: `make test` will try to start all containers, run the tests with `make test.ci` and then stop all containers.\n5.  If you like the change and think the project could use it, send a\n    pull request\n\nTo see what else is part of the automation, run `invoke -l`\n\n\n## Testing\n\n### Setting up Docker\nTo run the tests, you need to have Docker installed and running. If you are using a host OS that does not support\ndocker host networks out of the box (e.g. Windows, OSX), you need to set up a docker desktop and enable docker host networks.\n\n### Running tests\nCall `make test` to run all tests.\n\nContinuous Integration uses these same wrappers to run all of these\ntests against multiple versions of redis. Feel free to test your\nchanges against all the go versions supported, as declared by the\n[build.yml](./.github/workflows/build.yml) file.\n\n### Troubleshooting\n\nIf you get any errors when running `make test`, make sure\nthat you are using supported versions of Docker and go.\n\n## How to Report a Bug\n\n### Security Vulnerabilities\n\n**NOTE**: If you find a security vulnerability, do NOT open an issue.\nEmail [Redis Open Source (<oss@redis.com>)](mailto:oss@redis.com) instead.\n\nIn order to determine whether you are dealing with a security issue, ask\nyourself these two questions:\n\n-   Can I access something that's not mine, or something I shouldn't\n    have access to?\n-   Can I disable something for other people?\n\nIf the answer to either of those two questions are *yes*, then you're\nprobably dealing with a security issue. Note that even if you answer\n*no*  to both questions, you may still be dealing with a security\nissue, so if you're unsure, just email [us](mailto:oss@redis.com).\n\n### Everything Else\n\nWhen filing an issue, make sure to answer these five questions:\n\n1.  What version of go-redis are you using?\n2.  What version of redis are you using?\n3.  What did you do?\n4.  What did you expect to see?\n5.  What did you see instead?\n\n## Suggest a feature or enhancement\n\nIf you'd like to contribute a new feature, make sure you check our\nissue list to see if someone has already proposed it. Work may already\nbe underway on the feature you want or we may have rejected a\nfeature like it already.\n\nIf you don't see anything, open a new issue that describes the feature\nyou would like and how it should work.\n\n## Code review process\n\nThe core team regularly looks at pull requests. We will provide\nfeedback as soon as possible. After receiving our feedback, please respond\nwithin two weeks. After that time, we may close your PR if it isn't\nshowing any activity.\n\n## Support\n\nMaintainers can provide limited support to contributors on discord: https://discord.gg/W4txy5AeKM\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2013 The github.com/redis/go-redis Authors.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "Makefile",
    "content": "GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \\; | sort)\nREDIS_VERSION ?= 8.6\nRE_CLUSTER ?= false\nRCE_DOCKER ?= true\nCLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:custom-21860421418-debian-amd64\n\ndocker.start:\n\texport RE_CLUSTER=$(RE_CLUSTER) && \\\n\texport RCE_DOCKER=$(RCE_DOCKER) && \\\n\texport REDIS_VERSION=$(REDIS_VERSION) && \\\n\texport CLIENT_LIBS_TEST_IMAGE=$(CLIENT_LIBS_TEST_IMAGE) && \\\n\tdocker compose --profile all up -d --quiet-pull\n\ndocker.stop:\n\tdocker compose --profile all down\n\ndocker.e2e.start:\n\t@echo \"Starting Redis and cae-resp-proxy for E2E tests...\"\n\tdocker compose --profile e2e up -d --quiet-pull\n\t@echo \"Waiting for services to be ready...\"\n\t@sleep 3\n\t@echo \"Services ready!\"\n\ndocker.e2e.stop:\n\t@echo \"Stopping E2E services...\"\n\tdocker compose --profile e2e down\n\ntest:\n\t$(MAKE) docker.start\n\t@if [ -z \"$(REDIS_VERSION)\" ]; then \\\n\t\techo \"REDIS_VERSION not set, running all tests\"; \\\n\t\t$(MAKE) test.ci; \\\n\telse \\\n\t\tMAJOR_VERSION=$$(echo \"$(REDIS_VERSION)\" | cut -d. -f1); \\\n\t\tif [ \"$$MAJOR_VERSION\" -ge 8 ]; then \\\n\t\t\techo \"REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests\"; \\\n\t\t\t$(MAKE) test.ci; \\\n\t\telse \\\n\t\t\techo \"REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests\"; \\\n\t\t\t$(MAKE) test.ci.skip-vectorsets; \\\n\t\tfi; \\\n\tfi\n\t$(MAKE) docker.stop\n\ntest.ci:\n\tset -e; for dir in $(GO_MOD_DIRS); do \\\n\t  echo \"go test in $${dir}\"; \\\n\t  (cd \"$${dir}\" && \\\n\t    export RE_CLUSTER=$(RE_CLUSTER) && \\\n\t    export RCE_DOCKER=$(RCE_DOCKER) && \\\n\t    export REDIS_VERSION=$(REDIS_VERSION) && \\\n\t    go mod tidy && \\\n\t    go vet && \\\n\t    go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \\\n\tdone\n\tcd internal/customvet && go build .\n\tgo vet -vettool ./internal/customvet/customvet\n\ntest.ci.skip-vectorsets:\n\tset -e; for dir in $(GO_MOD_DIRS); do \\\n\t  echo \"go test in $${dir} (skipping vector sets)\"; \\\n\t  (cd \"$${dir}\" && \\\n\t    export RE_CLUSTER=$(RE_CLUSTER) && \\\n\t    export RCE_DOCKER=$(RCE_DOCKER) && \\\n\t    export REDIS_VERSION=$(REDIS_VERSION) && \\\n\t    go mod tidy && \\\n\t    go vet && \\\n\t    go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \\\n\t      -run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \\\n\tdone\n\tcd internal/customvet && go build .\n\tgo vet -vettool ./internal/customvet/customvet\n\nbench:\n\texport RE_CLUSTER=$(RE_CLUSTER) && \\\n\texport RCE_DOCKER=$(RCE_DOCKER) && \\\n\texport REDIS_VERSION=$(REDIS_VERSION) && \\\n\tgo test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example\n\ntest.e2e:\n\t@echo \"Running E2E tests with auto-start proxy...\"\n\t$(MAKE) docker.e2e.start\n\t@echo \"Running tests...\"\n\t@E2E_SCENARIO_TESTS=true go test -v ./maintnotifications/e2e/ -timeout 30m || ($(MAKE) docker.e2e.stop && exit 1)\n\t$(MAKE) docker.e2e.stop\n\t@echo \"E2E tests completed!\"\n\ntest.e2e.docker:\n\t@echo \"Running Docker-compatible E2E tests...\"\n\t$(MAKE) docker.e2e.start\n\t@echo \"Running unified injector tests...\"\n\t@E2E_SCENARIO_TESTS=true go test -v -run \"TestUnifiedInjector|TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation\" ./maintnotifications/e2e/ -timeout 10m || ($(MAKE) docker.e2e.stop && exit 1)\n\t$(MAKE) docker.e2e.stop\n\t@echo \"Docker E2E tests completed!\"\n\ntest.e2e.logic:\n\t@echo \"Running E2E logic tests (no proxy required)...\"\n\t@E2E_SCENARIO_TESTS=true \\\n\t\tREDIS_ENDPOINTS_CONFIG_PATH=/tmp/test_endpoints_verify.json \\\n\t\tFAULT_INJECTION_API_URL=http://localhost:8080 \\\n\t\tgo test -v -run \"TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation\" ./maintnotifications/e2e/\n\t@echo \"Logic tests completed!\"\n\n.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt test.e2e test.e2e.logic docker.e2e.start docker.e2e.stop\n\nbuild:\n\texport RE_CLUSTER=$(RE_CLUSTER) && \\\n\texport RCE_DOCKER=$(RCE_DOCKER) && \\\n\texport REDIS_VERSION=$(REDIS_VERSION) && \\\n\tgo build .\n\nfmt:\n\tgofumpt -w ./\n\tgoimports -w  -local github.com/redis/go-redis ./\n\ngo_mod_tidy:\n\tset -e; for dir in $(GO_MOD_DIRS); do \\\n\t  echo \"go mod tidy in $${dir}\"; \\\n\t  (cd \"$${dir}\" && \\\n\t    go get -u ./... && \\\n\t    go mod tidy); \\\n\tdone\n"
  },
  {
    "path": "README.md",
    "content": "# Redis client for Go\n\n[![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions)\n[![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)\n[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.io/docs/latest/develop/clients/go/)\n[![Go Report Card](https://goreportcard.com/badge/github.com/redis/go-redis/v9)](https://goreportcard.com/report/github.com/redis/go-redis/v9)\n[![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis)\n\n[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/W4txy5AeKM)\n[![Twitch](https://img.shields.io/twitch/status/redisinc?style=social)](https://www.twitch.tv/redisinc)\n[![YouTube](https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social)](https://www.youtube.com/redisinc)\n[![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc)\n[![Stack Exchange questions](https://img.shields.io/stackexchange/stackoverflow/t/go-redis?style=social&logo=stackoverflow&label=Stackoverflow)](https://stackoverflow.com/questions/tagged/go-redis)\n\n> go-redis is the official Redis client library for the Go programming language. It offers a straightforward interface for interacting with Redis servers. \n\n## Supported versions\n\nIn `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support:\n- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0\n- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2 \n- [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4\n\nAlthough the `go.mod` states it requires at minimum `go 1.24`, our CI is configured to run the tests against all three\nversions of Redis and multiple versions of Go ([1.24](https://go.dev/doc/devel/release#go1.24.0), oldstable, and stable). We observe that some modules related test may not pass with\nRedis Stack 7.2 and some commands are changed with Redis CE 8.0.\nAlthough it is not officially supported, `go-redis/v9`  should be able to work with any Redis 7.0+.\nPlease do refer to the documentation and the tests if you experience any issues.\n\n## How do I Redis?\n\n[Learn for free at Redis University](https://university.redis.com/)\n\n[Build faster with the Redis Launchpad](https://launchpad.redis.com/)\n\n[Try the Redis Cloud](https://redis.com/try-free/)\n\n[Dive in developer tutorials](https://developer.redis.com/)\n\n[Join the Redis community](https://redis.com/community/)\n\n[Work at Redis](https://redis.com/company/careers/jobs/)\n\n\n## Resources\n\n- [Discussions](https://github.com/redis/go-redis/discussions)\n- [Chat](https://discord.gg/W4txy5AeKM)\n- [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9)\n- [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples)\n\n## old documentation\n\n- [English](https://redis.uptrace.dev)\n- [简体中文](https://redis.uptrace.dev/zh/)\n\n## Ecosystem\n\n- [Entra ID (Azure AD)](https://github.com/redis/go-redis-entraid)\n- [Distributed Locks](https://github.com/bsm/redislock)\n- [Redis Cache](https://github.com/go-redis/cache)\n- [Rate limiting](https://github.com/go-redis/redis_rate)\n\n## Features\n\n- Redis commands except QUIT and SYNC.\n- Automatic connection pooling.\n- [StreamingCredentialsProvider (e.g. entra id, oauth)](#1-streaming-credentials-provider-highest-priority) (experimental)\n- [Pub/Sub](https://redis.uptrace.dev/guide/go-redis-pubsub.html).\n- [Pipelines and transactions](https://redis.uptrace.dev/guide/go-redis-pipelines.html).\n- [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html).\n- [Redis Sentinel](https://redis.uptrace.dev/guide/go-redis-sentinel.html).\n- [Redis Cluster](https://redis.uptrace.dev/guide/go-redis-cluster.html).\n- [Redis Performance Monitoring](https://redis.uptrace.dev/guide/redis-performance-monitoring.html).\n- [Redis Probabilistic [RedisStack]](https://redis.io/docs/data-types/probabilistic/)\n- [Customizable read and write buffers size.](#custom-buffer-sizes)\n\n## Installation\n\ngo-redis supports 2 last Go versions and requires a Go version with\n[modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go\nmodule:\n\n```shell\ngo mod init github.com/my/repo\n```\n\nThen install go-redis/**v9**:\n\n```shell\ngo get github.com/redis/go-redis/v9\n```\n\n## Quickstart\n\n```go\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/redis/go-redis/v9\"\n)\n\nvar ctx = context.Background()\n\nfunc ExampleClient() {\n    rdb := redis.NewClient(&redis.Options{\n        Addr:     \"localhost:6379\",\n        Password: \"\", // no password set\n        DB:       0,  // use default DB\n    })\n    defer rdb.Close()\n\n    err := rdb.Set(ctx, \"key\", \"value\", 0).Err()\n    if err != nil {\n        panic(err)\n    }\n\n    val, err := rdb.Get(ctx, \"key\").Result()\n    if err != nil {\n        panic(err)\n    }\n    fmt.Println(\"key\", val)\n\n    val2, err := rdb.Get(ctx, \"key2\").Result()\n    if err == redis.Nil {\n        fmt.Println(\"key2 does not exist\")\n    } else if err != nil {\n        panic(err)\n    } else {\n        fmt.Println(\"key2\", val2)\n    }\n    // Output: key value\n    // key2 does not exist\n}\n```\n\n### Dial retries and backoff\n\nConnection establishment can be retried by the connection pool when dialing fails.\n\n- **`DialerRetries`**: maximum number of dial attempts (default: 5).\n- **`DialerRetryTimeout`**: default delay between attempts when no custom backoff is provided (default: 100ms).\n- **`DialerRetryBackoff`**: optional function hook to control the delay between attempts.\n\nExample:\n\n```go\nrdb := redis.NewClient(&redis.Options{\n\tAddr: \"localhost:6379\",\n\n\tDialerRetries:      5,\n\tDialerRetryTimeout: 100 * time.Millisecond, // used when DialerRetryBackoff is nil\n\n\t// Optional: exponential backoff with jitter and a cap.\n\tDialerRetryBackoff: redis.DialRetryBackoffExponential(100*time.Millisecond, 2*time.Second),\n})\ndefer rdb.Close()\n```\n\n### Authentication\n\nThe Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options:\n\n#### 1. Streaming Credentials Provider (Highest Priority) - Experimental feature\n\nThe streaming credentials provider allows for dynamic credential updates during the connection lifetime. This is particularly useful for managed identity services and token-based authentication.\n\n```go\ntype StreamingCredentialsProvider interface {\n    Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error)\n}\n\ntype CredentialsListener interface {\n    OnNext(credentials Credentials)  // Called when credentials are updated\n    OnError(err error)              // Called when an error occurs\n}\n\ntype Credentials interface {\n    BasicAuth() (username string, password string)\n    RawCredentials() string\n}\n```\n\nExample usage:\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6379\",\n    StreamingCredentialsProvider: &MyCredentialsProvider{},\n})\n```\n\n**Note:** The streaming credentials provider can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) to enable Entra ID (formerly Azure AD) authentication. This allows for seamless integration with Azure's managed identity services and token-based authentication.\n\nExample with Entra ID:\n```go\nimport (\n    \"github.com/redis/go-redis/v9\"\n    \"github.com/redis/go-redis-entraid\"\n)\n\n// Create an Entra ID credentials provider\nprovider := entraid.NewDefaultAzureIdentityProvider()\n\n// Configure Redis client with Entra ID authentication\nrdb := redis.NewClient(&redis.Options{\n    Addr: \"your-redis-server.redis.cache.windows.net:6380\",\n    StreamingCredentialsProvider: provider,\n    TLSConfig: &tls.Config{\n        MinVersion: tls.VersionTLS12,\n    },\n})\n```\n\n#### 2. Context-based Credentials Provider\n\nThe context-based provider allows credentials to be determined at the time of each operation, using the context.\n\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6379\",\n    CredentialsProviderContext: func(ctx context.Context) (string, string, error) {\n        // Return username, password, and any error\n        return \"user\", \"pass\", nil\n    },\n})\n```\n\n#### 3. Regular Credentials Provider\n\nA simple function-based provider that returns static credentials.\n\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6379\",\n    CredentialsProvider: func() (string, string) {\n        // Return username and password\n        return \"user\", \"pass\"\n    },\n})\n```\n\n#### 4. Username/Password Fields (Lowest Priority)\n\nThe most basic way to provide credentials is through the `Username` and `Password` fields in the options.\n\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr:     \"localhost:6379\",\n    Username: \"user\",\n    Password: \"pass\",\n})\n```\n\n#### Priority Order\n\nThe client will use credentials in the following priority order:\n1. Streaming Credentials Provider (if set)\n2. Context-based Credentials Provider (if set)\n3. Regular Credentials Provider (if set)\n4. Username/Password fields (if set)\n\nIf none of these are set, the client will attempt to connect without authentication.\n\n### Protocol Version\n\nThe client supports both RESP2 and RESP3 protocols. You can specify the protocol version in the options:\n\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr:     \"localhost:6379\",\n    Password: \"\", // no password set\n    DB:       0,  // use default DB\n    Protocol: 3,  // specify 2 for RESP 2 or 3 for RESP 3\n})\n```\n\n### Connecting via a redis url\n\ngo-redis also supports connecting via the\n[redis uri specification](https://github.com/redis/redis-specifications/tree/master/uri/redis.txt).\nThe example below demonstrates how the connection can easily be configured using a string, adhering\nto this specification.\n\n```go\nimport (\n    \"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient() *redis.Client {\n    url := \"redis://user:password@localhost:6379/0?protocol=3\"\n    opts, err := redis.ParseURL(url)\n    if err != nil {\n        panic(err)\n    }\n\n    return redis.NewClient(opts)\n}\n\n```\n\n### Instrument with OpenTelemetry\n\n```go\nimport (\n    \"github.com/redis/go-redis/v9\"\n    \"github.com/redis/go-redis/extra/redisotel/v9\"\n    \"errors\"\n)\n\nfunc main() {\n    ...\n    rdb := redis.NewClient(&redis.Options{...})\n\n    if err := errors.Join(redisotel.InstrumentTracing(rdb), redisotel.InstrumentMetrics(rdb)); err != nil {\n        log.Fatal(err)\n    }\n```\n\n\n### Buffer Size Configuration\n\ngo-redis uses 32KiB read and write buffers by default for optimal performance. For high-throughput applications or large pipelines, you can customize buffer sizes:\n\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr:            \"localhost:6379\",\n    ReadBufferSize:  1024 * 1024, // 1MiB read buffer\n    WriteBufferSize: 1024 * 1024, // 1MiB write buffer\n})\n```\n\n### Advanced Configuration\n\ngo-redis supports extending the client identification phase to allow projects to send their own custom client identification.\n\n#### Default Client Identification\n\nBy default, go-redis automatically sends the client library name and version during the connection process. This feature is available in redis-server as of version 7.2. As a result, the command is \"fire and forget\", meaning it should fail silently, in the case that the redis server does not support this feature.\n\n#### Disabling Identity Verification\n\nWhen connection identity verification is not required or needs to be explicitly disabled, a `DisableIdentity` configuration option exists.\nInitially there was a typo and the option was named `DisableIndentity` instead of `DisableIdentity`. The misspelled option is marked as Deprecated and will be removed in V10 of this library.\nAlthough both options will work at the moment, the correct option is `DisableIdentity`. The deprecated option will be removed in V10 of this library, so please use the correct option name to avoid any issues.\n\nTo disable verification, set the `DisableIdentity` option to `true` in the Redis client options:\n\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr:            \"localhost:6379\",\n    Password:        \"\",\n    DB:              0,\n    DisableIdentity: true, // Disable set-info on connect\n})\n```\n\n#### Unstable RESP3 Structures for RediSearch Commands\nWhen integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes.\n\nTo enable unstable RESP3, set the option in your client configuration:\n\n```go\nredis.NewClient(&redis.Options{\n\t\t\tUnstableResp3: true,\n\t\t})\n```\n**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data.\n          Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them:\n\n```go\nres1, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo bar\", &redis.FTSearchOptions{}).RawResult()\nval1 := client.FTSearchWithArgs(ctx, \"txt\", \"foo bar\", &redis.FTSearchOptions{}).RawVal()\n```\n\n#### Redis-Search Default Dialect\n\nIn the Redis-Search module, **the default dialect is 2**. If needed, you can explicitly specify a different dialect using the appropriate configuration in your queries.\n\n**Important**: Be aware that the query dialect may impact the results returned. If needed, you can revert to a different dialect version by passing the desired dialect in the arguments of the command you want to execute.\nFor example:\n```\n\tres2, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\",\n\t\t\"@pickup_zone:[CONTAINS $bike]\",\n\t\t&redis.FTSearchOptions{\n\t\t\tParams: map[string]interface{}{\n\t\t\t\t\"bike\": \"POINT(-0.1278 51.5074)\",\n\t\t\t},\n\t\t\tDialectVersion: 3,\n\t\t},\n\t).Result()\n```\nYou can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/).\n\n#### Custom buffer sizes\nPrior to v9.12, the buffer size was the default go value of 4096 bytes. Starting from v9.12, \ngo-redis uses 32KiB read and write buffers by default for optimal performance.\nFor high-throughput applications or large pipelines, you can customize buffer sizes:\n\n```go\nrdb := redis.NewClient(&redis.Options{\n    Addr:            \"localhost:6379\",\n    ReadBufferSize:  1024 * 1024, // 1MiB read buffer\n    WriteBufferSize: 1024 * 1024, // 1MiB write buffer\n})\n```\n\n**Important**: If you experience any issues with the default buffer sizes, please try setting them to the go default of 4096 bytes.\n\n## Contributing\nWe welcome contributions to the go-redis library! If you have a bug fix, feature request, or improvement, please open an issue or pull request on GitHub.\nWe appreciate your help in making go-redis better for everyone.\nIf you are interested in contributing to the go-redis library, please check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to get started.\n\n## Look and feel\n\nSome corner cases:\n\n```go\n// SET key value EX 10 NX\nset, err := rdb.SetNX(ctx, \"key\", \"value\", 10*time.Second).Result()\n\n// SET key value keepttl NX\nset, err := rdb.SetNX(ctx, \"key\", \"value\", redis.KeepTTL).Result()\n\n// SORT list LIMIT 0 2 ASC\nvals, err := rdb.Sort(ctx, \"list\", &redis.Sort{Offset: 0, Count: 2, Order: \"ASC\"}).Result()\n\n// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2\nvals, err := rdb.ZRangeByScoreWithScores(ctx, \"zset\", &redis.ZRangeBy{\n    Min: \"-inf\",\n    Max: \"+inf\",\n    Offset: 0,\n    Count: 2,\n}).Result()\n\n// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM\nvals, err := rdb.ZInterStore(ctx, \"out\", &redis.ZStore{\n    Keys: []string{\"zset1\", \"zset2\"},\n    Weights: []int64{2, 3}\n}).Result()\n\n// EVAL \"return {KEYS[1],ARGV[1]}\" 1 \"key\" \"hello\"\nvals, err := rdb.Eval(ctx, \"return {KEYS[1],ARGV[1]}\", []string{\"key\"}, \"hello\").Result()\n\n// custom command\nres, err := rdb.Do(ctx, \"set\", \"key\", \"value\").Result()\n```\n\n## Typed Errors\n\ngo-redis provides typed error checking functions for common Redis errors:\n\n```go\n// Cluster and replication errors\nredis.IsLoadingError(err)        // Redis is loading the dataset\nredis.IsReadOnlyError(err)       // Write to read-only replica\nredis.IsClusterDownError(err)    // Cluster is down\nredis.IsTryAgainError(err)       // Command should be retried\nredis.IsMasterDownError(err)     // Master is down\nredis.IsMovedError(err)          // Returns (address, true) if key moved\nredis.IsAskError(err)            // Returns (address, true) if key being migrated\n\n// Connection and resource errors\nredis.IsMaxClientsError(err)     // Maximum clients reached\nredis.IsAuthError(err)           // Authentication failed (NOAUTH, WRONGPASS, unauthenticated)\nredis.IsPermissionError(err)     // Permission denied (NOPERM)\nredis.IsOOMError(err)            // Out of memory (OOM)\n\n// Transaction errors\nredis.IsExecAbortError(err)      // Transaction aborted (EXECABORT)\n```\n\n### Error Wrapping in Hooks\n\nWhen wrapping errors in hooks, use custom error types with `Unwrap()` method (preferred) or `fmt.Errorf` with `%w`. Always call `cmd.SetErr()` to preserve error type information:\n\n```go\n// Custom error type (preferred)\ntype AppError struct {\n    Code      string\n    RequestID string\n    Err       error\n}\n\nfunc (e *AppError) Error() string {\n    return fmt.Sprintf(\"[%s] request_id=%s: %v\", e.Code, e.RequestID, e.Err)\n}\n\nfunc (e *AppError) Unwrap() error {\n    return e.Err\n}\n\n// Hook implementation\nfunc (h MyHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {\n    return func(ctx context.Context, cmd redis.Cmder) error {\n        err := next(ctx, cmd)\n        if err != nil {\n            // Wrap with custom error type\n            wrappedErr := &AppError{\n                Code:      \"REDIS_ERROR\",\n                RequestID: getRequestID(ctx),\n                Err:       err,\n            }\n            cmd.SetErr(wrappedErr)\n            return wrappedErr  // Return wrapped error to preserve it\n        }\n        return nil\n    }\n}\n\n// Typed error detection works through wrappers\nif redis.IsLoadingError(err) {\n    // Retry logic\n}\n\n// Extract custom error if needed\nvar appErr *AppError\nif errors.As(err, &appErr) {\n    log.Printf(\"Request: %s\", appErr.RequestID)\n}\n```\n\nAlternatively, use `fmt.Errorf` with `%w`:\n```go\nwrappedErr := fmt.Errorf(\"context: %w\", err)\ncmd.SetErr(wrappedErr)\n```\n\n### Pipeline Hook Example\n\nFor pipeline operations, use `ProcessPipelineHook`:\n\n```go\ntype PipelineLoggingHook struct{}\n\nfunc (h PipelineLoggingHook) DialHook(next redis.DialHook) redis.DialHook {\n    return next\n}\n\nfunc (h PipelineLoggingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {\n    return next\n}\n\nfunc (h PipelineLoggingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n    return func(ctx context.Context, cmds []redis.Cmder) error {\n        start := time.Now()\n\n        // Execute the pipeline\n        err := next(ctx, cmds)\n\n        duration := time.Since(start)\n        log.Printf(\"Pipeline executed %d commands in %v\", len(cmds), duration)\n\n        // Process individual command errors\n        // Note: Individual command errors are already set on each cmd by the pipeline execution\n        for _, cmd := range cmds {\n            if cmdErr := cmd.Err(); cmdErr != nil {\n                // Check for specific error types using typed error functions\n                if redis.IsAuthError(cmdErr) {\n                    log.Printf(\"Auth error in pipeline command %s: %v\", cmd.Name(), cmdErr)\n                } else if redis.IsPermissionError(cmdErr) {\n                    log.Printf(\"Permission error in pipeline command %s: %v\", cmd.Name(), cmdErr)\n                }\n\n                // Optionally wrap individual command errors to add context\n                // The wrapped error preserves type information through errors.As()\n                wrappedErr := fmt.Errorf(\"pipeline cmd %s failed: %w\", cmd.Name(), cmdErr)\n                cmd.SetErr(wrappedErr)\n            }\n        }\n\n        // Return the pipeline-level error (connection errors, etc.)\n        // You can wrap it if needed, or return it as-is\n        return err\n    }\n}\n\n// Register the hook\nrdb.AddHook(PipelineLoggingHook{})\n\n// Use pipeline - errors are still properly typed\npipe := rdb.Pipeline()\npipe.Set(ctx, \"key1\", \"value1\", 0)\npipe.Get(ctx, \"key2\")\n_, err := pipe.Exec(ctx)\n```\n\n## Run the test\n\nRecommended to use Docker, just need to run:\n```shell\nmake test\n```\n\n## See also\n\n- [Golang ORM](https://bun.uptrace.dev) for PostgreSQL, MySQL, MSSQL, and SQLite\n- [Golang PostgreSQL](https://bun.uptrace.dev/postgres/)\n- [Golang HTTP router](https://bunrouter.uptrace.dev/)\n- [Golang ClickHouse ORM](https://github.com/uptrace/go-clickhouse)\n\n## Contributors\n\n> The go-redis project was originally initiated by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).\n> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can\n> use it to monitor applications and set up automatic alerts to receive notifications via email,\n> Slack, Telegram, and others.\n>\n> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which\n> demonstrates how you can use Uptrace to monitor go-redis.\n\nThanks to all the people who already contributed!\n\n<a href=\"https://github.com/redis/go-redis/graphs/contributors\">\n  <img src=\"https://contributors-img.web.app/image?repo=redis/go-redis\" />\n</a>\n"
  },
  {
    "path": "RELEASE-NOTES.md",
    "content": "# Release Notes\n\n# 9.18.0 (2026-02-16)\n\n## 🚀 Highlights\n\n### Redis 8.6 Support\n\nAdded support for Redis 8.6, including new commands and features for streams idempotent production and HOTKEYS.\n\n### Smart Client Handoff (Maintenance Notifications) for Cluster\n\nThis release introduces comprehensive support for Redis Cluster maintenance notifications via SMIGRATING/SMIGRATED push notifications. The client now automatically handles slot migrations by:\n- **Relaxing timeouts during migration** (SMIGRATING) to prevent false failures\n- **Triggering lazy cluster state reloads** upon completion (SMIGRATED)\n- Enabling seamless operations during Redis Enterprise maintenance windows\n\n([#3643](https://github.com/redis/go-redis/pull/3643)) by [@ndyakov](https://github.com/ndyakov)\n\n### OpenTelemetry Native Metrics Support\n\nAdded comprehensive OpenTelemetry metrics support following the [OpenTelemetry Database Client Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/database/database-metrics/). The implementation uses a Bridge Pattern to keep the core library dependency-free while providing optional metrics instrumentation through the new `extra/redisotel-native` package.\n\n**Metric groups include:**\n- Command metrics: Operation duration with retry tracking\n- Connection basic: Connection count and creation time\n- Resiliency: Errors, handoffs, timeout relaxation\n- Connection advanced: Wait time and use time\n- Pubsub metrics: Published and received messages\n- Stream metrics: Processing duration and maintenance notifications\n\n([#3637](https://github.com/redis/go-redis/pull/3637)) by [@ofekshenawa](https://github.com/ofekshenawa)\n\n## ✨ New Features\n\n- **HOTKEYS Commands**: Added support for Redis HOTKEYS feature for identifying hot keys based on CPU consumption and network utilization ([#3695](https://github.com/redis/go-redis/pull/3695)) by [@ofekshenawa](https://github.com/ofekshenawa)\n- **Streams Idempotent Production**: Added support for Redis 8.6+ Streams Idempotent Production with `ProducerID`, `IdempotentID`, `IdempotentAuto` in `XAddArgs` and new `XCFGSET` command ([#3693](https://github.com/redis/go-redis/pull/3693)) by [@ofekshenawa](https://github.com/ofekshenawa)\n- **NaN Values for TimeSeries**: Added support for NaN (Not a Number) values in Redis time series commands ([#3687](https://github.com/redis/go-redis/pull/3687)) by [@ofekshenawa](https://github.com/ofekshenawa)\n- **DialerRetries Options**: Added `DialerRetries` and `DialerRetryTimeout` to `ClusterOptions`, `RingOptions`, and `FailoverOptions` ([#3686](https://github.com/redis/go-redis/pull/3686)) by [@naveenchander30](https://github.com/naveenchander30)\n- **ConnMaxLifetimeJitter**: Added jitter configuration to distribute connection expiration times and prevent thundering herd ([#3666](https://github.com/redis/go-redis/pull/3666)) by [@cyningsun](https://github.com/cyningsun)\n- **Digest Helper Functions**: Added `DigestString` and `DigestBytes` helper functions for client-side xxh3 hashing compatible with Redis DIGEST command ([#3679](https://github.com/redis/go-redis/pull/3679)) by [@ofekshenawa](https://github.com/ofekshenawa)\n- **SMIGRATED New Format**: Updated SMIGRATED parser to support new format and remember original host:port ([#3697](https://github.com/redis/go-redis/pull/3697)) by [@ndyakov](https://github.com/ndyakov)\n- **Cluster State Reload Interval**: Added cluster state reload interval option for maintenance notifications ([#3663](https://github.com/redis/go-redis/pull/3663)) by [@ndyakov](https://github.com/ndyakov)\n\n## 🐛 Bug Fixes\n\n- **PubSub nil pointer dereference**: Fixed nil pointer dereference in PubSub after `WithTimeout()` - `pubSubPool` is now properly cloned ([#3710](https://github.com/redis/go-redis/pull/3710)) by [@Copilot](https://github.com/apps/copilot-swe-agent)\n- **MaintNotificationsConfig nil check**: Guard against nil `MaintNotificationsConfig` in `initConn` ([#3707](https://github.com/redis/go-redis/pull/3707)) by [@veeceey](https://github.com/veeceey)\n- **wantConnQueue zombie elements**: Fixed zombie `wantConn` elements accumulation in `wantConnQueue` ([#3680](https://github.com/redis/go-redis/pull/3680)) by [@cyningsun](https://github.com/cyningsun)\n- **XADD/XTRIM approx flag**: Fixed XADD and XTRIM to use `=` when approx is false ([#3684](https://github.com/redis/go-redis/pull/3684)) by [@ndyakov](https://github.com/ndyakov)\n- **Sentinel timeout retry**: When connection to a sentinel times out, attempt to connect to other sentinels ([#3654](https://github.com/redis/go-redis/pull/3654)) by [@cxljs](https://github.com/cxljs)\n\n## ⚡ Performance\n\n- **Fuzz test optimization**: Eliminated repeated string conversions, used functional approach for cleaner operation selection ([#3692](https://github.com/redis/go-redis/pull/3692)) by [@feiguoL](https://github.com/feiguoL)\n- **Pre-allocate capacity**: Pre-allocate slice capacity to prevent multiple capacity expansions ([#3689](https://github.com/redis/go-redis/pull/3689)) by [@feelshu](https://github.com/feelshu)\n\n## 🧪 Testing\n\n- **Comprehensive TLS tests**: Added comprehensive TLS tests and example for standalone, cluster, and certificate authentication ([#3681](https://github.com/redis/go-redis/pull/3681)) by [@ndyakov](https://github.com/ndyakov)\n- **Redis 8.6**: Updated CI to use Redis 8.6-pre ([#3685](https://github.com/redis/go-redis/pull/3685)) by [@ndyakov](https://github.com/ndyakov)\n\n## 🧰 Maintenance\n\n- **Deprecation warnings**: Added deprecation warnings for commands based on Redis documentation ([#3673](https://github.com/redis/go-redis/pull/3673)) by [@ndyakov](https://github.com/ndyakov)\n- **Use errors.Join()**: Replaced custom error join function with standard library `errors.Join()` ([#3653](https://github.com/redis/go-redis/pull/3653)) by [@cxljs](https://github.com/cxljs)\n- **Use Go 1.21 min/max**: Use Go 1.21's built-in min/max functions ([#3656](https://github.com/redis/go-redis/pull/3656)) by [@cxljs](https://github.com/cxljs)\n- **Proper formatting**: Code formatting improvements ([#3670](https://github.com/redis/go-redis/pull/3670)) by [@12ya](https://github.com/12ya)\n- **Set commands documentation**: Added comprehensive documentation to all set command methods ([#3642](https://github.com/redis/go-redis/pull/3642)) by [@iamamirsalehi](https://github.com/iamamirsalehi)\n- **MaxActiveConns docs**: Added default value documentation for `MaxActiveConns` ([#3674](https://github.com/redis/go-redis/pull/3674)) by [@codykaup](https://github.com/codykaup)\n- **README example update**: Updated README example ([#3657](https://github.com/redis/go-redis/pull/3657)) by [@cxljs](https://github.com/cxljs)\n- **Cluster maintnotif example**: Added example application for cluster maintenance notifications ([#3651](https://github.com/redis/go-redis/pull/3651)) by [@ndyakov](https://github.com/ndyakov)\n\n## 👥 Contributors\n\nWe'd like to thank all the contributors who worked on this release!\n\n[@12ya](https://github.com/12ya), [@Copilot](https://github.com/apps/copilot-swe-agent), [@codykaup](https://github.com/codykaup), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@feelshu](https://github.com/feelshu), [@feiguoL](https://github.com/feiguoL), [@iamamirsalehi](https://github.com/iamamirsalehi), [@naveenchander30](https://github.com/naveenchander30), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@veeceey](https://github.com/veeceey)\n\n---\n\n**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.18.0\n\n# 9.18.0-beta.2 (2025-12-09)\n\n## 🚀 Highlights\n\n### Go Version Update\n\nThis release updates the minimum required Go version to 1.21. This is part of a gradual migration strategy where the minimum supported Go version will be three versions behind the latest release. With each new Go version release, we will bump the minimum version by one, ensuring compatibility while staying current with the Go ecosystem.\n\n### Stability Improvements\n\nThis release includes several important stability fixes:\n- Fixed a critical panic in the handoff worker manager that could occur when handling nil errors\n- Improved test reliability for Smart Client Handoff functionality\n- Fixed logging format issues that could cause runtime errors\n\n## ✨ New Features\n\n- OpenTelemetry metrics improvements for nil response handling ([#3638](https://github.com/redis/go-redis/pull/3638)) by [@fengve](https://github.com/fengve)\n\n## 🐛 Bug Fixes\n\n- Fixed panic on nil error in handoffWorkerManager closeConnFromRequest ([#3633](https://github.com/redis/go-redis/pull/3633)) by [@ccoVeille](https://github.com/ccoVeille)\n- Fixed bad sprintf syntax in logging ([#3632](https://github.com/redis/go-redis/pull/3632)) by [@ccoVeille](https://github.com/ccoVeille)\n\n## 🧰 Maintenance\n\n- Updated minimum Go version to 1.21 ([#3640](https://github.com/redis/go-redis/pull/3640)) by [@ndyakov](https://github.com/ndyakov)\n- Use Go 1.20 idiomatic string<->byte conversion ([#3435](https://github.com/redis/go-redis/pull/3435)) by [@justinhwang](https://github.com/justinhwang)\n- Reduce flakiness of Smart Client Handoff test ([#3641](https://github.com/redis/go-redis/pull/3641)) by [@kiryazovi-redis](https://github.com/kiryazovi-redis)\n- Revert PR #3634 (Observability metrics phase1) ([#3635](https://github.com/redis/go-redis/pull/3635)) by [@ofekshenawa](https://github.com/ofekshenawa)\n\n## 👥 Contributors\n\nWe'd like to thank all the contributors who worked on this release!\n\n[@justinhwang](https://github.com/justinhwang), [@ndyakov](https://github.com/ndyakov), [@kiryazovi-redis](https://github.com/kiryazovi-redis), [@fengve](https://github.com/fengve), [@ccoVeille](https://github.com/ccoVeille), [@ofekshenawa](https://github.com/ofekshenawa)\n\n---\n\n**Full Changelog**: https://github.com/redis/go-redis/compare/v9.18.0-beta.1...v9.18.0-beta.2\n\n# 9.18.0-beta.1 (2025-12-01)\n\n## 🚀 Highlights\n\n### Request and Response Policy Based Routing in Cluster Mode\n\nThis beta release introduces comprehensive support for Redis COMMAND-based request and response policy routing for cluster clients. This feature enables intelligent command routing and response aggregation based on Redis command metadata.\n\n**Key Features:**\n- **Command Policy Loader**: Automatically parses and caches COMMAND metadata with routing/aggregation hints\n- **Enhanced Routing Engine**: Supports all request policies including:\n  - `default(keyless)` - Commands without keys\n  - `default(hashslot)` - Commands with hash slot routing\n  - `all_shards` - Commands that need to run on all shards\n  - `all_nodes` - Commands that need to run on all nodes\n  - `multi_shard` - Commands that span multiple shards\n  - `special` - Commands with custom routing logic\n- **Response Aggregator**: Intelligently combines multi-shard replies based on response policies:\n  - `all_succeeded` - All shards must succeed\n  - `one_succeeded` - At least one shard must succeed\n  - `agg_sum` - Aggregate numeric responses\n  - `special` - Custom aggregation logic (e.g., FT.CURSOR)\n- **Raw Command Support**: Policies are enforced on `Client.Do(ctx, args...)`\n\nThis feature is particularly useful for Redis Stack commands like RediSearch that need to operate across multiple shards in a cluster.\n\n### Connection Pool Improvements\n\nFixed a critical defect in the connection pool's turn management mechanism that could lead to connection leaks under certain conditions. The fix ensures proper 1:1 correspondence between turns and connections.\n\n## ✨ New Features\n\n- Request and Response Policy Based Routing in Cluster Mode ([#3422](https://github.com/redis/go-redis/pull/3422)) by [@ofekshenawa](https://github.com/ofekshenawa)\n\n## 🐛 Bug Fixes\n\n- Fixed connection pool turn management to prevent connection leaks ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun)\n\n## 🧰 Maintenance\n\n- chore(deps): bump rojopolis/spellcheck-github-actions from 0.54.0 to 0.55.0 ([#3627](https://github.com/redis/go-redis/pull/3627))\n\n## 👥 Contributors\n\nWe'd like to thank all the contributors who worked on this release!\n\n[@cyningsun](https://github.com/cyningsun), [@ofekshenawa](https://github.com/ofekshenawa), [@ndyakov](https://github.com/ndyakov)\n\n---\n\n**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.1...v9.18.0-beta.1\n\n# 9.17.1 (2025-11-25)\n\n## 🐛 Bug Fixes\n\n- add wait to keyless commands list ([#3615](https://github.com/redis/go-redis/pull/3615)) by [@marcoferrer](https://github.com/marcoferrer)\n- fix(time): remove cached time optimization ([#3611](https://github.com/redis/go-redis/pull/3611)) by [@ndyakov](https://github.com/ndyakov)\n\n## 🧰 Maintenance\n\n- chore(deps): bump golangci/golangci-lint-action from 9.0.0 to 9.1.0 ([#3609](https://github.com/redis/go-redis/pull/3609))\n- chore(deps): bump actions/checkout from 5 to 6 ([#3610](https://github.com/redis/go-redis/pull/3610))\n- chore(script): fix help call in tag.sh ([#3606](https://github.com/redis/go-redis/pull/3606)) by [@ndyakov](https://github.com/ndyakov)\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@marcoferrer](https://github.com/marcoferrer) and [@ndyakov](https://github.com/ndyakov)\n\n---\n\n**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.17.1\n\n# 9.17.0 (2025-11-19)\n\n## 🚀 Highlights\n\n### Redis 8.4 Support\nAdded support for Redis 8.4, including new commands and features ([#3572](https://github.com/redis/go-redis/pull/3572))\n\n### Typed Errors\nIntroduced typed errors for better error handling using `errors.As` instead of string checks. Errors can now be wrapped and set to commands in hooks without breaking library functionality ([#3602](https://github.com/redis/go-redis/pull/3602))\n\n### New Commands\n- **CAS/CAD Commands**: Added support for Compare-And-Set/Compare-And-Delete operations with conditional matching (`IFEQ`, `IFNE`, `IFDEQ`, `IFDNE`) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595))\n- **MSETEX**: Atomically set multiple key-value pairs with expiration options and conditional modes ([#3580](https://github.com/redis/go-redis/pull/3580))\n- **XReadGroup CLAIM**: Consume both incoming and idle pending entries from streams in a single call ([#3578](https://github.com/redis/go-redis/pull/3578))\n- **ACL Commands**: Added `ACLGenPass`, `ACLUsers`, and `ACLWhoAmI` ([#3576](https://github.com/redis/go-redis/pull/3576))\n- **SLOWLOG Commands**: Added `SLOWLOG LEN` and `SLOWLOG RESET` ([#3585](https://github.com/redis/go-redis/pull/3585))\n- **LATENCY Commands**: Added `LATENCY LATEST` and `LATENCY RESET` ([#3584](https://github.com/redis/go-redis/pull/3584))\n\n### Search & Vector Improvements\n- **Hybrid Search**: Added  **EXPERIMENTAL** support for the new `FT.HYBRID` command ([#3573](https://github.com/redis/go-redis/pull/3573))\n- **Vector Range**: Added `VRANGE` command for vector sets ([#3543](https://github.com/redis/go-redis/pull/3543))\n- **FT.INFO Enhancements**: Added vector-specific attributes in FT.INFO response ([#3596](https://github.com/redis/go-redis/pull/3596))\n\n### Connection Pool Improvements\n- **Improved Connection Success Rate**: Implemented FIFO queue-based fairness and context pattern for connection creation to prevent premature cancellation under high concurrency ([#3518](https://github.com/redis/go-redis/pull/3518))\n- **Connection State Machine**: Resolved race conditions and improved pool performance with proper state tracking ([#3559](https://github.com/redis/go-redis/pull/3559))\n- **Pool Performance**: Significant performance improvements with faster semaphores, lockless hook manager, and reduced allocations (47-67% faster Get/Put operations) ([#3565](https://github.com/redis/go-redis/pull/3565))\n\n### Metrics & Observability\n- **Canceled Metric Attribute**: Added 'canceled' metrics attribute to distinguish context cancellation errors from other errors ([#3566](https://github.com/redis/go-redis/pull/3566))\n\n## ✨ New Features\n\n- Typed errors with wrapping support ([#3602](https://github.com/redis/go-redis/pull/3602)) by [@ndyakov](https://github.com/ndyakov)\n- CAS/CAD commands (marked as experimental) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595)) by [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis)\n- MSETEX command support ([#3580](https://github.com/redis/go-redis/pull/3580)) by [@ofekshenawa](https://github.com/ofekshenawa)\n- XReadGroup CLAIM argument ([#3578](https://github.com/redis/go-redis/pull/3578)) by [@ofekshenawa](https://github.com/ofekshenawa)\n- ACL commands: GenPass, Users, WhoAmI ([#3576](https://github.com/redis/go-redis/pull/3576)) by [@destinyoooo](https://github.com/destinyoooo)\n- SLOWLOG commands: LEN, RESET ([#3585](https://github.com/redis/go-redis/pull/3585)) by [@destinyoooo](https://github.com/destinyoooo)\n- LATENCY commands: LATEST, RESET ([#3584](https://github.com/redis/go-redis/pull/3584)) by [@destinyoooo](https://github.com/destinyoooo)\n- Hybrid search command (FT.HYBRID) ([#3573](https://github.com/redis/go-redis/pull/3573)) by [@htemelski-redis](https://github.com/htemelski-redis)\n- Vector range command (VRANGE) ([#3543](https://github.com/redis/go-redis/pull/3543)) by [@cxljs](https://github.com/cxljs)\n- Vector-specific attributes in FT.INFO ([#3596](https://github.com/redis/go-redis/pull/3596)) by [@ndyakov](https://github.com/ndyakov)\n- Improved connection pool success rate with FIFO queue ([#3518](https://github.com/redis/go-redis/pull/3518)) by [@cyningsun](https://github.com/cyningsun)\n- Canceled metrics attribute for context errors ([#3566](https://github.com/redis/go-redis/pull/3566)) by [@pvragov](https://github.com/pvragov)\n\n## 🐛 Bug Fixes\n\n- Fixed Failover Client MaintNotificationsConfig ([#3600](https://github.com/redis/go-redis/pull/3600)) by [@ajax16384](https://github.com/ajax16384)\n- Fixed ACLGenPass function to use the bit parameter ([#3597](https://github.com/redis/go-redis/pull/3597)) by [@destinyoooo](https://github.com/destinyoooo)\n- Return error instead of panic from commands ([#3568](https://github.com/redis/go-redis/pull/3568)) by [@dragneelfps](https://github.com/dragneelfps)\n- Safety harness in `joinErrors` to prevent panic ([#3577](https://github.com/redis/go-redis/pull/3577)) by [@manisharma](https://github.com/manisharma)\n\n## ⚡ Performance\n\n- Connection state machine with race condition fixes ([#3559](https://github.com/redis/go-redis/pull/3559)) by [@ndyakov](https://github.com/ndyakov)\n- Pool performance improvements: 47-67% faster Get/Put, 33% less memory, 50% fewer allocations ([#3565](https://github.com/redis/go-redis/pull/3565)) by [@ndyakov](https://github.com/ndyakov)\n\n## 🧪 Testing & Infrastructure\n\n- Updated to Redis 8.4.0 image ([#3603](https://github.com/redis/go-redis/pull/3603)) by [@ndyakov](https://github.com/ndyakov)\n- Added Redis 8.4-RC1-pre to CI ([#3572](https://github.com/redis/go-redis/pull/3572)) by [@ndyakov](https://github.com/ndyakov)\n- Refactored tests for idiomatic Go ([#3561](https://github.com/redis/go-redis/pull/3561), [#3562](https://github.com/redis/go-redis/pull/3562), [#3563](https://github.com/redis/go-redis/pull/3563)) by [@12ya](https://github.com/12ya)\n\n## 👥 Contributors\n\nWe'd like to thank all the contributors who worked on this release!\n\n[@12ya](https://github.com/12ya), [@ajax16384](https://github.com/ajax16384), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@destinyoooo](https://github.com/destinyoooo), [@dragneelfps](https://github.com/dragneelfps), [@htemelski-redis](https://github.com/htemelski-redis), [@manisharma](https://github.com/manisharma), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@pvragov](https://github.com/pvragov)\n\n---\n\n**Full Changelog**: https://github.com/redis/go-redis/compare/v9.16.0...v9.17.0\n\n# 9.16.0 (2025-10-23)\n\n## 🚀 Highlights\n\n### Maintenance Notifications Support\n\nThis release introduces comprehensive support for Redis maintenance notifications, enabling applications to handle server maintenance events gracefully. The new `maintnotifications` package provides:\n\n- **RESP3 Push Notifications**: Full support for Redis RESP3 protocol push notifications\n- **Connection Handoff**: Automatic connection migration during server maintenance with configurable retry policies and circuit breakers\n- **Graceful Degradation**: Configurable timeout relaxation during maintenance windows to prevent false failures\n- **Event-Driven Architecture**: Background workers with on-demand scaling for efficient handoff processing\n- **Production-Ready**: Comprehensive E2E testing framework and monitoring capabilities\n\nFor detailed usage examples and configuration options, see the [maintenance notifications documentation](maintnotifications/README.md).\n\n## ✨ New Features\n\n- **Trace Filtering**: Add support for filtering traces for specific commands, including pipeline operations and dial operations ([#3519](https://github.com/redis/go-redis/pull/3519), [#3550](https://github.com/redis/go-redis/pull/3550))\n  - New `TraceCmdFilter` option to selectively trace commands\n  - Reduces overhead by excluding high-frequency or low-value commands from traces\n\n## 🐛 Bug Fixes\n\n- **Pipeline Error Handling**: Fix issue where pipeline repeatedly sets the same error ([#3525](https://github.com/redis/go-redis/pull/3525))\n- **Connection Pool**: Ensure re-authentication does not interfere with connection handoff operations ([#3547](https://github.com/redis/go-redis/pull/3547))\n\n## 🔧 Improvements\n\n- **Hash Commands**: Update hash command implementations ([#3523](https://github.com/redis/go-redis/pull/3523))\n- **OpenTelemetry**: Use `metric.WithAttributeSet` to avoid unnecessary attribute copying in redisotel ([#3552](https://github.com/redis/go-redis/pull/3552))\n\n## 📚 Documentation\n\n- **Cluster Client**: Add explanation for why `MaxRetries` is disabled for `ClusterClient` ([#3551](https://github.com/redis/go-redis/pull/3551))\n\n## 🧪 Testing & Infrastructure\n\n- **E2E Testing**: Upgrade E2E testing framework with improved reliability and coverage ([#3541](https://github.com/redis/go-redis/pull/3541))\n- **Release Process**: Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530))\n\n## 📦 Dependencies\n\n- Bump `rojopolis/spellcheck-github-actions` from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520))\n- Bump `github/codeql-action` from 3 to 4 ([#3544](https://github.com/redis/go-redis/pull/3544))\n\n## 👥 Contributors\n\nWe'd like to thank all the contributors who worked on this release!\n\n[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@Sovietaced](https://github.com/Sovietaced), [@Udhayarajan](https://github.com/Udhayarajan), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@Pika-Gopher](https://github.com/Pika-Gopher), [@cxljs](https://github.com/cxljs), [@huiyifyj](https://github.com/huiyifyj), [@omid-h70](https://github.com/omid-h70)\n\n---\n\n**Full Changelog**: https://github.com/redis/go-redis/compare/v9.14.0...v9.16.0\n\n\n# 9.15.0 was accidentally released. Please use version 9.16.0 instead.\n\n# 9.15.0-beta.3 (2025-09-26)\n\n## Highlights\nThis beta release includes a pre-production version of processing push notifications and hitless upgrades.\n\n# Changes\n\n- chore: Update hash_commands.go ([#3523](https://github.com/redis/go-redis/pull/3523))\n\n## 🚀 New Features\n\n- feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418))\n\n## 🐛 Bug Fixes\n\n- fix: pipeline repeatedly sets the error ([#3525](https://github.com/redis/go-redis/pull/3525))\n\n## 🧰 Maintenance\n\n- chore(deps): bump rojopolis/spellcheck-github-actions from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520))\n- feat(e2e-testing): maintnotifications e2e and refactor ([#3526](https://github.com/redis/go-redis/pull/3526))\n- feat(tag.sh): Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@cxljs](https://github.com/cxljs), [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), and [@omid-h70](https://github.com/omid-h70)\n\n\n# 9.15.0-beta.1 (2025-09-10)\n\n## Highlights\nThis beta release includes a pre-production version of processing push notifications and hitless upgrades.\n\n### Hitless Upgrades\nHitless upgrades is a major new feature that allows for zero-downtime upgrades in Redis clusters.\nYou can find more information in the [Hitless Upgrades documentation](https://github.com/redis/go-redis/tree/master/hitless).\n\n# Changes\n\n## 🚀 New Features\n- [CAE-1088] & [CAE-1072] feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@ofekshenawa](https://github.com/ofekshenawa)\n\n\n# 9.14.0 (2025-09-10)\n\n## Highlights\n- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510))\n\n# Changes\n\n## 🚀 New Features\n\n- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510))\n\n## 🐛 Bug Fixes\n\n- fix: SetErr on Cmd if the command cannot be queued correctly in multi/exec ([#3509](https://github.com/redis/go-redis/pull/3509))\n\n## 🧰 Maintenance\n\n- Updates release drafter config to exclude dependabot ([#3511](https://github.com/redis/go-redis/pull/3511))\n- chore(deps): bump actions/setup-go from 5 to 6 ([#3504](https://github.com/redis/go-redis/pull/3504))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@elena-kolevska](https://github.com/elena-kolevksa), [@htemelski-redis](https://github.com/htemelski-redis) and [@ndyakov](https://github.com/ndyakov)\n\n\n# 9.13.0 (2025-09-03)\n\n## Highlights\n- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496))\n- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470))\n- Fixes on Read and Write buffer sizes and UniversalOptions\n\n## Changes\n- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496))\n- fix(test): fix a timing issue in pubsub test ([#3498](https://github.com/redis/go-redis/pull/3498))\n- Allow users to enable read-write splitting in failover mode. ([#3482](https://github.com/redis/go-redis/pull/3482))\n- Set the read/write buffer size of the sentinel client to 4KiB ([#3476](https://github.com/redis/go-redis/pull/3476))\n\n## 🚀 New Features\n\n- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499))\n- Support subscriptions against cluster slave nodes ([#3480](https://github.com/redis/go-redis/pull/3480))\n- Add wait metrics to otel ([#3493](https://github.com/redis/go-redis/pull/3493))\n- Clean failing timeout implementation ([#3472](https://github.com/redis/go-redis/pull/3472))\n\n## 🐛 Bug Fixes\n\n- Do not assume that all non-IP hosts are loopbacks ([#3085](https://github.com/redis/go-redis/pull/3085))\n- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470))\n\n## 🧰 Maintenance\n\n- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499))\n- fix(make test): Add default env in makefile ([#3491](https://github.com/redis/go-redis/pull/3491))\n- Update the introduction to running tests in README.md ([#3495](https://github.com/redis/go-redis/pull/3495))\n- test: Add comprehensive edge case tests for IncrByFloat command ([#3477](https://github.com/redis/go-redis/pull/3477))\n- Set the default read/write buffer size of Redis connection to 32KiB ([#3483](https://github.com/redis/go-redis/pull/3483))\n- Bumps test image to 8.2.1-pre ([#3478](https://github.com/redis/go-redis/pull/3478))\n- fix UniversalOptions miss ReadBufferSize and WriteBufferSize options ([#3485](https://github.com/redis/go-redis/pull/3485))\n- chore(deps): bump actions/checkout from 4 to 5 ([#3484](https://github.com/redis/go-redis/pull/3484))\n- Removes dry run for stale issues policy ([#3471](https://github.com/redis/go-redis/pull/3471))\n- Update otel metrics URL ([#3474](https://github.com/redis/go-redis/pull/3474))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@LINKIWI](https://github.com/LINKIWI), [@cxljs](https://github.com/cxljs), [@cybersmeashish](https://github.com/cybersmeashish), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@mwhooker](https://github.com/mwhooker), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@suever](https://github.com/suever)\n\n\n# 9.12.1 (2025-08-11)\n## 🚀 Highlights\nIn the last version (9.12.0) the client introduced bigger write and read buffer sized. The default value we set was 512KiB.\nHowever, users reported that this is too big for most use cases and can lead to high memory usage.\nIn this version the default value is changed to 256KiB. The `README.md` was updated to reflect the\ncorrect default value and include a note that the default value can be changed.\n\n## 🐛 Bug Fixes\n\n- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468))\n\n## 🧰 Maintenance\n\n- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468))\n- chore: update & fix otel example ([#3466](https://github.com/redis/go-redis/pull/3466))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@ndyakov](https://github.com/ndyakov) and [@vmihailenco](https://github.com/vmihailenco)\n\n# 9.12.0 (2025-08-05)\n\n## 🚀 Highlights\n\n- This release includes support for [Redis 8.2](https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.2-release-notes/).\n- Introduces an experimental Query Builders for `FTSearch`, `FTAggregate` and other search commands.\n- Adds support for `EPSILON` option in `FT.VSIM`.\n- Includes bug fixes and improvements contributed by the community related to ring and [redisotel](https://github.com/redis/go-redis/tree/master/extra/redisotel).\n\n## Changes\n- Improve stale issue workflow ([#3458](https://github.com/redis/go-redis/pull/3458))\n- chore(ci): Add 8.2 rc2 pre build for CI ([#3459](https://github.com/redis/go-redis/pull/3459))\n- Added new stream commands ([#3450](https://github.com/redis/go-redis/pull/3450))\n- feat: Add \"skip_verify\" to Sentinel ([#3428](https://github.com/redis/go-redis/pull/3428))\n- fix: `errors.Join` requires Go 1.20 or later ([#3442](https://github.com/redis/go-redis/pull/3442))\n- DOC-4344 document quickstart examples ([#3426](https://github.com/redis/go-redis/pull/3426))\n- feat(bitop): add support for the new bitop operations ([#3409](https://github.com/redis/go-redis/pull/3409))\n\n## 🚀 New Features\n\n- feat: recover addIdleConn may occur panic ([#2445](https://github.com/redis/go-redis/pull/2445))\n- feat(ring): specify custom health check func via HeartbeatFn option ([#2940](https://github.com/redis/go-redis/pull/2940))\n- Add Query Builder for RediSearch commands ([#3436](https://github.com/redis/go-redis/pull/3436))\n- add configurable buffer sizes for Redis connections ([#3453](https://github.com/redis/go-redis/pull/3453))\n- Add VAMANA vector type to RediSearch ([#3449](https://github.com/redis/go-redis/pull/3449))\n- VSIM add `EPSILON` option ([#3454](https://github.com/redis/go-redis/pull/3454))\n- Add closing support to otel metrics instrumentation ([#3444](https://github.com/redis/go-redis/pull/3444))\n\n## 🐛 Bug Fixes\n\n- fix(redisotel): fix buggy append in reportPoolStats ([#3122](https://github.com/redis/go-redis/pull/3122))\n- fix(search): return results even if doc is empty ([#3457](https://github.com/redis/go-redis/pull/3457))\n- [ISSUE-3402]: Ring.Pipelined return dial timeout error ([#3403](https://github.com/redis/go-redis/pull/3403))\n\n## 🧰 Maintenance\n\n- Merges stale issues jobs into one job with two steps ([#3463](https://github.com/redis/go-redis/pull/3463))\n- improve code readability ([#3446](https://github.com/redis/go-redis/pull/3446))\n- chore(release): 9.12.0-beta.1 ([#3460](https://github.com/redis/go-redis/pull/3460))\n- DOC-5472 time series doc examples ([#3443](https://github.com/redis/go-redis/pull/3443))\n- Add VAMANA compression algorithm tests ([#3461](https://github.com/redis/go-redis/pull/3461))\n- bumped redis 8.2 version used in the CI/CD ([#3451](https://github.com/redis/go-redis/pull/3451))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@andy-stark-redis](https://github.com/andy-stark-redis), [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@jouir](https://github.com/jouir), [@monkey92t](https://github.com/monkey92t), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@rokn](https://github.com/rokn), [@smnvdev](https://github.com/smnvdev), [@strobil](https://github.com/strobil) and [@wzy9607](https://github.com/wzy9607)\n\n## New Contributors\n* [@htemelski-redis](https://github.com/htemelski-redis) made their first contribution in [#3409](https://github.com/redis/go-redis/pull/3409)\n* [@smnvdev](https://github.com/smnvdev) made their first contribution in [#3403](https://github.com/redis/go-redis/pull/3403)\n* [@rokn](https://github.com/rokn) made their first contribution in [#3444](https://github.com/redis/go-redis/pull/3444)\n\n# 9.11.0 (2025-06-24)\n\n## 🚀 Highlights\n\nFixes TxPipeline to work correctly in cluster scenarios, allowing execution of commands\nonly in the same slot.\n\n# Changes\n\n## 🚀 New Features\n\n- Set cluster slot for `scan` commands, rather than random ([#2623](https://github.com/redis/go-redis/pull/2623))\n- Add CredentialsProvider field to UniversalOptions ([#2927](https://github.com/redis/go-redis/pull/2927))\n- feat(redisotel): add WithCallerEnabled option ([#3415](https://github.com/redis/go-redis/pull/3415))\n\n## 🐛 Bug Fixes\n\n- fix(txpipeline): keyless commands should take the slot of the keyed ([#3411](https://github.com/redis/go-redis/pull/3411))\n- fix(loading): cache the loaded flag for slave nodes ([#3410](https://github.com/redis/go-redis/pull/3410))\n- fix(txpipeline): should return error on multi/exec on multiple slots ([#3408](https://github.com/redis/go-redis/pull/3408))\n- fix: check if the shard exists to avoid returning nil ([#3396](https://github.com/redis/go-redis/pull/3396))\n\n## 🧰 Maintenance\n\n- feat: optimize connection pool waitTurn ([#3412](https://github.com/redis/go-redis/pull/3412))\n- chore(ci): update CI redis builds ([#3407](https://github.com/redis/go-redis/pull/3407))\n- chore: remove a redundant method from `Ring`, `Client` and `ClusterClient` ([#3401](https://github.com/redis/go-redis/pull/3401))\n- test: refactor TestBasicCredentials using table-driven tests ([#3406](https://github.com/redis/go-redis/pull/3406))\n- perf: reduce unnecessary memory allocation operations ([#3399](https://github.com/redis/go-redis/pull/3399))\n- fix: insert entry during iterating over a map ([#3398](https://github.com/redis/go-redis/pull/3398))\n- DOC-5229 probabilistic data type examples ([#3413](https://github.com/redis/go-redis/pull/3413))\n- chore(deps): bump rojopolis/spellcheck-github-actions from 0.49.0 to 0.51.0 ([#3414](https://github.com/redis/go-redis/pull/3414))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@andy-stark-redis](https://github.com/andy-stark-redis), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@cxljs](https://github.com/cxljs), [@dcherubini](https://github.com/dcherubini), [@dependabot[bot]](https://github.com/apps/dependabot), [@iamamirsalehi](https://github.com/iamamirsalehi), [@ndyakov](https://github.com/ndyakov), [@pete-woods](https://github.com/pete-woods), [@twz915](https://github.com/twz915) and [dependabot[bot]](https://github.com/apps/dependabot)\n\n# 9.10.0 (2025-06-06)\n\n## 🚀 Highlights\n\n`go-redis` now supports [vector sets](https://redis.io/docs/latest/develop/data-types/vector-sets/). This data type is marked\nas \"in preview\" in Redis and its support in `go-redis` is marked as experimental. You can find examples in the documentation and\nin the `doctests` folder.\n\n# Changes\n\n## 🚀 New Features\n\n- feat: support vectorset ([#3375](https://github.com/redis/go-redis/pull/3375))\n\n## 🧰 Maintenance\n\n- Add the missing NewFloatSliceResult for testing ([#3393](https://github.com/redis/go-redis/pull/3393))\n- DOC-5078 vector set examples ([#3394](https://github.com/redis/go-redis/pull/3394))\n\n## Contributors\nWe'd like to thank all the contributors who worked on this release!\n\n[@AndBobsYourUncle](https://github.com/AndBobsYourUncle), [@andy-stark-redis](https://github.com/andy-stark-redis), [@fukua95](https://github.com/fukua95) and [@ndyakov](https://github.com/ndyakov)\n\n\n\n# 9.9.0 (2025-05-27)\n\n## 🚀 Highlights\n- **Token-based Authentication**: Added `StreamingCredentialsProvider` for dynamic credential updates (experimental)\n  - Can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) for Azure AD authentication\n- **Connection Statistics**: Added connection waiting statistics for better monitoring\n- **Failover Improvements**: Added `ParseFailoverURL` for easier failover configuration\n- **Ring Client Enhancements**: Added shard access methods for better Pub/Sub management\n\n## ✨ New Features\n- Added `StreamingCredentialsProvider` for token-based authentication ([#3320](https://github.com/redis/go-redis/pull/3320))\n  - Supports dynamic credential updates\n  - Includes connection close hooks\n  - Note: Currently marked as experimental\n- Added `ParseFailoverURL` for parsing failover URLs ([#3362](https://github.com/redis/go-redis/pull/3362))\n- Added connection waiting statistics ([#2804](https://github.com/redis/go-redis/pull/2804))\n- Added new utility functions:\n  - `ParseFloat` and `MustParseFloat` in public utils package ([#3371](https://github.com/redis/go-redis/pull/3371))\n  - Unit tests for `Atoi`, `ParseInt`, `ParseUint`, and `ParseFloat` ([#3377](https://github.com/redis/go-redis/pull/3377))\n- Added Ring client shard access methods:\n  - `GetShardClients()` to retrieve all active shard clients\n  - `GetShardClientForKey(key string)` to get the shard client for a specific key ([#3388](https://github.com/redis/go-redis/pull/3388))\n\n## 🐛 Bug Fixes\n- Fixed routing reads to loading slave nodes ([#3370](https://github.com/redis/go-redis/pull/3370))\n- Added support for nil lag in XINFO GROUPS ([#3369](https://github.com/redis/go-redis/pull/3369))\n- Fixed pool acquisition timeout issues ([#3381](https://github.com/redis/go-redis/pull/3381))\n- Optimized unnecessary copy operations ([#3376](https://github.com/redis/go-redis/pull/3376))\n\n## 📚 Documentation\n- Updated documentation for XINFO GROUPS with nil lag support ([#3369](https://github.com/redis/go-redis/pull/3369))\n- Added package-level comments for new features\n\n## ⚡ Performance and Reliability\n- Optimized `ReplaceSpaces` function ([#3383](https://github.com/redis/go-redis/pull/3383))\n- Set default value for `Options.Protocol` in `init()` ([#3387](https://github.com/redis/go-redis/pull/3387))\n- Exported pool errors for public consumption ([#3380](https://github.com/redis/go-redis/pull/3380))\n\n## 🔧 Dependencies and Infrastructure\n- Updated Redis CI to version 8.0.1 ([#3372](https://github.com/redis/go-redis/pull/3372))\n- Updated spellcheck GitHub Actions ([#3389](https://github.com/redis/go-redis/pull/3389))\n- Removed unused parameters ([#3382](https://github.com/redis/go-redis/pull/3382), [#3384](https://github.com/redis/go-redis/pull/3384))\n\n## 🧪 Testing\n- Added unit tests for pool acquisition timeout ([#3381](https://github.com/redis/go-redis/pull/3381))\n- Added unit tests for utility functions ([#3377](https://github.com/redis/go-redis/pull/3377))\n\n## 👥 Contributors\n\nWe would like to thank all the contributors who made this release possible:\n\n[@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@LINKIWI](https://github.com/LINKIWI), [@iamamirsalehi](https://github.com/iamamirsalehi), [@fukua95](https://github.com/fukua95), [@lzakharov](https://github.com/lzakharov), [@DengY11](https://github.com/DengY11)\n\n## 📝 Changelog\n\nFor a complete list of changes, see the [full changelog](https://github.com/redis/go-redis/compare/v9.8.0...v9.9.0).\n\n# 9.8.0 (2025-04-30)\n\n## 🚀 Highlights\n- **Redis 8 Support**: Full compatibility with Redis 8.0, including testing and CI integration\n- **Enhanced Hash Operations**: Added support for new hash commands (`HGETDEL`, `HGETEX`, `HSETEX`) and `HSTRLEN` command\n- **Search Improvements**: Enabled Search DIALECT 2 by default and added `CountOnly` argument for `FT.Search`\n\n## ✨ New Features\n- Added support for new hash commands: `HGETDEL`, `HGETEX`, `HSETEX` ([#3305](https://github.com/redis/go-redis/pull/3305))\n- Added `HSTRLEN` command for hash operations ([#2843](https://github.com/redis/go-redis/pull/2843))\n- Added `Do` method for raw query by single connection from `pool.Conn()` ([#3182](https://github.com/redis/go-redis/pull/3182))\n- Prevent false-positive marshaling by treating zero time.Time as empty in isEmptyValue ([#3273](https://github.com/redis/go-redis/pull/3273))\n- Added FailoverClusterClient support for Universal client ([#2794](https://github.com/redis/go-redis/pull/2794))\n- Added support for cluster mode with `IsClusterMode` config parameter ([#3255](https://github.com/redis/go-redis/pull/3255))\n- Added client name support in `HELLO` RESP handshake ([#3294](https://github.com/redis/go-redis/pull/3294))\n- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213))\n- Added read-only option for failover configurations ([#3281](https://github.com/redis/go-redis/pull/3281))\n- Added `CountOnly` argument for `FT.Search` to use `LIMIT 0 0` ([#3338](https://github.com/redis/go-redis/pull/3338))\n- Added `DB` option support in `NewFailoverClusterClient` ([#3342](https://github.com/redis/go-redis/pull/3342))\n- Added `nil` check for the options when creating a client ([#3363](https://github.com/redis/go-redis/pull/3363))\n\n## 🐛 Bug Fixes\n- Fixed `PubSub` concurrency safety issues ([#3360](https://github.com/redis/go-redis/pull/3360))\n- Fixed panic caused when argument is `nil` ([#3353](https://github.com/redis/go-redis/pull/3353))\n- Improved error handling when fetching master node from sentinels ([#3349](https://github.com/redis/go-redis/pull/3349))\n- Fixed connection pool timeout issues and increased retries ([#3298](https://github.com/redis/go-redis/pull/3298))\n- Fixed context cancellation error leading to connection spikes on Primary instances ([#3190](https://github.com/redis/go-redis/pull/3190))\n- Fixed RedisCluster client to consider `MASTERDOWN` a retriable error ([#3164](https://github.com/redis/go-redis/pull/3164))\n- Fixed tracing to show complete commands instead of truncated versions ([#3290](https://github.com/redis/go-redis/pull/3290))\n- Fixed OpenTelemetry instrumentation to prevent multiple span reporting ([#3168](https://github.com/redis/go-redis/pull/3168))\n- Fixed `FT.Search` Limit argument and added `CountOnly` argument for limit 0 0 ([#3338](https://github.com/redis/go-redis/pull/3338))\n- Fixed missing command in interface ([#3344](https://github.com/redis/go-redis/pull/3344))\n- Fixed slot calculation for `COUNTKEYSINSLOT` command ([#3327](https://github.com/redis/go-redis/pull/3327))\n- Updated PubSub implementation with correct context ([#3329](https://github.com/redis/go-redis/pull/3329))\n\n## 📚 Documentation\n- Added hash search examples ([#3357](https://github.com/redis/go-redis/pull/3357))\n- Fixed documentation comments ([#3351](https://github.com/redis/go-redis/pull/3351))\n- Added `CountOnly` search example ([#3345](https://github.com/redis/go-redis/pull/3345))\n- Added examples for list commands: `LLEN`, `LPOP`, `LPUSH`, `LRANGE`, `RPOP`, `RPUSH` ([#3234](https://github.com/redis/go-redis/pull/3234))\n- Added `SADD` and `SMEMBERS` command examples ([#3242](https://github.com/redis/go-redis/pull/3242))\n- Updated `README.md` to use Redis Discord guild ([#3331](https://github.com/redis/go-redis/pull/3331))\n- Updated `HExpire` command documentation ([#3355](https://github.com/redis/go-redis/pull/3355))\n- Featured OpenTelemetry instrumentation more prominently ([#3316](https://github.com/redis/go-redis/pull/3316))\n- Updated `README.md` with additional information ([#310ce55](https://github.com/redis/go-redis/commit/310ce55))\n\n## ⚡ Performance and Reliability\n- Bound connection pool background dials to configured dial timeout ([#3089](https://github.com/redis/go-redis/pull/3089))\n- Ensured context isn't exhausted via concurrent query ([#3334](https://github.com/redis/go-redis/pull/3334))\n\n## 🔧 Dependencies and Infrastructure\n- Updated testing image to Redis 8.0-RC2 ([#3361](https://github.com/redis/go-redis/pull/3361))\n- Enabled CI for Redis CE 8.0 ([#3274](https://github.com/redis/go-redis/pull/3274))\n- Updated various dependencies:\n  - Bumped golangci/golangci-lint-action from 6.5.0 to 7.0.0 ([#3354](https://github.com/redis/go-redis/pull/3354))\n  - Bumped rojopolis/spellcheck-github-actions ([#3336](https://github.com/redis/go-redis/pull/3336))\n  - Bumped golang.org/x/net in example/otel ([#3308](https://github.com/redis/go-redis/pull/3308))\n- Migrated golangci-lint configuration to v2 format ([#3354](https://github.com/redis/go-redis/pull/3354))\n\n## ⚠️ Breaking Changes\n- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213))\n- Dropped RedisGears (Triggers and Functions) support ([#3321](https://github.com/redis/go-redis/pull/3321))\n- Dropped FT.PROFILE command that was never enabled ([#3323](https://github.com/redis/go-redis/pull/3323))\n\n## 🔒 Security\n- Fixed network error handling on SETINFO (CVE-2025-29923) ([#3295](https://github.com/redis/go-redis/pull/3295))\n\n## 🧪 Testing\n- Added integration tests for Redis 8 behavior changes in Redis Search ([#3337](https://github.com/redis/go-redis/pull/3337))\n- Added vector types INT8 and UINT8 tests ([#3299](https://github.com/redis/go-redis/pull/3299))\n- Added test codes for search_commands.go ([#3285](https://github.com/redis/go-redis/pull/3285))\n- Fixed example test sorting ([#3292](https://github.com/redis/go-redis/pull/3292))\n\n## 👥 Contributors\n\nWe would like to thank all the contributors who made this release possible:\n\n[@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi)\n\n\n# Old Changelog\n## Unreleased\n\n### Changed\n\n* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980))\n  Users can use the OpenTelemetry sampler to control the sampling behavior.\n  For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep\n  a similar behavior (drop orphan spans) of `go-redis` as before.\n\n## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29)\n\n\n### Features\n\n* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602))\n* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe))\n* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af))\n\n\n\n## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01)\n\n\n### Bug Fixes\n\n* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241))\n\n\n### Features\n\n* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e))\n* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8))\n* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af))\n\n\n\n## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02)\n\n### New Features\n\n- feat(scan): scan time.Time sets the default decoding (#2413)\n- Add support for CLUSTER LINKS command (#2504)\n- Add support for acl dryrun command (#2502)\n- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500)\n- Add support for LCS Command (#2480)\n- Add support for BZMPOP (#2456)\n- Adding support for ZMPOP command (#2408)\n- Add support for LMPOP (#2440)\n- feat: remove pool unused fields (#2438)\n- Expiretime and PExpireTime (#2426)\n- Implement `FUNCTION` group of commands (#2475)\n- feat(zadd): add ZAddLT and ZAddGT (#2429)\n- Add: Support for COMMAND LIST command (#2491)\n- Add support for BLMPOP (#2442)\n- feat: check pipeline.Do to prevent confusion with Exec (#2517)\n- Function stats, function kill, fcall and fcall_ro (#2486)\n- feat: Add support for CLUSTER SHARDS command (#2507)\n- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498)\n\n### Fixed\n\n- fix: eval api cmd.SetFirstKeyPos (#2501)\n- fix: limit the number of connections created (#2441)\n- fixed #2462  v9 continue support dragonfly,  it's Hello command return \"NOAUTH Authentication required\" error (#2479)\n- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458)\n- fix: group lag can be null (#2448)\n\n### Maintenance\n\n- Updating to the latest version of redis (#2508)\n- Allowing for running tests on a port other than the fixed 6380 (#2466)\n- redis 7.0.8 in tests (#2450)\n- docs: Update redisotel example for v9 (#2425)\n- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476)\n- chore: add Chinese translation (#2436)\n- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421)\n- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420)\n- chore(deps): bump actions/setup-go from 3 to 4 (#2495)\n- docs: add instructions for the HSet api (#2503)\n- docs: add reading lag field comment (#2451)\n- test: update go mod before testing(go mod tidy) (#2423)\n- docs: fix comment typo (#2505)\n- test: remove testify (#2463)\n- refactor: change ListElementCmd to KeyValuesCmd. (#2443)\n- fix(appendArg): appendArg case special type (#2489)\n\n## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01)\n\n### Features\n\n* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65))\n\n## v9 2023-01-30\n\n### Breaking\n\n- Changed Pipelines to not be thread-safe any more.\n\n### Added\n\n- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was\n  contributed by @monkey92t who has done the majority of work in this release.\n- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts\n  and deadlines. See\n  [Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details.\n- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example,\n  `redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`.\n- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See\n  [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html)\n- Added `redis.HasErrorPrefix` to help working with errors.\n\n### Changed\n\n- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is\n  completely gone in v9.\n- Reworked hook interface and added `DialHook`.\n- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See\n  [example](example/otel) and\n  [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html).\n- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making\n  an allocation.\n- Renamed the option `MaxConnAge` to `ConnMaxLifetime`.\n- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`.\n- Removed connection reaper in favor of `MaxIdleConns`.\n- Removed `WithContext` since `context.Context` can be passed directly as an arg.\n- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and\n  it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to\n  reset commands for some reason.\n\n### Fixed\n\n- Improved and fixed pipeline retries.\n- As usually, added support for more commands and fixed some bugs.\n"
  },
  {
    "path": "RELEASING.md",
    "content": "# Releasing\n\n1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub:\n\n```shell\nTAG=v1.0.0 ./scripts/release.sh\n```\n\n2. Open a pull request and wait for the build to finish.\n\n3. Merge the pull request and run `tag.sh` to create tags for packages:\n\n```shell\nTAG=v1.0.0 ./scripts/tag.sh\n```\n"
  },
  {
    "path": "acl_commands.go",
    "content": "package redis\n\nimport \"context\"\n\ntype ACLCmdable interface {\n\tACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd\n\n\tACLLog(ctx context.Context, count int64) *ACLLogCmd\n\tACLLogReset(ctx context.Context) *StatusCmd\n\n\tACLGenPass(ctx context.Context, bit int) *StringCmd\n\n\tACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd\n\tACLDelUser(ctx context.Context, username string) *IntCmd\n\tACLUsers(ctx context.Context) *StringSliceCmd\n\tACLWhoAmI(ctx context.Context) *StringCmd\n\tACLList(ctx context.Context) *StringSliceCmd\n\n\tACLCat(ctx context.Context) *StringSliceCmd\n\tACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd\n}\n\ntype ACLCatArgs struct {\n\tCategory string\n}\n\nfunc (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd {\n\targs := make([]interface{}, 0, 3+len(command))\n\targs = append(args, \"acl\", \"dryrun\", username)\n\targs = append(args, command...)\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLLog(ctx context.Context, count int64) *ACLLogCmd {\n\targs := make([]interface{}, 0, 3)\n\targs = append(args, \"acl\", \"log\")\n\tif count > 0 {\n\t\targs = append(args, count)\n\t}\n\tcmd := NewACLLogCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"acl\", \"log\", \"reset\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"acl\", \"deluser\", username)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd {\n\targs := make([]interface{}, 3+len(rules))\n\targs[0] = \"acl\"\n\targs[1] = \"setuser\"\n\targs[2] = username\n\tfor i, rule := range rules {\n\t\targs[i+3] = rule\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLGenPass(ctx context.Context, bit int) *StringCmd {\n\targs := make([]interface{}, 0, 3)\n\targs = append(args, \"acl\", \"genpass\")\n\tif bit > 0 {\n\t\targs = append(args, bit)\n\t}\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLUsers(ctx context.Context) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"acl\", \"users\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLWhoAmI(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"acl\", \"whoami\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLList(ctx context.Context) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"acl\", \"list\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"acl\", \"cat\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd {\n\t// if there is a category passed, build new cmd, if there isn't - use the ACLCat method\n\tif options != nil && options.Category != \"\" {\n\t\tcmd := NewStringSliceCmd(ctx, \"acl\", \"cat\", options.Category)\n\t\t_ = c(ctx, cmd)\n\t\treturn cmd\n\t}\n\n\treturn c.ACLCat(ctx)\n}\n"
  },
  {
    "path": "acl_commands_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\n\t\"github.com/redis/go-redis/v9\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nvar TestUserName string = \"goredis\"\nvar _ = Describe(\"ACL\", func() {\n\tvar client *redis.Client\n\tvar ctx context.Context\n\n\tBeforeEach(func() {\n\t\tctx = context.Background()\n\t\topt := redisOptions()\n\t\tclient = redis.NewClient(opt)\n\t})\n\n\tIt(\"should ACL LOG\", Label(\"NonRedisEnterprise\"), func() {\n\t\tExpect(client.ACLLogReset(ctx).Err()).NotTo(HaveOccurred())\n\t\terr := client.Do(ctx, \"acl\", \"setuser\", \"test\", \">test\", \"on\", \"allkeys\", \"+get\").Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tclientAcl := redis.NewClient(redisOptions())\n\t\tclientAcl.Options().Username = \"test\"\n\t\tclientAcl.Options().Password = \"test\"\n\t\tclientAcl.Options().DB = 0\n\t\t_ = clientAcl.Set(ctx, \"mystring\", \"foo\", 0).Err()\n\t\t_ = clientAcl.HSet(ctx, \"myhash\", \"foo\", \"bar\").Err()\n\t\t_ = clientAcl.SAdd(ctx, \"myset\", \"foo\", \"bar\").Err()\n\n\t\tlogEntries, err := client.ACLLog(ctx, 10).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(logEntries)).To(Equal(4))\n\n\t\tfor _, entry := range logEntries {\n\t\t\tExpect(entry.Reason).To(Equal(\"command\"))\n\t\t\tExpect(entry.Context).To(Equal(\"toplevel\"))\n\t\t\tExpect(entry.Object).NotTo(BeEmpty())\n\t\t\tExpect(entry.Username).To(Equal(\"test\"))\n\t\t\tExpect(entry.AgeSeconds).To(BeNumerically(\">=\", 0))\n\t\t\tExpect(entry.ClientInfo).NotTo(BeNil())\n\t\t\tExpect(entry.EntryID).To(BeNumerically(\">=\", 0))\n\t\t\tExpect(entry.TimestampCreated).To(BeNumerically(\">=\", 0))\n\t\t\tExpect(entry.TimestampLastUpdated).To(BeNumerically(\">=\", 0))\n\t\t}\n\n\t\tlimitedLogEntries, err := client.ACLLog(ctx, 2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(limitedLogEntries)).To(Equal(2))\n\n\t\t// cleanup after creating the user\n\t\terr = client.Do(ctx, \"acl\", \"deluser\", \"test\").Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should ACL LOG RESET\", Label(\"NonRedisEnterprise\"), func() {\n\t\t// Call ACL LOG RESET\n\t\tresetCmd := client.ACLLogReset(ctx)\n\t\tExpect(resetCmd.Err()).NotTo(HaveOccurred())\n\t\tExpect(resetCmd.Val()).To(Equal(\"OK\"))\n\n\t\t// Verify that the log is empty after the reset\n\t\tlogEntries, err := client.ACLLog(ctx, 10).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(logEntries)).To(Equal(0))\n\t})\n\n})\nvar _ = Describe(\"ACL user commands\", Label(\"NonRedisEnterprise\"), func() {\n\tvar client *redis.Client\n\tvar ctx context.Context\n\n\tBeforeEach(func() {\n\t\tctx = context.Background()\n\t\topt := redisOptions()\n\t\tclient = redis.NewClient(opt)\n\t})\n\n\tAfterEach(func() {\n\t\t_, err := client.ACLDelUser(context.Background(), TestUserName).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"list only default user\", func() {\n\t\tres, err := client.ACLList(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(HaveLen(1))\n\t\tExpect(res[0]).To(ContainSubstring(\"default\"))\n\n\t\tres, err = client.ACLUsers(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(HaveLen(1))\n\t\tExpect(res[0]).To(Equal(\"default\"))\n\n\t\tres1, err := client.ACLWhoAmI(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1).To(Equal(\"default\"))\n\t})\n\n\tIt(\"gen password\", func() {\n\t\tpassword, err := client.ACLGenPass(ctx, 0).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(password).NotTo(BeEmpty())\n\t})\n\n\tIt(\"gen password with length\", func() {\n\t\tbit := 128\n\t\tpassword, err := client.ACLGenPass(ctx, bit).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(password).NotTo(BeEmpty())\n\t\tExpect(len(password)).To(Equal(bit / 4))\n\t})\n\n\tIt(\"setuser and deluser\", func() {\n\t\tres, err := client.ACLList(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(HaveLen(1))\n\t\tExpect(res[0]).To(ContainSubstring(\"default\"))\n\n\t\tadd, err := client.ACLSetUser(ctx, TestUserName, \"nopass\", \"on\", \"allkeys\", \"+set\", \"+get\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(add).To(Equal(\"OK\"))\n\n\t\tresAfter, err := client.ACLList(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resAfter).To(HaveLen(2))\n\t\tExpect(resAfter[1]).To(ContainSubstring(TestUserName))\n\n\t\tdeletedN, err := client.ACLDelUser(ctx, TestUserName).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(deletedN).To(BeNumerically(\"==\", 1))\n\n\t\tresAfterDeletion, err := client.ACLList(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resAfterDeletion).To(HaveLen(1))\n\t\tExpect(resAfterDeletion[0]).To(BeEquivalentTo(res[0]))\n\t})\n\n\tIt(\"should acl dryrun\", func() {\n\t\tdryRun := client.ACLDryRun(ctx, \"default\", \"get\", \"randomKey\")\n\t\tExpect(dryRun.Err()).NotTo(HaveOccurred())\n\t\tExpect(dryRun.Val()).To(Equal(\"OK\"))\n\t})\n})\n\nvar _ = Describe(\"ACL permissions\", Label(\"NonRedisEnterprise\"), func() {\n\tvar client *redis.Client\n\tvar ctx context.Context\n\n\tBeforeEach(func() {\n\t\tctx = context.Background()\n\t\topt := redisOptions()\n\t\topt.UnstableResp3 = true\n\t\tclient = redis.NewClient(opt)\n\t})\n\n\tAfterEach(func() {\n\t\t_, err := client.ACLDelUser(context.Background(), TestUserName).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"reset permissions\", func() {\n\t\tadd, err := client.ACLSetUser(ctx,\n\t\t\tTestUserName,\n\t\t\t\"reset\",\n\t\t\t\"nopass\",\n\t\t\t\"on\",\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(add).To(Equal(\"OK\"))\n\n\t\tconnection := client.Conn()\n\t\tauthed, err := connection.AuthACL(ctx, TestUserName, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(authed).To(Equal(\"OK\"))\n\n\t\t_, err = connection.Get(ctx, \"anykey\").Result()\n\t\tExpect(err).To(HaveOccurred())\n\t})\n\n\tIt(\"add write permissions\", func() {\n\t\tadd, err := client.ACLSetUser(ctx,\n\t\t\tTestUserName,\n\t\t\t\"reset\",\n\t\t\t\"nopass\",\n\t\t\t\"on\",\n\t\t\t\"~*\",\n\t\t\t\"+SET\",\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(add).To(Equal(\"OK\"))\n\n\t\tconnection := client.Conn()\n\t\tauthed, err := connection.AuthACL(ctx, TestUserName, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(authed).To(Equal(\"OK\"))\n\n\t\t// can write\n\t\tv, err := connection.Set(ctx, \"anykey\", \"anyvalue\", 0).Result()\n\t\tExpect(err).ToNot(HaveOccurred())\n\t\tExpect(v).To(Equal(\"OK\"))\n\n\t\t// but can't read\n\t\tvalue, err := connection.Get(ctx, \"anykey\").Result()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(value).To(BeEmpty())\n\t})\n\n\tIt(\"add read permissions\", func() {\n\t\tadd, err := client.ACLSetUser(ctx,\n\t\t\tTestUserName,\n\t\t\t\"reset\",\n\t\t\t\"nopass\",\n\t\t\t\"on\",\n\t\t\t\"~*\",\n\t\t\t\"+GET\",\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(add).To(Equal(\"OK\"))\n\n\t\tconnection := client.Conn()\n\t\tauthed, err := connection.AuthACL(ctx, TestUserName, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(authed).To(Equal(\"OK\"))\n\n\t\t// can read\n\t\tvalue, err := connection.Get(ctx, \"anykey\").Result()\n\t\tExpect(err).ToNot(HaveOccurred())\n\t\tExpect(value).To(Equal(\"anyvalue\"))\n\n\t\t// but can't delete\n\t\tdel, err := connection.Del(ctx, \"anykey\").Result()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(del).ToNot(Equal(1))\n\t})\n\n\tIt(\"add del permissions\", func() {\n\t\tadd, err := client.ACLSetUser(ctx,\n\t\t\tTestUserName,\n\t\t\t\"reset\",\n\t\t\t\"nopass\",\n\t\t\t\"on\",\n\t\t\t\"~*\",\n\t\t\t\"+DEL\",\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(add).To(Equal(\"OK\"))\n\n\t\tconnection := client.Conn()\n\t\tauthed, err := connection.AuthACL(ctx, TestUserName, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(authed).To(Equal(\"OK\"))\n\n\t\t// can read\n\t\tdel, err := connection.Del(ctx, \"anykey\").Result()\n\t\tExpect(err).ToNot(HaveOccurred())\n\t\tExpect(del).To(BeEquivalentTo(1))\n\t})\n\n\tIt(\"set permissions for module commands\", func() {\n\t\tSkipBeforeRedisVersion(8, \"permissions for modules are supported for Redis Version >=8\")\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo baz\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"foo bar\")\n\t\tadd, err := client.ACLSetUser(ctx,\n\t\t\tTestUserName,\n\t\t\t\"reset\",\n\t\t\t\"nopass\",\n\t\t\t\"on\",\n\t\t\t\"~*\",\n\t\t\t\"+FT.SEARCH\",\n\t\t\t\"-FT.DROPINDEX\",\n\t\t\t\"+json.set\",\n\t\t\t\"+json.get\",\n\t\t\t\"-json.clear\",\n\t\t\t\"+bf.reserve\",\n\t\t\t\"-bf.info\",\n\t\t\t\"+cf.reserve\",\n\t\t\t\"+cms.initbydim\",\n\t\t\t\"+topk.reserve\",\n\t\t\t\"+tdigest.create\",\n\t\t\t\"+ts.create\",\n\t\t\t\"-ts.info\",\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(add).To(Equal(\"OK\"))\n\n\t\tc := client.Conn()\n\t\tauthed, err := c.AuthACL(ctx, TestUserName, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(authed).To(Equal(\"OK\"))\n\n\t\t// has perm for search\n\t\tExpect(c.FTSearch(ctx, \"txt\", \"foo ~bar\").Err()).NotTo(HaveOccurred())\n\n\t\t// no perm for dropindex\n\t\terr = c.FTDropIndex(ctx, \"txt\").Err()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"NOPERM\"))\n\n\t\t// json set and get have perm\n\t\tExpect(c.JSONSet(ctx, \"foo\", \"$\", \"\\\"bar\\\"\").Err()).NotTo(HaveOccurred())\n\t\tExpect(c.JSONGet(ctx, \"foo\", \"$\").Val()).To(BeEquivalentTo(\"[\\\"bar\\\"]\"))\n\n\t\t// no perm for json clear\n\t\terr = c.JSONClear(ctx, \"foo\", \"$\").Err()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"NOPERM\"))\n\n\t\t// perm for reserve\n\t\tExpect(c.BFReserve(ctx, \"bloom\", 0.01, 100).Err()).NotTo(HaveOccurred())\n\n\t\t// no perm for info\n\t\terr = c.BFInfo(ctx, \"bloom\").Err()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"NOPERM\"))\n\n\t\t// perm for cf.reserve\n\t\tExpect(c.CFReserve(ctx, \"cfres\", 100).Err()).NotTo(HaveOccurred())\n\t\t// perm for cms.initbydim\n\t\tExpect(c.CMSInitByDim(ctx, \"cmsdim\", 100, 5).Err()).NotTo(HaveOccurred())\n\t\t// perm for topk.reserve\n\t\tExpect(c.TopKReserve(ctx, \"topk\", 10).Err()).NotTo(HaveOccurred())\n\t\t// perm for tdigest.create\n\t\tExpect(c.TDigestCreate(ctx, \"tdc\").Err()).NotTo(HaveOccurred())\n\t\t// perm for ts.create\n\t\tExpect(c.TSCreate(ctx, \"tsts\").Err()).NotTo(HaveOccurred())\n\t\t// noperm for ts.info\n\t\terr = c.TSInfo(ctx, \"tsts\").Err()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"NOPERM\"))\n\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"set permissions for module categories\", func() {\n\t\tSkipBeforeRedisVersion(8, \"permissions for modules are supported for Redis Version >=8\")\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo baz\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"foo bar\")\n\t\tadd, err := client.ACLSetUser(ctx,\n\t\t\tTestUserName,\n\t\t\t\"reset\",\n\t\t\t\"nopass\",\n\t\t\t\"on\",\n\t\t\t\"~*\",\n\t\t\t\"+@search\",\n\t\t\t\"+@json\",\n\t\t\t\"+@bloom\",\n\t\t\t\"+@cuckoo\",\n\t\t\t\"+@topk\",\n\t\t\t\"+@cms\",\n\t\t\t\"+@timeseries\",\n\t\t\t\"+@tdigest\",\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(add).To(Equal(\"OK\"))\n\n\t\tc := client.Conn()\n\t\tauthed, err := c.AuthACL(ctx, TestUserName, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(authed).To(Equal(\"OK\"))\n\n\t\t// has perm for search\n\t\tExpect(c.FTSearch(ctx, \"txt\", \"foo ~bar\").Err()).NotTo(HaveOccurred())\n\t\t// perm for dropindex\n\t\tExpect(c.FTDropIndex(ctx, \"txt\").Err()).NotTo(HaveOccurred())\n\t\t// json set and get have perm\n\t\tExpect(c.JSONSet(ctx, \"foo\", \"$\", \"\\\"bar\\\"\").Err()).NotTo(HaveOccurred())\n\t\tExpect(c.JSONGet(ctx, \"foo\", \"$\").Val()).To(BeEquivalentTo(\"[\\\"bar\\\"]\"))\n\t\t// perm for json clear\n\t\tExpect(c.JSONClear(ctx, \"foo\", \"$\").Err()).NotTo(HaveOccurred())\n\t\t// perm for reserve\n\t\tExpect(c.BFReserve(ctx, \"bloom\", 0.01, 100).Err()).NotTo(HaveOccurred())\n\t\t// perm for info\n\t\tExpect(c.BFInfo(ctx, \"bloom\").Err()).NotTo(HaveOccurred())\n\t\t// perm for cf.reserve\n\t\tExpect(c.CFReserve(ctx, \"cfres\", 100).Err()).NotTo(HaveOccurred())\n\t\t// perm for cms.initbydim\n\t\tExpect(c.CMSInitByDim(ctx, \"cmsdim\", 100, 5).Err()).NotTo(HaveOccurred())\n\t\t// perm for topk.reserve\n\t\tExpect(c.TopKReserve(ctx, \"topk\", 10).Err()).NotTo(HaveOccurred())\n\t\t// perm for tdigest.create\n\t\tExpect(c.TDigestCreate(ctx, \"tdc\").Err()).NotTo(HaveOccurred())\n\t\t// perm for ts.create\n\t\tExpect(c.TSCreate(ctx, \"tsts\").Err()).NotTo(HaveOccurred())\n\t\t// perm for ts.info\n\t\tExpect(c.TSInfo(ctx, \"tsts\").Err()).NotTo(HaveOccurred())\n\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n})\n\nvar _ = Describe(\"ACL Categories\", func() {\n\tvar client *redis.Client\n\tvar ctx context.Context\n\n\tBeforeEach(func() {\n\t\tctx = context.Background()\n\t\topt := redisOptions()\n\t\tclient = redis.NewClient(opt)\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"lists acl categories and subcategories\", func() {\n\t\tres, err := client.ACLCat(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(res)).To(BeNumerically(\">\", 20))\n\t\tExpect(res).To(ContainElements(\n\t\t\t\"read\",\n\t\t\t\"write\",\n\t\t\t\"keyspace\",\n\t\t\t\"dangerous\",\n\t\t\t\"slow\",\n\t\t\t\"set\",\n\t\t\t\"sortedset\",\n\t\t\t\"list\",\n\t\t\t\"hash\",\n\t\t))\n\n\t\tres, err = client.ACLCatArgs(ctx, &redis.ACLCatArgs{Category: \"read\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(ContainElement(\"get\"))\n\t})\n\n\tIt(\"lists acl categories and subcategories with Modules\", func() {\n\t\tSkipBeforeRedisVersion(8, \"modules are included in acl for redis version >= 8\")\n\t\taclTestCase := map[string]string{\n\t\t\t\"search\":     \"FT.CREATE\",\n\t\t\t\"bloom\":      \"bf.add\",\n\t\t\t\"json\":       \"json.get\",\n\t\t\t\"cuckoo\":     \"cf.insert\",\n\t\t\t\"cms\":        \"cms.query\",\n\t\t\t\"topk\":       \"topk.list\",\n\t\t\t\"tdigest\":    \"tdigest.rank\",\n\t\t\t\"timeseries\": \"ts.range\",\n\t\t}\n\t\tvar cats []interface{}\n\n\t\tfor cat, subitem := range aclTestCase {\n\t\t\tcats = append(cats, cat)\n\n\t\t\tres, err := client.ACLCatArgs(ctx, &redis.ACLCatArgs{\n\t\t\t\tCategory: cat,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(ContainElement(subitem))\n\t\t}\n\n\t\tres, err := client.ACLCat(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(ContainElements(cats...))\n\t})\n})\n"
  },
  {
    "path": "adapters.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/interfaces\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// ErrInvalidCommand is returned when an invalid command is passed to ExecuteCommand.\nvar ErrInvalidCommand = errors.New(\"invalid command type\")\n\n// ErrInvalidPool is returned when the pool type is not supported.\nvar ErrInvalidPool = errors.New(\"invalid pool type\")\n\n// newClientAdapter creates a new client adapter for regular Redis clients.\nfunc newClientAdapter(client *baseClient) interfaces.ClientInterface {\n\treturn &clientAdapter{client: client}\n}\n\n// clientAdapter adapts a Redis client to implement interfaces.ClientInterface.\ntype clientAdapter struct {\n\tclient *baseClient\n}\n\n// GetOptions returns the client options.\nfunc (ca *clientAdapter) GetOptions() interfaces.OptionsInterface {\n\treturn &optionsAdapter{options: ca.client.opt}\n}\n\n// GetPushProcessor returns the client's push notification processor.\nfunc (ca *clientAdapter) GetPushProcessor() interfaces.NotificationProcessor {\n\treturn &pushProcessorAdapter{processor: ca.client.pushProcessor}\n}\n\n// optionsAdapter adapts Redis options to implement interfaces.OptionsInterface.\ntype optionsAdapter struct {\n\toptions *Options\n}\n\n// GetReadTimeout returns the read timeout.\nfunc (oa *optionsAdapter) GetReadTimeout() time.Duration {\n\treturn oa.options.ReadTimeout\n}\n\n// GetWriteTimeout returns the write timeout.\nfunc (oa *optionsAdapter) GetWriteTimeout() time.Duration {\n\treturn oa.options.WriteTimeout\n}\n\n// GetNetwork returns the network type.\nfunc (oa *optionsAdapter) GetNetwork() string {\n\treturn oa.options.Network\n}\n\n// GetAddr returns the connection address.\nfunc (oa *optionsAdapter) GetAddr() string {\n\treturn oa.options.Addr\n}\n\n// GetNodeAddress returns the address of the Redis node as reported by the server.\n// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation.\n// For standalone clients, this defaults to Addr.\nfunc (oa *optionsAdapter) GetNodeAddress() string {\n\treturn oa.options.NodeAddress\n}\n\n// IsTLSEnabled returns true if TLS is enabled.\nfunc (oa *optionsAdapter) IsTLSEnabled() bool {\n\treturn oa.options.TLSConfig != nil\n}\n\n// GetProtocol returns the protocol version.\nfunc (oa *optionsAdapter) GetProtocol() int {\n\treturn oa.options.Protocol\n}\n\n// GetPoolSize returns the connection pool size.\nfunc (oa *optionsAdapter) GetPoolSize() int {\n\treturn oa.options.PoolSize\n}\n\n// NewDialer returns a new dialer function for the connection.\nfunc (oa *optionsAdapter) NewDialer() func(context.Context) (net.Conn, error) {\n\tbaseDialer := oa.options.NewDialer()\n\treturn func(ctx context.Context) (net.Conn, error) {\n\t\t// Extract network and address from the options\n\t\tnetwork := oa.options.Network\n\t\taddr := oa.options.Addr\n\t\treturn baseDialer(ctx, network, addr)\n\t}\n}\n\n// pushProcessorAdapter adapts a push.NotificationProcessor to implement interfaces.NotificationProcessor.\ntype pushProcessorAdapter struct {\n\tprocessor push.NotificationProcessor\n}\n\n// RegisterHandler registers a handler for a specific push notification name.\nfunc (ppa *pushProcessorAdapter) RegisterHandler(pushNotificationName string, handler interface{}, protected bool) error {\n\tif pushHandler, ok := handler.(push.NotificationHandler); ok {\n\t\treturn ppa.processor.RegisterHandler(pushNotificationName, pushHandler, protected)\n\t}\n\treturn errors.New(\"handler must implement push.NotificationHandler\")\n}\n\n// UnregisterHandler removes a handler for a specific push notification name.\nfunc (ppa *pushProcessorAdapter) UnregisterHandler(pushNotificationName string) error {\n\treturn ppa.processor.UnregisterHandler(pushNotificationName)\n}\n\n// GetHandler returns the handler for a specific push notification name.\nfunc (ppa *pushProcessorAdapter) GetHandler(pushNotificationName string) interface{} {\n\treturn ppa.processor.GetHandler(pushNotificationName)\n}\n"
  },
  {
    "path": "async_handoff_integration_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// mockNetConn implements net.Conn for testing\ntype mockNetConn struct {\n\taddr string\n}\n\nfunc (m *mockNetConn) Read(b []byte) (n int, err error)   { return 0, nil }\nfunc (m *mockNetConn) Write(b []byte) (n int, err error)  { return len(b), nil }\nfunc (m *mockNetConn) Close() error                       { return nil }\nfunc (m *mockNetConn) LocalAddr() net.Addr                { return &mockAddr{m.addr} }\nfunc (m *mockNetConn) RemoteAddr() net.Addr               { return &mockAddr{m.addr} }\nfunc (m *mockNetConn) SetDeadline(t time.Time) error      { return nil }\nfunc (m *mockNetConn) SetReadDeadline(t time.Time) error  { return nil }\nfunc (m *mockNetConn) SetWriteDeadline(t time.Time) error { return nil }\n\ntype mockAddr struct {\n\taddr string\n}\n\nfunc (m *mockAddr) Network() string { return \"tcp\" }\nfunc (m *mockAddr) String() string  { return m.addr }\n\n// TestEventDrivenHandoffIntegration tests the complete event-driven handoff flow\nfunc TestEventDrivenHandoffIntegration(t *testing.T) {\n\tt.Run(\"EventDrivenHandoffWithPoolSkipping\", func(t *testing.T) {\n\t\t// Create a base dialer for testing\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\t// Create processor with event-driven handoff support\n\t\tprocessor := maintnotifications.NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Reset circuit breakers to ensure clean state for this test\n\t\tprocessor.ResetCircuitBreakers()\n\n\t\t// Create a test pool with hooks\n\t\thookManager := pool.NewPoolHookManager()\n\t\thookManager.AddHook(processor)\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\treturn &mockNetConn{addr: \"original:6379\"}, nil\n\t\t\t},\n\t\t\tPoolSize:           int32(5),\n\t\t\tMaxConcurrentDials: 5,\n\t\t\tPoolTimeout:        time.Second,\n\t\t})\n\n\t\t// Add the hook to the pool after creation\n\t\ttestPool.AddPoolHook(processor)\n\t\tdefer testPool.Close()\n\n\t\t// Set the pool reference in the processor for connection removal on handoff failure\n\t\tprocessor.SetPool(testPool)\n\n\t\tctx := context.Background()\n\n\t\t// Get a connection and mark it for handoff\n\t\tconn, err := testPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to get connection: %v\", err)\n\t\t}\n\n\t\t// Set initialization function with a small delay to ensure handoff is pending\n\t\tvar initConnCalled atomic.Bool\n\t\tinitConnStarted := make(chan struct{})\n\t\tinitConnFunc := func(ctx context.Context, cn *pool.Conn) error {\n\t\t\tclose(initConnStarted)            // Signal that InitConn has started\n\t\t\ttime.Sleep(50 * time.Millisecond) // Add delay to keep handoff pending\n\t\t\tinitConnCalled.Store(true)\n\t\t\treturn nil\n\t\t}\n\t\tconn.SetInitConnFunc(initConnFunc)\n\n\t\t// Mark connection for handoff\n\t\terr = conn.MarkForHandoff(\"new-endpoint:6379\", 12345)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\tt.Logf(\"Connection state before Put: %v, ShouldHandoff: %v\", conn.GetStateMachine().GetState(), conn.ShouldHandoff())\n\n\t\t// Return connection to pool - this should queue handoff\n\t\ttestPool.Put(ctx, conn)\n\n\t\tt.Logf(\"Connection state after Put: %v, ShouldHandoff: %v, IsHandoffPending: %v\",\n\t\t\tconn.GetStateMachine().GetState(), conn.ShouldHandoff(), processor.IsHandoffPending(conn))\n\n\t\t// Give the worker goroutine time to start and begin processing\n\t\t// We wait for InitConn to actually start (which signals via channel)\n\t\t// This ensures the handoff is actively being processed\n\t\tselect {\n\t\tcase <-initConnStarted:\n\t\t\t// Good - handoff started processing, InitConn is now running\n\t\tcase <-time.After(500 * time.Millisecond):\n\t\t\t// Handoff didn't start - this could be due to:\n\t\t\t// 1. Worker didn't start yet (on-demand worker creation is async)\n\t\t\t// 2. Circuit breaker is open\n\t\t\t// 3. Connection was not queued\n\t\t\t// For now, we'll skip the pending map check and just verify behavioral correctness below\n\t\t\tt.Logf(\"Warning: Handoff did not start processing within 500ms, skipping pending map check\")\n\t\t}\n\n\t\t// Only check pending map if handoff actually started\n\t\tselect {\n\t\tcase <-initConnStarted:\n\t\t\t// Handoff started - verify it's still pending (InitConn is sleeping)\n\t\t\tif !processor.IsHandoffPending(conn) {\n\t\t\t\tt.Error(\"Handoff should be in pending map while InitConn is running\")\n\t\t\t}\n\t\tdefault:\n\t\t\t// Handoff didn't start yet - skip this check\n\t\t}\n\n\t\t// Try to get the same connection - should be skipped due to pending handoff\n\t\tconn2, err := testPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to get second connection: %v\", err)\n\t\t}\n\n\t\t// Should get a different connection (the pending one should be skipped)\n\t\tif conn == conn2 {\n\t\t\tt.Error(\"Should have gotten a different connection while handoff is pending\")\n\t\t}\n\n\t\t// Return the second connection\n\t\ttestPool.Put(ctx, conn2)\n\n\t\t// Wait for handoff to complete\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t// Only verify handoff completion if it actually started\n\t\tselect {\n\t\tcase <-initConnStarted:\n\t\t\t// Handoff started - verify it completed\n\t\t\tif processor.IsHandoffPending(conn) {\n\t\t\t\tt.Error(\"Handoff should have completed and been removed from pending map\")\n\t\t\t}\n\n\t\t\tif !initConnCalled.Load() {\n\t\t\t\tt.Error(\"InitConn should have been called during handoff\")\n\t\t\t}\n\t\tdefault:\n\t\t\t// Handoff never started - this is a known timing issue with on-demand workers\n\t\t\t// The test still validates the important behavior: connections are skipped when marked for handoff\n\t\t\tt.Logf(\"Handoff did not start within timeout - skipping completion checks\")\n\t\t}\n\n\t\t// Now the original connection should be available again\n\t\tconn3, err := testPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to get third connection: %v\", err)\n\t\t}\n\n\t\t// Could be the original connection (now handed off) or a new one\n\t\ttestPool.Put(ctx, conn3)\n\t})\n\n\tt.Run(\"ConcurrentHandoffs\", func(t *testing.T) {\n\t\t// Create a base dialer that simulates slow handoffs\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\ttime.Sleep(50 * time.Millisecond) // Simulate network delay\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\tprocessor := maintnotifications.NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Create hooks manager and add processor as hook\n\t\thookManager := pool.NewPoolHookManager()\n\t\thookManager.AddHook(processor)\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\treturn &mockNetConn{addr: \"original:6379\"}, nil\n\t\t\t},\n\n\t\t\tPoolSize:           int32(10),\n\t\t\tMaxConcurrentDials: 10,\n\t\t\tPoolTimeout:        time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Add the hook to the pool after creation\n\t\ttestPool.AddPoolHook(processor)\n\n\t\t// Set the pool reference in the processor\n\t\tprocessor.SetPool(testPool)\n\n\t\tctx := context.Background()\n\t\tvar wg sync.WaitGroup\n\n\t\t// Start multiple concurrent handoffs\n\t\tfor i := 0; i < 5; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func(id int) {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\t// Get connection\n\t\t\t\tconn, err := testPool.Get(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Failed to get conn[%d]: %v\", id, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// Set initialization function\n\t\t\t\tinitConnFunc := func(ctx context.Context, cn *pool.Conn) error {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tconn.SetInitConnFunc(initConnFunc)\n\n\t\t\t\t// Mark for handoff\n\t\t\t\tconn.MarkForHandoff(\"new-endpoint:6379\", int64(id))\n\n\t\t\t\t// Return to pool (starts async handoff)\n\t\t\t\ttestPool.Put(ctx, conn)\n\t\t\t}(i)\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Wait for all handoffs to complete\n\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t// Verify pool is still functional\n\t\tconn, err := testPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Pool should still be functional after concurrent handoffs: %v\", err)\n\t\t}\n\t\ttestPool.Put(ctx, conn)\n\t})\n\n\tt.Run(\"HandoffFailureRecovery\", func(t *testing.T) {\n\t\t// Create a failing base dialer\n\t\tfailingDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn nil, &net.OpError{Op: \"dial\", Err: &net.DNSError{Name: addr}}\n\t\t}\n\n\t\tprocessor := maintnotifications.NewPoolHook(failingDialer, \"tcp\", nil, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Create hooks manager and add processor as hook\n\t\thookManager := pool.NewPoolHookManager()\n\t\thookManager.AddHook(processor)\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\treturn &mockNetConn{addr: \"original:6379\"}, nil\n\t\t\t},\n\n\t\t\tPoolSize:           int32(3),\n\t\t\tMaxConcurrentDials: 3,\n\t\t\tPoolTimeout:        time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Add the hook to the pool after creation\n\t\ttestPool.AddPoolHook(processor)\n\n\t\t// Set the pool reference in the processor\n\t\tprocessor.SetPool(testPool)\n\n\t\tctx := context.Background()\n\n\t\t// Get connection and mark for handoff\n\t\tconn, err := testPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to get connection: %v\", err)\n\t\t}\n\n\t\tconn.MarkForHandoff(\"unreachable-endpoint:6379\", 12345)\n\n\t\t// Return to pool (starts async handoff that will fail)\n\t\ttestPool.Put(ctx, conn)\n\n\t\t// Wait for handoff to start processing\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t// Connection should still be in pending map (waiting for retry after dial failure)\n\t\tif !processor.IsHandoffPending(conn) {\n\t\t\tt.Error(\"Connection should still be in pending map while waiting for retry\")\n\t\t}\n\n\t\t// Wait for retry delay to pass and handoff to be re-queued\n\t\ttime.Sleep(600 * time.Millisecond)\n\n\t\t// Connection should still be pending (retry was queued)\n\t\tif !processor.IsHandoffPending(conn) {\n\t\t\tt.Error(\"Connection should still be in pending map after retry was queued\")\n\t\t}\n\n\t\t// Pool should still be functional\n\t\tconn2, err := testPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Pool should still be functional: %v\", err)\n\t\t}\n\n\t\t// In event-driven approach, the original connection remains in pool\n\t\t// even after failed handoff (it's still a valid connection)\n\t\t// We might get the same connection or a different one\n\t\ttestPool.Put(ctx, conn2)\n\t})\n\n\tt.Run(\"GracefulShutdown\", func(t *testing.T) {\n\t\t// Create a slow base dialer\n\t\tslowDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\tprocessor := maintnotifications.NewPoolHook(slowDialer, \"tcp\", nil, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Create hooks manager and add processor as hook\n\t\thookManager := pool.NewPoolHookManager()\n\t\thookManager.AddHook(processor)\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\treturn &mockNetConn{addr: \"original:6379\"}, nil\n\t\t\t},\n\n\t\t\tPoolSize:           int32(2),\n\t\t\tMaxConcurrentDials: 2,\n\t\t\tPoolTimeout:        time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Add the hook to the pool after creation\n\t\ttestPool.AddPoolHook(processor)\n\n\t\t// Set the pool reference in the processor\n\t\tprocessor.SetPool(testPool)\n\n\t\tctx := context.Background()\n\n\t\t// Start a handoff\n\t\tconn, err := testPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to get connection: %v\", err)\n\t\t}\n\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 12345); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\t// Set a mock initialization function with delay to ensure handoff is pending\n\t\tconn.SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\ttime.Sleep(50 * time.Millisecond) // Add delay to keep handoff pending\n\t\t\treturn nil\n\t\t})\n\n\t\ttestPool.Put(ctx, conn)\n\n\t\t// Give the on-demand worker a moment to start and begin processing\n\t\t// The handoff should be pending because the slowDialer takes 100ms\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t// Verify handoff was queued and is being processed\n\t\tif !processor.IsHandoffPending(conn) {\n\t\t\tt.Error(\"Handoff should be queued in pending map\")\n\t\t}\n\n\t\t// Give the handoff a moment to start processing\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t// Shutdown processor gracefully\n\t\t// Use a longer timeout to account for slow dialer (100ms) plus processing overhead\n\t\tshutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\n\t\tdefer cancel()\n\n\t\terr = processor.Shutdown(shutdownCtx)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Graceful shutdown should succeed: %v\", err)\n\t\t}\n\n\t\t// Handoff should have completed (removed from pending map)\n\t\tif processor.IsHandoffPending(conn) {\n\t\t\tt.Error(\"Handoff should have completed and been removed from pending map after shutdown\")\n\t\t}\n\t})\n}\n\nfunc init() {\n\tlogging.Disable()\n}\n"
  },
  {
    "path": "auth/auth.go",
    "content": "// Package auth package provides authentication-related interfaces and types.\n// It also includes a basic implementation of credentials using username and password.\npackage auth\n\n// StreamingCredentialsProvider is an interface that defines the methods for a streaming credentials provider.\n// It is used to provide credentials for authentication.\n// The CredentialsListener is used to receive updates when the credentials change.\ntype StreamingCredentialsProvider interface {\n\t// Subscribe subscribes to the credentials provider for updates.\n\t// It returns the current credentials, a cancel function to unsubscribe from the provider,\n\t// and an error if any.\n\t// TODO(ndyakov): Should we add context to the Subscribe method?\n\tSubscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error)\n}\n\n// UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider.\n// It is used to unsubscribe from the provider when the credentials are no longer needed.\ntype UnsubscribeFunc func() error\n\n// CredentialsListener is an interface that defines the methods for a credentials listener.\n// It is used to receive updates when the credentials change.\n// The OnNext method is called when the credentials change.\n// The OnError method is called when an error occurs while requesting the credentials.\ntype CredentialsListener interface {\n\tOnNext(credentials Credentials)\n\tOnError(err error)\n}\n\n// Credentials is an interface that defines the methods for credentials.\n// It is used to provide the credentials for authentication.\ntype Credentials interface {\n\t// BasicAuth returns the username and password for basic authentication.\n\tBasicAuth() (username string, password string)\n\t// RawCredentials returns the raw credentials as a string.\n\t// This can be used to extract the username and password from the raw credentials or\n\t// additional information if present in the token.\n\tRawCredentials() string\n}\n\ntype basicAuth struct {\n\tusername string\n\tpassword string\n}\n\n// RawCredentials returns the raw credentials as a string.\nfunc (b *basicAuth) RawCredentials() string {\n\treturn b.username + \":\" + b.password\n}\n\n// BasicAuth returns the username and password for basic authentication.\nfunc (b *basicAuth) BasicAuth() (username string, password string) {\n\treturn b.username, b.password\n}\n\n// NewBasicCredentials creates a new Credentials object from the given username and password.\nfunc NewBasicCredentials(username, password string) Credentials {\n\treturn &basicAuth{\n\t\tusername: username,\n\t\tpassword: password,\n\t}\n}\n"
  },
  {
    "path": "auth/auth_test.go",
    "content": "package auth\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype mockStreamingProvider struct {\n\tcredentials Credentials\n\terr         error\n\tupdates     chan Credentials\n}\n\nfunc newMockStreamingProvider(initialCreds Credentials) *mockStreamingProvider {\n\treturn &mockStreamingProvider{\n\t\tcredentials: initialCreds,\n\t\tupdates:     make(chan Credentials, 10),\n\t}\n}\n\nfunc (m *mockStreamingProvider) Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) {\n\tif m.err != nil {\n\t\treturn nil, nil, m.err\n\t}\n\n\t// Send initial credentials\n\tlistener.OnNext(m.credentials)\n\n\t// Start goroutine to handle updates\n\tgo func() {\n\t\tfor creds := range m.updates {\n\t\t\tlistener.OnNext(creds)\n\t\t}\n\t}()\n\n\treturn m.credentials, func() error {\n\t\tclose(m.updates)\n\t\treturn nil\n\t}, nil\n}\n\nfunc TestStreamingCredentialsProvider(t *testing.T) {\n\tt.Run(\"successful subscription\", func(t *testing.T) {\n\t\tinitialCreds := NewBasicCredentials(\"user1\", \"pass1\")\n\t\tprovider := newMockStreamingProvider(initialCreds)\n\n\t\tvar receivedCreds []Credentials\n\t\tvar receivedErrors []error\n\t\tvar mu sync.Mutex\n\n\t\tlistener := NewReAuthCredentialsListener(\n\t\t\tfunc(creds Credentials) error {\n\t\t\t\tmu.Lock()\n\t\t\t\treceivedCreds = append(receivedCreds, creds)\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc(err error) {\n\t\t\t\treceivedErrors = append(receivedErrors, err)\n\t\t\t},\n\t\t)\n\n\t\tcreds, cancel, err := provider.Subscribe(listener)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif cancel == nil {\n\t\t\tt.Fatal(\"expected cancel function to be non-nil\")\n\t\t}\n\t\tif creds != initialCreds {\n\t\t\tt.Fatalf(\"expected credentials %v, got %v\", initialCreds, creds)\n\t\t}\n\t\tif len(receivedCreds) != 1 {\n\t\t\tt.Fatalf(\"expected 1 received credential, got %d\", len(receivedCreds))\n\t\t}\n\t\tif receivedCreds[0] != initialCreds {\n\t\t\tt.Fatalf(\"expected received credential %v, got %v\", initialCreds, receivedCreds[0])\n\t\t}\n\t\tif len(receivedErrors) != 0 {\n\t\t\tt.Fatalf(\"expected no errors, got %d\", len(receivedErrors))\n\t\t}\n\n\t\t// Send an update\n\t\tnewCreds := NewBasicCredentials(\"user2\", \"pass2\")\n\t\tprovider.updates <- newCreds\n\n\t\t// Wait for update to be processed\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tmu.Lock()\n\t\tif len(receivedCreds) != 2 {\n\t\t\tt.Fatalf(\"expected 2 received credentials, got %d\", len(receivedCreds))\n\t\t}\n\t\tif receivedCreds[1] != newCreds {\n\t\t\tt.Fatalf(\"expected received credential %v, got %v\", newCreds, receivedCreds[1])\n\t\t}\n\t\tmu.Unlock()\n\n\t\t// Cancel subscription\n\t\tif err := cancel(); err != nil {\n\t\t\tt.Fatalf(\"unexpected error cancelling subscription: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"subscription error\", func(t *testing.T) {\n\t\tprovider := &mockStreamingProvider{\n\t\t\terr: errors.New(\"subscription failed\"),\n\t\t}\n\n\t\tvar receivedCreds []Credentials\n\t\tvar receivedErrors []error\n\n\t\tlistener := NewReAuthCredentialsListener(\n\t\t\tfunc(creds Credentials) error {\n\t\t\t\treceivedCreds = append(receivedCreds, creds)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc(err error) {\n\t\t\t\treceivedErrors = append(receivedErrors, err)\n\t\t\t},\n\t\t)\n\n\t\tcreds, cancel, err := provider.Subscribe(listener)\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif cancel != nil {\n\t\t\tt.Fatal(\"expected cancel function to be nil\")\n\t\t}\n\t\tif creds != nil {\n\t\t\tt.Fatalf(\"expected nil credentials, got %v\", creds)\n\t\t}\n\t\tif len(receivedCreds) != 0 {\n\t\t\tt.Fatalf(\"expected no received credentials, got %d\", len(receivedCreds))\n\t\t}\n\t\tif len(receivedErrors) != 0 {\n\t\t\tt.Fatalf(\"expected no errors, got %d\", len(receivedErrors))\n\t\t}\n\t})\n\n\tt.Run(\"re-auth error\", func(t *testing.T) {\n\t\tinitialCreds := NewBasicCredentials(\"user1\", \"pass1\")\n\t\tprovider := newMockStreamingProvider(initialCreds)\n\n\t\treauthErr := errors.New(\"re-auth failed\")\n\t\tvar receivedErrors []error\n\n\t\tlistener := NewReAuthCredentialsListener(\n\t\t\tfunc(creds Credentials) error {\n\t\t\t\treturn reauthErr\n\t\t\t},\n\t\t\tfunc(err error) {\n\t\t\t\treceivedErrors = append(receivedErrors, err)\n\t\t\t},\n\t\t)\n\n\t\tcreds, cancel, err := provider.Subscribe(listener)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif cancel == nil {\n\t\t\tt.Fatal(\"expected cancel function to be non-nil\")\n\t\t}\n\t\tif creds != initialCreds {\n\t\t\tt.Fatalf(\"expected credentials %v, got %v\", initialCreds, creds)\n\t\t}\n\t\tif len(receivedErrors) != 1 {\n\t\t\tt.Fatalf(\"expected 1 error, got %d\", len(receivedErrors))\n\t\t}\n\t\tif receivedErrors[0] != reauthErr {\n\t\t\tt.Fatalf(\"expected error %v, got %v\", reauthErr, receivedErrors[0])\n\t\t}\n\n\t\tif err := cancel(); err != nil {\n\t\t\tt.Fatalf(\"unexpected error cancelling subscription: %v\", err)\n\t\t}\n\t})\n}\n\nfunc TestBasicCredentials(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tusername     string\n\t\tpassword     string\n\t\texpectedUser string\n\t\texpectedPass string\n\t\texpectedRaw  string\n\t}{\n\t\t{\n\t\t\tname:         \"basic auth\",\n\t\t\tusername:     \"user1\",\n\t\t\tpassword:     \"pass1\",\n\t\t\texpectedUser: \"user1\",\n\t\t\texpectedPass: \"pass1\",\n\t\t\texpectedRaw:  \"user1:pass1\",\n\t\t},\n\t\t{\n\t\t\tname:         \"empty username\",\n\t\t\tusername:     \"\",\n\t\t\tpassword:     \"pass1\",\n\t\t\texpectedUser: \"\",\n\t\t\texpectedPass: \"pass1\",\n\t\t\texpectedRaw:  \":pass1\",\n\t\t},\n\t\t{\n\t\t\tname:         \"empty password\",\n\t\t\tusername:     \"user1\",\n\t\t\tpassword:     \"\",\n\t\t\texpectedUser: \"user1\",\n\t\t\texpectedPass: \"\",\n\t\t\texpectedRaw:  \"user1:\",\n\t\t},\n\t\t{\n\t\t\tname:         \"both username and password empty\",\n\t\t\tusername:     \"\",\n\t\t\tpassword:     \"\",\n\t\t\texpectedUser: \"\",\n\t\t\texpectedPass: \"\",\n\t\t\texpectedRaw:  \":\",\n\t\t},\n\t\t{\n\t\t\tname:         \"special characters\",\n\t\t\tusername:     \"user:1\",\n\t\t\tpassword:     \"pa:ss@!#\",\n\t\t\texpectedUser: \"user:1\",\n\t\t\texpectedPass: \"pa:ss@!#\",\n\t\t\texpectedRaw:  \"user:1:pa:ss@!#\",\n\t\t},\n\t\t{\n\t\t\tname:         \"unicode characters\",\n\t\t\tusername:     \"ユーザー\",\n\t\t\tpassword:     \"密碼123\",\n\t\t\texpectedUser: \"ユーザー\",\n\t\t\texpectedPass: \"密碼123\",\n\t\t\texpectedRaw:  \"ユーザー:密碼123\",\n\t\t},\n\t\t{\n\t\t\tname:         \"long credentials\",\n\t\t\tusername:     strings.Repeat(\"u\", 1000),\n\t\t\tpassword:     strings.Repeat(\"p\", 1000),\n\t\t\texpectedUser: strings.Repeat(\"u\", 1000),\n\t\t\texpectedPass: strings.Repeat(\"p\", 1000),\n\t\t\texpectedRaw:  strings.Repeat(\"u\", 1000) + \":\" + strings.Repeat(\"p\", 1000),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcreds := NewBasicCredentials(tt.username, tt.password)\n\n\t\t\tuser, pass := creds.BasicAuth()\n\t\t\tif user != tt.expectedUser {\n\t\t\t\tt.Errorf(\"BasicAuth() username = %q; want %q\", user, tt.expectedUser)\n\t\t\t}\n\t\t\tif pass != tt.expectedPass {\n\t\t\t\tt.Errorf(\"BasicAuth() password = %q; want %q\", pass, tt.expectedPass)\n\t\t\t}\n\n\t\t\traw := creds.RawCredentials()\n\t\t\tif raw != tt.expectedRaw {\n\t\t\t\tt.Errorf(\"RawCredentials() = %q; want %q\", raw, tt.expectedRaw)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReAuthCredentialsListener(t *testing.T) {\n\tt.Run(\"successful re-auth\", func(t *testing.T) {\n\t\tvar reAuthCalled bool\n\t\tvar onErrCalled bool\n\t\tvar receivedCreds Credentials\n\n\t\tlistener := NewReAuthCredentialsListener(\n\t\t\tfunc(creds Credentials) error {\n\t\t\t\treAuthCalled = true\n\t\t\t\treceivedCreds = creds\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc(err error) {\n\t\t\t\tonErrCalled = true\n\t\t\t},\n\t\t)\n\n\t\tcreds := NewBasicCredentials(\"user1\", \"pass1\")\n\t\tlistener.OnNext(creds)\n\n\t\tif !reAuthCalled {\n\t\t\tt.Fatal(\"expected reAuth to be called\")\n\t\t}\n\t\tif onErrCalled {\n\t\t\tt.Fatal(\"expected onErr not to be called\")\n\t\t}\n\t\tif receivedCreds != creds {\n\t\t\tt.Fatalf(\"expected credentials %v, got %v\", creds, receivedCreds)\n\t\t}\n\t})\n\n\tt.Run(\"re-auth error\", func(t *testing.T) {\n\t\tvar reAuthCalled bool\n\t\tvar onErrCalled bool\n\t\tvar receivedErr error\n\t\texpectedErr := errors.New(\"re-auth failed\")\n\n\t\tlistener := NewReAuthCredentialsListener(\n\t\t\tfunc(creds Credentials) error {\n\t\t\t\treAuthCalled = true\n\t\t\t\treturn expectedErr\n\t\t\t},\n\t\t\tfunc(err error) {\n\t\t\t\tonErrCalled = true\n\t\t\t\treceivedErr = err\n\t\t\t},\n\t\t)\n\n\t\tcreds := NewBasicCredentials(\"user1\", \"pass1\")\n\t\tlistener.OnNext(creds)\n\n\t\tif !reAuthCalled {\n\t\t\tt.Fatal(\"expected reAuth to be called\")\n\t\t}\n\t\tif !onErrCalled {\n\t\t\tt.Fatal(\"expected onErr to be called\")\n\t\t}\n\t\tif receivedErr != expectedErr {\n\t\t\tt.Fatalf(\"expected error %v, got %v\", expectedErr, receivedErr)\n\t\t}\n\t})\n\n\tt.Run(\"on error\", func(t *testing.T) {\n\t\tvar onErrCalled bool\n\t\tvar receivedErr error\n\t\texpectedErr := errors.New(\"provider error\")\n\n\t\tlistener := NewReAuthCredentialsListener(\n\t\t\tfunc(creds Credentials) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc(err error) {\n\t\t\t\tonErrCalled = true\n\t\t\t\treceivedErr = err\n\t\t\t},\n\t\t)\n\n\t\tlistener.OnError(expectedErr)\n\n\t\tif !onErrCalled {\n\t\t\tt.Fatal(\"expected onErr to be called\")\n\t\t}\n\t\tif receivedErr != expectedErr {\n\t\t\tt.Fatalf(\"expected error %v, got %v\", expectedErr, receivedErr)\n\t\t}\n\t})\n\n\tt.Run(\"nil callbacks\", func(t *testing.T) {\n\t\tlistener := NewReAuthCredentialsListener(nil, nil)\n\n\t\t// Should not panic\n\t\tlistener.OnNext(NewBasicCredentials(\"user1\", \"pass1\"))\n\t\tlistener.OnError(errors.New(\"test error\"))\n\t})\n}\n"
  },
  {
    "path": "auth/reauth_credentials_listener.go",
    "content": "package auth\n\n// ReAuthCredentialsListener is a struct that implements the CredentialsListener interface.\n// It is used to re-authenticate the credentials when they are updated.\n// It contains:\n// - reAuth: a function that takes the new credentials and returns an error if any.\n// - onErr: a function that takes an error and handles it.\ntype ReAuthCredentialsListener struct {\n\treAuth func(credentials Credentials) error\n\tonErr  func(err error)\n}\n\n// OnNext is called when the credentials are updated.\n// It calls the reAuth function with the new credentials.\n// If the reAuth function returns an error, it calls the onErr function with the error.\nfunc (c *ReAuthCredentialsListener) OnNext(credentials Credentials) {\n\tif c.reAuth == nil {\n\t\treturn\n\t}\n\n\terr := c.reAuth(credentials)\n\tif err != nil {\n\t\tc.OnError(err)\n\t}\n}\n\n// OnError is called when an error occurs.\n// It can be called from both the credentials provider and the reAuth function.\nfunc (c *ReAuthCredentialsListener) OnError(err error) {\n\tif c.onErr == nil {\n\t\treturn\n\t}\n\n\tc.onErr(err)\n}\n\n// NewReAuthCredentialsListener creates a new ReAuthCredentialsListener.\n// Implements the auth.CredentialsListener interface.\nfunc NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, onErr func(err error)) *ReAuthCredentialsListener {\n\treturn &ReAuthCredentialsListener{\n\t\treAuth: reAuth,\n\t\tonErr:  onErr,\n\t}\n}\n\n// Ensure ReAuthCredentialsListener implements the CredentialsListener interface.\nvar _ CredentialsListener = (*ReAuthCredentialsListener)(nil)\n"
  },
  {
    "path": "bench_test.go",
    "content": "package redis_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc benchmarkRedisClient(ctx context.Context, poolSize int) *redis.Client {\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr:         \":6379\",\n\t\tDialTimeout:  time.Second,\n\t\tReadTimeout:  time.Second,\n\t\tWriteTimeout: time.Second,\n\t\tPoolSize:     poolSize,\n\t})\n\tif err := client.FlushDB(ctx).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\treturn client\n}\n\nfunc BenchmarkRedisPing(b *testing.B) {\n\tctx := context.Background()\n\trdb := benchmarkRedisClient(ctx, 10)\n\tdefer rdb.Close()\n\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc BenchmarkSetGoroutines(b *testing.B) {\n\tctx := context.Background()\n\trdb := benchmarkRedisClient(ctx, 10)\n\tdefer rdb.Close()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tvar wg sync.WaitGroup\n\n\t\tfor i := 0; i < 1000; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\terr := rdb.Set(ctx, \"hello\", \"world\", 0).Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\t}\n}\n\nfunc BenchmarkRedisGetNil(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkRedisClient(ctx, 10)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tif err := client.Get(ctx, \"key\").Err(); err != redis.Nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\ntype setStringBenchmark struct {\n\tpoolSize  int\n\tvalueSize int\n}\n\nfunc (bm setStringBenchmark) String() string {\n\treturn fmt.Sprintf(\"pool=%d value=%d\", bm.poolSize, bm.valueSize)\n}\n\nfunc BenchmarkRedisSetString(b *testing.B) {\n\tbenchmarks := []setStringBenchmark{\n\t\t{10, 64},\n\t\t{10, 1024},\n\t\t{10, 64 * 1024},\n\t\t{10, 1024 * 1024},\n\t\t{10, 10 * 1024 * 1024},\n\n\t\t{100, 64},\n\t\t{100, 1024},\n\t\t{100, 64 * 1024},\n\t\t{100, 1024 * 1024},\n\t\t{100, 10 * 1024 * 1024},\n\t}\n\tfor _, bm := range benchmarks {\n\t\tb.Run(bm.String(), func(b *testing.B) {\n\t\t\tctx := context.Background()\n\t\t\tclient := benchmarkRedisClient(ctx, bm.poolSize)\n\t\t\tdefer client.Close()\n\n\t\t\tvalue := strings.Repeat(\"1\", bm.valueSize)\n\n\t\t\tb.ResetTimer()\n\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\terr := client.Set(ctx, \"key\", value, 0).Err()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tb.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc BenchmarkRedisSetGetBytes(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkRedisClient(ctx, 10)\n\tdefer client.Close()\n\n\tvalue := bytes.Repeat([]byte{'1'}, 10000)\n\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tif err := client.Set(ctx, \"key\", value, 0).Err(); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\n\t\t\tgot, err := client.Get(ctx, \"key\").Bytes()\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t\tif !bytes.Equal(got, value) {\n\t\t\t\tb.Fatalf(\"got != value\")\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc BenchmarkRedisMGet(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkRedisClient(ctx, 10)\n\tdefer client.Close()\n\n\tif err := client.MSet(ctx, \"key1\", \"hello1\", \"key2\", \"hello2\").Err(); err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tif err := client.MGet(ctx, \"key1\", \"key2\").Err(); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc BenchmarkSetExpire(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkRedisClient(ctx, 10)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tif err := client.Set(ctx, \"key\", \"hello\", 0).Err(); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t\tif err := client.Expire(ctx, \"key\", time.Second).Err(); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc BenchmarkPipeline(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkRedisClient(ctx, 10)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Set(ctx, \"key\", \"hello\", 0)\n\t\t\t\tpipe.Expire(ctx, \"key\", time.Second)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc BenchmarkZAdd(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkRedisClient(ctx, 10)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\terr := client.ZAdd(ctx, \"key\", redis.Z{\n\t\t\t\tScore:  float64(1),\n\t\t\t\tMember: \"hello\",\n\t\t\t}).Err()\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc BenchmarkXRead(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkRedisClient(ctx, 10)\n\tdefer client.Close()\n\n\targs := redis.XAddArgs{\n\t\tStream: \"1\",\n\t\tID:     \"*\",\n\t\tValues: map[string]string{\"uno\": \"dos\"},\n\t}\n\n\tlenStreams := 16\n\tstreams := make([]string, 0, lenStreams)\n\tfor i := 0; i < lenStreams; i++ {\n\t\tstreams = append(streams, strconv.Itoa(i))\n\t}\n\tfor i := 0; i < lenStreams; i++ {\n\t\tstreams = append(streams, \"0\")\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tclient.XAdd(ctx, &args)\n\n\t\t\terr := client.XRead(ctx, &redis.XReadArgs{\n\t\t\t\tStreams: streams,\n\t\t\t\tCount:   1,\n\t\t\t\tBlock:   time.Second,\n\t\t\t}).Err()\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\n//------------------------------------------------------------------------------\n\nfunc newClusterScenario() *clusterScenario {\n\treturn &clusterScenario{\n\t\tports:   []string{\"16600\", \"16601\", \"16602\", \"16603\", \"16604\", \"16605\"},\n\t\tnodeIDs: make([]string, 6),\n\t\tclients: make(map[string]*redis.Client, 6),\n\t}\n}\n\nvar clusterBench *clusterScenario\n\nfunc BenchmarkClusterPing(b *testing.B) {\n\tif testing.Short() {\n\t\tb.Skip(\"skipping in short mode\")\n\t}\n\n\tctx := context.Background()\n\tif clusterBench == nil {\n\t\tclusterBench = newClusterScenario()\n\t\tif err := configureClusterTopology(ctx, clusterBench); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n\n\tclient := clusterBench.newClusterClient(ctx, redisClusterOptions())\n\tdefer client.Close()\n\n\tb.Run(\"cluster ping\", func(b *testing.B) {\n\t\tb.ResetTimer()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\terr := client.Ping(ctx).Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc BenchmarkClusterDoInt(b *testing.B) {\n\tif testing.Short() {\n\t\tb.Skip(\"skipping in short mode\")\n\t}\n\n\tctx := context.Background()\n\tif clusterBench == nil {\n\t\tclusterBench = newClusterScenario()\n\t\tif err := configureClusterTopology(ctx, clusterBench); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n\n\tclient := clusterBench.newClusterClient(ctx, redisClusterOptions())\n\tdefer client.Close()\n\n\tb.Run(\"cluster do set int\", func(b *testing.B) {\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\terr := client.Do(ctx, \"SET\", 10, 10).Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc BenchmarkClusterSetString(b *testing.B) {\n\tif testing.Short() {\n\t\tb.Skip(\"skipping in short mode\")\n\t}\n\n\tctx := context.Background()\n\tif clusterBench == nil {\n\t\tclusterBench = newClusterScenario()\n\t\tif err := configureClusterTopology(ctx, clusterBench); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n\n\tclient := clusterBench.newClusterClient(ctx, redisClusterOptions())\n\tdefer client.Close()\n\n\tvalue := string(bytes.Repeat([]byte{'1'}, 10000))\n\n\tb.Run(\"cluster set string\", func(b *testing.B) {\n\t\tb.ResetTimer()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\terr := client.Set(ctx, \"key\", value, 0).Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc BenchmarkExecRingSetAddrsCmd(b *testing.B) {\n\tconst (\n\t\tringShard1Name = \"ringShardOne\"\n\t\tringShard2Name = \"ringShardTwo\"\n\t)\n\n\tring := redis.NewRing(&redis.RingOptions{\n\t\tAddrs: map[string]string{\n\t\t\t\"ringShardOne\": \":\" + ringShard1Port,\n\t\t},\n\t\tNewClient: func(opt *redis.Options) *redis.Client {\n\t\t\t// Simulate slow shard creation\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\treturn redis.NewClient(opt)\n\t\t},\n\t})\n\tdefer ring.Close()\n\n\tif _, err := ring.Ping(context.Background()).Result(); err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\t// Continuously update addresses by adding and removing one address\n\tupdatesDone := make(chan struct{})\n\tdefer func() { close(updatesDone) }()\n\tgo func() {\n\t\tticker := time.NewTicker(10 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\t\tfor i := 0; ; i++ {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\tif i%2 == 0 {\n\t\t\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\t\t\tringShard1Name: \":\" + ringShard1Port,\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\t\t\tringShard1Name: \":\" + ringShard1Port,\n\t\t\t\t\t\tringShard2Name: \":\" + ringShard2Port,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\tcase <-updatesDone:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif _, err := ring.Ping(context.Background()).Result(); err != nil {\n\t\t\tif err == redis.ErrClosed {\n\t\t\t\t// The shard client could be closed while ping command is in progress\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "bitmap_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\ntype BitMapCmdable interface {\n\tGetBit(ctx context.Context, key string, offset int64) *IntCmd\n\tSetBit(ctx context.Context, key string, offset int64, value int) *IntCmd\n\tBitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd\n\tBitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd\n\tBitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd\n\tBitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd\n\tBitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd\n\tBitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd\n\tBitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd\n\tBitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd\n\tBitOpNot(ctx context.Context, destKey string, key string) *IntCmd\n\tBitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd\n\tBitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd\n\tBitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd\n\tBitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd\n}\n\nfunc (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"getbit\", key, offset)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd {\n\tcmd := NewIntCmd(\n\t\tctx,\n\t\t\"setbit\",\n\t\tkey,\n\t\toffset,\n\t\tvalue,\n\t)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype BitCount struct {\n\tStart, End int64\n\tUnit       string // BYTE(default) | BIT\n}\n\nconst BitCountIndexByte string = \"BYTE\"\nconst BitCountIndexBit string = \"BIT\"\n\nfunc (c cmdable) BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd {\n\targs := make([]any, 2, 5)\n\targs[0] = \"bitcount\"\n\targs[1] = key\n\tif bitCount != nil {\n\t\targs = append(args, bitCount.Start, bitCount.End)\n\t\tif bitCount.Unit != \"\" {\n\t\t\tif bitCount.Unit != BitCountIndexByte && bitCount.Unit != BitCountIndexBit {\n\t\t\t\tcmd := NewIntCmd(ctx)\n\t\t\t\tcmd.SetErr(errors.New(\"redis: invalid bitcount index\"))\n\t\t\t\treturn cmd\n\t\t\t}\n\t\t\targs = append(args, bitCount.Unit)\n\t\t}\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) *IntCmd {\n\targs := make([]interface{}, 3+len(keys))\n\targs[0] = \"bitop\"\n\targs[1] = op\n\targs[2] = destKey\n\tfor i, key := range keys {\n\t\targs[3+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BitOpAnd creates a new bitmap in which users are members of all given bitmaps\nfunc (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd {\n\treturn c.bitOp(ctx, \"and\", destKey, keys...)\n}\n\n// BitOpOr creates a new bitmap in which users are member of at least one given bitmap\nfunc (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd {\n\treturn c.bitOp(ctx, \"or\", destKey, keys...)\n}\n\n// BitOpXor creates a new bitmap in which users are the result of XORing all given bitmaps\nfunc (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd {\n\treturn c.bitOp(ctx, \"xor\", destKey, keys...)\n}\n\n// BitOpNot creates a new bitmap in which users are not members of a given bitmap\nfunc (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd {\n\treturn c.bitOp(ctx, \"not\", destKey, key)\n}\n\n// BitOpDiff creates a new bitmap in which users are members of bitmap X but not of any of bitmaps Y1, Y2, …\n// Introduced with Redis 8.2\nfunc (c cmdable) BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd {\n\treturn c.bitOp(ctx, \"diff\", destKey, keys...)\n}\n\n// BitOpDiff1 creates a new bitmap in which users are members of one or more of bitmaps Y1, Y2, … but not members of bitmap X\n// Introduced with Redis 8.2\nfunc (c cmdable) BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd {\n\treturn c.bitOp(ctx, \"diff1\", destKey, keys...)\n}\n\n// BitOpAndOr creates a new bitmap in which users are members of bitmap X and also members of one or more of bitmaps Y1, Y2, …\n// Introduced with Redis 8.2\nfunc (c cmdable) BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd {\n\treturn c.bitOp(ctx, \"andor\", destKey, keys...)\n}\n\n// BitOpOne creates a new bitmap in which users are members of exactly one of the given bitmaps\n// Introduced with Redis 8.2\nfunc (c cmdable) BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd {\n\treturn c.bitOp(ctx, \"one\", destKey, keys...)\n}\n\n// BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end\n// if you need the `byte | bit` parameter, please use `BitPosSpan`.\nfunc (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd {\n\targs := make([]interface{}, 3+len(pos))\n\targs[0] = \"bitpos\"\n\targs[1] = key\n\targs[2] = bit\n\tswitch len(pos) {\n\tcase 0:\n\tcase 1:\n\t\targs[3] = pos[0]\n\tcase 2:\n\t\targs[3] = pos[0]\n\t\targs[4] = pos[1]\n\tdefault:\n\t\tcmd := NewIntCmd(ctx)\n\t\tcmd.SetErr(errors.New(\"too many arguments\"))\n\t\treturn cmd\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BitPosSpan supports the `byte | bit` parameters in redis version 7.0,\n// the bitpos command defaults to using byte type for the `start-end` range,\n// which means it counts in bytes from start to end. you can set the value\n// of \"span\" to determine the type of `start-end`.\n// span = \"bit\", cmd: bitpos key bit start end bit\n// span = \"byte\", cmd: bitpos key bit start end byte\nfunc (c cmdable) BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"bitpos\", key, bit, start, end, span)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BitField accepts multiple values:\n//   - BitField(\"set\", \"i1\", \"offset1\", \"value1\",\"cmd2\", \"type2\", \"offset2\", \"value2\")\n//   - BitField([]string{\"cmd1\", \"type1\", \"offset1\", \"value1\",\"cmd2\", \"type2\", \"offset2\", \"value2\"})\n//   - BitField([]interface{}{\"cmd1\", \"type1\", \"offset1\", \"value1\",\"cmd2\", \"type2\", \"offset2\", \"value2\"})\nfunc (c cmdable) BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"bitfield\"\n\targs[1] = key\n\targs = appendArgs(args, values)\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BitFieldRO - Read-only variant of the BITFIELD command.\n// It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas.\n// - BitFieldRO(ctx, key, \"<Encoding0>\", \"<Offset0>\", \"<Encoding1>\",\"<Offset1>\")\nfunc (c cmdable) BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"BITFIELD_RO\"\n\targs[1] = key\n\tif len(values)%2 != 0 {\n\t\tc := NewIntSliceCmd(ctx)\n\t\tc.SetErr(errors.New(\"BitFieldRO: invalid number of arguments, must be even\"))\n\t\treturn c\n\t}\n\tfor i := 0; i < len(values); i += 2 {\n\t\targs = append(args, \"GET\", values[i], values[i+1])\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "bitmap_commands_test.go",
    "content": "package redis_test\n\nimport (\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype bitCountExpected struct {\n\tStart    int64\n\tEnd      int64\n\tExpected int64\n}\n\nvar _ = Describe(\"BitCountBite\", func() {\n\tvar client *redis.Client\n\tkey := \"bit_count_test\"\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t\tvalues := []int{0, 1, 0, 0, 1, 0, 1, 0, 1, 1}\n\t\tfor i, v := range values {\n\t\t\tcmd := client.SetBit(ctx, key, int64(i), v)\n\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"bit count bite\", func() {\n\t\tvar expected = []bitCountExpected{\n\t\t\t{0, 0, 0},\n\t\t\t{0, 1, 1},\n\t\t\t{0, 2, 1},\n\t\t\t{0, 3, 1},\n\t\t\t{0, 4, 2},\n\t\t\t{0, 5, 2},\n\t\t\t{0, 6, 3},\n\t\t\t{0, 7, 3},\n\t\t\t{0, 8, 4},\n\t\t\t{0, 9, 5},\n\t\t}\n\n\t\tfor _, e := range expected {\n\t\t\tcmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexBit})\n\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(cmd.Val()).To(Equal(e.Expected))\n\t\t}\n\t})\n})\n\nvar _ = Describe(\"BitCountByte\", func() {\n\tvar client *redis.Client\n\tkey := \"bit_count_test\"\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t\tvalues := []int{0, 0, 0, 0, 0, 0, 0, 1, 1, 1}\n\t\tfor i, v := range values {\n\t\t\tcmd := client.SetBit(ctx, key, int64(i), v)\n\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"bit count byte\", func() {\n\t\tvar expected = []bitCountExpected{\n\t\t\t{0, 0, 1},\n\t\t\t{0, 1, 3},\n\t\t}\n\n\t\tfor _, e := range expected {\n\t\t\tcmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexByte})\n\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(cmd.Val()).To(Equal(e.Expected))\n\t\t}\n\t})\n\n\tIt(\"bit count byte with no unit specified\", func() {\n\t\tvar expected = []bitCountExpected{\n\t\t\t{0, 0, 1},\n\t\t\t{0, 1, 3},\n\t\t}\n\n\t\tfor _, e := range expected {\n\t\t\tcmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End})\n\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(cmd.Val()).To(Equal(e.Expected))\n\t\t}\n\t})\n})\n"
  },
  {
    "path": "cluster_commands.go",
    "content": "package redis\n\nimport \"context\"\n\ntype ClusterCmdable interface {\n\tClusterMyShardID(ctx context.Context) *StringCmd\n\tClusterMyID(ctx context.Context) *StringCmd\n\tClusterSlots(ctx context.Context) *ClusterSlotsCmd\n\tClusterShards(ctx context.Context) *ClusterShardsCmd\n\tClusterLinks(ctx context.Context) *ClusterLinksCmd\n\tClusterNodes(ctx context.Context) *StringCmd\n\tClusterMeet(ctx context.Context, host, port string) *StatusCmd\n\tClusterForget(ctx context.Context, nodeID string) *StatusCmd\n\tClusterReplicate(ctx context.Context, nodeID string) *StatusCmd\n\tClusterResetSoft(ctx context.Context) *StatusCmd\n\tClusterResetHard(ctx context.Context) *StatusCmd\n\tClusterInfo(ctx context.Context) *StringCmd\n\tClusterKeySlot(ctx context.Context, key string) *IntCmd\n\tClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd\n\tClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd\n\tClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd\n\tClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd\n\tClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd\n\tClusterSaveConfig(ctx context.Context) *StatusCmd\n\tClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd\n\tClusterFailover(ctx context.Context) *StatusCmd\n\tClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd\n\tClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd\n\tReadOnly(ctx context.Context) *StatusCmd\n\tReadWrite(ctx context.Context) *StatusCmd\n}\n\nfunc (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"cluster\", \"myshardid\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterMyID(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"cluster\", \"myid\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ClusterSlots returns the mapping of cluster slots to nodes.\n//\n// Deprecated: Use ClusterShards instead as of Redis 7.0.0.\nfunc (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {\n\tcmd := NewClusterSlotsCmd(ctx, \"cluster\", \"slots\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterShards(ctx context.Context) *ClusterShardsCmd {\n\tcmd := NewClusterShardsCmd(ctx, \"cluster\", \"shards\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterLinks(ctx context.Context) *ClusterLinksCmd {\n\tcmd := NewClusterLinksCmd(ctx, \"cluster\", \"links\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterNodes(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"cluster\", \"nodes\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterMeet(ctx context.Context, host, port string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"cluster\", \"meet\", host, port)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterForget(ctx context.Context, nodeID string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"cluster\", \"forget\", nodeID)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"cluster\", \"replicate\", nodeID)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterResetSoft(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"cluster\", \"reset\", \"soft\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterResetHard(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"cluster\", \"reset\", \"hard\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterInfo(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"cluster\", \"info\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterKeySlot(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"cluster\", \"keyslot\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"cluster\", \"getkeysinslot\", slot, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"cluster\", \"count-failure-reports\", nodeID)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"cluster\", \"countkeysinslot\", slot)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd {\n\targs := make([]interface{}, 2+len(slots))\n\targs[0] = \"cluster\"\n\targs[1] = \"delslots\"\n\tfor i, slot := range slots {\n\t\targs[2+i] = slot\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd {\n\tsize := max - min + 1\n\tslots := make([]int, size)\n\tfor i := 0; i < size; i++ {\n\t\tslots[i] = min + i\n\t}\n\treturn c.ClusterDelSlots(ctx, slots...)\n}\n\nfunc (c cmdable) ClusterSaveConfig(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"cluster\", \"saveconfig\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ClusterSlaves lists the replica nodes of a master node.\n//\n// Deprecated: Use ClusterReplicas instead as of Redis 5.0.0.\nfunc (c cmdable) ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"cluster\", \"slaves\", nodeID)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterFailover(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"cluster\", \"failover\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd {\n\targs := make([]interface{}, 2+len(slots))\n\targs[0] = \"cluster\"\n\targs[1] = \"addslots\"\n\tfor i, num := range slots {\n\t\targs[2+i] = num\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd {\n\tsize := max - min + 1\n\tslots := make([]int, size)\n\tfor i := 0; i < size; i++ {\n\t\tslots[i] = min + i\n\t}\n\treturn c.ClusterAddSlots(ctx, slots...)\n}\n\nfunc (c cmdable) ReadOnly(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"readonly\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ReadWrite(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"readwrite\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "command.go",
    "content": "package redis\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/hscan\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/routing\"\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\n// keylessCommands contains Redis commands that have empty key specifications (9th slot empty)\n// Only includes core Redis commands, excludes FT.*, ts.*, timeseries.*, search.* and subcommands\nvar keylessCommands = map[string]struct{}{\n\t\"acl\":          {},\n\t\"asking\":       {},\n\t\"auth\":         {},\n\t\"bgrewriteaof\": {},\n\t\"bgsave\":       {},\n\t\"client\":       {},\n\t\"cluster\":      {},\n\t\"config\":       {},\n\t\"debug\":        {},\n\t\"discard\":      {},\n\t\"echo\":         {},\n\t\"exec\":         {},\n\t\"failover\":     {},\n\t\"function\":     {},\n\t\"hello\":        {},\n\t\"hotkeys\":      {},\n\t\"latency\":      {},\n\t\"lolwut\":       {},\n\t\"module\":       {},\n\t\"monitor\":      {},\n\t\"multi\":        {},\n\t\"pfselftest\":   {},\n\t\"ping\":         {},\n\t\"psubscribe\":   {},\n\t\"psync\":        {},\n\t\"publish\":      {},\n\t\"pubsub\":       {},\n\t\"punsubscribe\": {},\n\t\"quit\":         {},\n\t\"readonly\":     {},\n\t\"readwrite\":    {},\n\t\"replconf\":     {},\n\t\"replicaof\":    {},\n\t\"role\":         {},\n\t\"save\":         {},\n\t\"script\":       {},\n\t\"select\":       {},\n\t\"shutdown\":     {},\n\t\"slaveof\":      {},\n\t\"slowlog\":      {},\n\t\"subscribe\":    {},\n\t\"swapdb\":       {},\n\t\"sync\":         {},\n\t\"time\":         {},\n\t\"unsubscribe\":  {},\n\t\"unwatch\":      {},\n\t\"wait\":         {},\n}\n\n// CmdTyper interface for getting command type\ntype CmdTyper interface {\n\tGetCmdType() CmdType\n}\n\n// CmdTypeGetter interface for getting command type without circular imports\ntype CmdTypeGetter interface {\n\tGetCmdType() CmdType\n}\n\ntype CmdType uint8\n\nconst (\n\tCmdTypeGeneric CmdType = iota\n\tCmdTypeString\n\tCmdTypeInt\n\tCmdTypeBool\n\tCmdTypeFloat\n\tCmdTypeStringSlice\n\tCmdTypeIntSlice\n\tCmdTypeFloatSlice\n\tCmdTypeBoolSlice\n\tCmdTypeMapStringString\n\tCmdTypeMapStringInt\n\tCmdTypeMapStringInterface\n\tCmdTypeMapStringInterfaceSlice\n\tCmdTypeSlice\n\tCmdTypeStatus\n\tCmdTypeDuration\n\tCmdTypeTime\n\tCmdTypeKeyValueSlice\n\tCmdTypeStringStructMap\n\tCmdTypeXMessageSlice\n\tCmdTypeXStreamSlice\n\tCmdTypeXPending\n\tCmdTypeXPendingExt\n\tCmdTypeXAutoClaim\n\tCmdTypeXAutoClaimJustID\n\tCmdTypeXInfoConsumers\n\tCmdTypeXInfoGroups\n\tCmdTypeXInfoStream\n\tCmdTypeXInfoStreamFull\n\tCmdTypeZSlice\n\tCmdTypeZWithKey\n\tCmdTypeScan\n\tCmdTypeClusterSlots\n\tCmdTypeGeoLocation\n\tCmdTypeGeoSearchLocation\n\tCmdTypeGeoPos\n\tCmdTypeCommandsInfo\n\tCmdTypeSlowLog\n\tCmdTypeMapStringStringSlice\n\tCmdTypeMapMapStringInterface\n\tCmdTypeKeyValues\n\tCmdTypeZSliceWithKey\n\tCmdTypeFunctionList\n\tCmdTypeFunctionStats\n\tCmdTypeLCS\n\tCmdTypeKeyFlags\n\tCmdTypeClusterLinks\n\tCmdTypeClusterShards\n\tCmdTypeRankWithScore\n\tCmdTypeClientInfo\n\tCmdTypeACLLog\n\tCmdTypeInfo\n\tCmdTypeMonitor\n\tCmdTypeJSON\n\tCmdTypeJSONSlice\n\tCmdTypeIntPointerSlice\n\tCmdTypeScanDump\n\tCmdTypeBFInfo\n\tCmdTypeCFInfo\n\tCmdTypeCMSInfo\n\tCmdTypeTopKInfo\n\tCmdTypeTDigestInfo\n\tCmdTypeFTSynDump\n\tCmdTypeAggregate\n\tCmdTypeFTInfo\n\tCmdTypeFTSpellCheck\n\tCmdTypeFTSearch\n\tCmdTypeTSTimestampValue\n\tCmdTypeTSTimestampValueSlice\n\tCmdTypeHotKeys\n)\n\ntype (\n\tCmdTypeXAutoClaimValue struct {\n\t\tmessages []XMessage\n\t\tstart    string\n\t}\n\n\tCmdTypeXAutoClaimJustIDValue struct {\n\t\tids   []string\n\t\tstart string\n\t}\n\n\tCmdTypeScanValue struct {\n\t\tkeys   []string\n\t\tcursor uint64\n\t}\n\n\tCmdTypeKeyValuesValue struct {\n\t\tkey    string\n\t\tvalues []string\n\t}\n\n\tCmdTypeZSliceWithKeyValue struct {\n\t\tkey    string\n\t\tzSlice []Z\n\t}\n)\n\ntype Cmder interface {\n\t// command name.\n\t// e.g. \"set k v ex 10\" -> \"set\", \"cluster info\" -> \"cluster\".\n\tName() string\n\n\t// full command name.\n\t// e.g. \"set k v ex 10\" -> \"set\", \"cluster info\" -> \"cluster info\".\n\tFullName() string\n\n\t// all args of the command.\n\t// e.g. \"set k v ex 10\" -> \"[set k v ex 10]\".\n\tArgs() []interface{}\n\n\t// format request and response string.\n\t// e.g. \"set k v ex 10\" -> \"set k v ex 10: OK\", \"get k\" -> \"get k: v\".\n\tString() string\n\n\t// Clone creates a copy of the command.\n\tClone() Cmder\n\n\tstringArg(int) string\n\tfirstKeyPos() int8\n\tSetFirstKeyPos(int8)\n\tstepCount() int8\n\tSetStepCount(int8)\n\n\treadTimeout() *time.Duration\n\treadReply(rd *proto.Reader) error\n\treadRawReply(rd *proto.Reader) error\n\tSetErr(error)\n\tErr() error\n\n\t// GetCmdType returns the command type for fast value extraction\n\tGetCmdType() CmdType\n}\n\nfunc setCmdsErr(cmds []Cmder, e error) {\n\tfor _, cmd := range cmds {\n\t\tif cmd.Err() == nil {\n\t\t\tcmd.SetErr(e)\n\t\t}\n\t}\n}\n\nfunc cmdsFirstErr(cmds []Cmder) error {\n\tfor _, cmd := range cmds {\n\t\tif err := cmd.Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc writeCmds(wr *proto.Writer, cmds []Cmder) error {\n\tfor _, cmd := range cmds {\n\t\tif err := writeCmd(wr, cmd); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc writeCmd(wr *proto.Writer, cmd Cmder) error {\n\treturn wr.WriteArgs(cmd.Args())\n}\n\n// cmdFirstKeyPos returns the position of the first key in the command's arguments.\n// If the command does not have a key, it returns 0.\n// TODO: Use the data in CommandInfo to determine the first key position.\nfunc cmdFirstKeyPos(cmd Cmder) int {\n\tif pos := cmd.firstKeyPos(); pos != 0 {\n\t\treturn int(pos)\n\t}\n\n\tname := cmd.Name()\n\n\t// first check if the command is keyless\n\tif _, ok := keylessCommands[name]; ok {\n\t\treturn 0\n\t}\n\n\tswitch name {\n\tcase \"eval\", \"evalsha\", \"eval_ro\", \"evalsha_ro\":\n\t\tif cmd.stringArg(2) != \"0\" {\n\t\t\treturn 3\n\t\t}\n\n\t\treturn 0\n\tcase \"publish\":\n\t\treturn 1\n\tcase \"memory\":\n\t\t// https://github.com/redis/redis/issues/7493\n\t\tif cmd.stringArg(1) == \"usage\" {\n\t\t\treturn 2\n\t\t}\n\t}\n\treturn 1\n}\n\nfunc cmdString(cmd Cmder, val interface{}) string {\n\tb := make([]byte, 0, 64)\n\n\tfor i, arg := range cmd.Args() {\n\t\tif i > 0 {\n\t\t\tb = append(b, ' ')\n\t\t}\n\t\tb = internal.AppendArg(b, arg)\n\t}\n\n\tif err := cmd.Err(); err != nil {\n\t\tb = append(b, \": \"...)\n\t\tb = append(b, err.Error()...)\n\t} else if val != nil {\n\t\tb = append(b, \": \"...)\n\t\tb = internal.AppendArg(b, val)\n\t}\n\n\treturn util.BytesToString(b)\n}\n\n//------------------------------------------------------------------------------\n\ntype baseCmd struct {\n\tctx          context.Context\n\targs         []interface{}\n\terr          error\n\tkeyPos       int8\n\t_stepCount   int8\n\trawVal       interface{}\n\t_readTimeout *time.Duration\n\tcmdType      CmdType\n}\n\nvar _ Cmder = (*Cmd)(nil)\n\nfunc (cmd *baseCmd) Name() string {\n\tif len(cmd.args) == 0 {\n\t\treturn \"\"\n\t}\n\t// Cmd name must be lower cased.\n\treturn internal.ToLower(cmd.stringArg(0))\n}\n\nfunc (cmd *baseCmd) FullName() string {\n\tswitch name := cmd.Name(); name {\n\tcase \"cluster\", \"command\":\n\t\tif len(cmd.args) == 1 {\n\t\t\treturn name\n\t\t}\n\t\tif s2, ok := cmd.args[1].(string); ok {\n\t\t\treturn name + \" \" + s2\n\t\t}\n\t\treturn name\n\tdefault:\n\t\treturn name\n\t}\n}\n\nfunc (cmd *baseCmd) Args() []interface{} {\n\treturn cmd.args\n}\n\nfunc (cmd *baseCmd) stringArg(pos int) string {\n\tif pos < 0 || pos >= len(cmd.args) {\n\t\treturn \"\"\n\t}\n\targ := cmd.args[pos]\n\tswitch v := arg.(type) {\n\tcase string:\n\t\treturn v\n\tcase []byte:\n\t\treturn string(v)\n\tdefault:\n\t\t// TODO: consider using appendArg\n\t\treturn fmt.Sprint(v)\n\t}\n}\n\nfunc (cmd *baseCmd) firstKeyPos() int8 {\n\treturn cmd.keyPos\n}\n\nfunc (cmd *baseCmd) SetFirstKeyPos(keyPos int8) {\n\tcmd.keyPos = keyPos\n}\n\nfunc (cmd *baseCmd) stepCount() int8 {\n\treturn cmd._stepCount\n}\n\nfunc (cmd *baseCmd) SetStepCount(stepCount int8) {\n\tcmd._stepCount = stepCount\n}\n\nfunc (cmd *baseCmd) SetErr(e error) {\n\tcmd.err = e\n}\n\nfunc (cmd *baseCmd) Err() error {\n\treturn cmd.err\n}\n\nfunc (cmd *baseCmd) readTimeout() *time.Duration {\n\treturn cmd._readTimeout\n}\n\nfunc (cmd *baseCmd) setReadTimeout(d time.Duration) {\n\tcmd._readTimeout = &d\n}\n\nfunc (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) {\n\tcmd.rawVal, err = rd.ReadReply()\n\treturn err\n}\n\nfunc (cmd *baseCmd) GetCmdType() CmdType {\n\treturn cmd.cmdType\n}\n\nfunc (cmd *baseCmd) cloneBaseCmd() baseCmd {\n\tvar readTimeout *time.Duration\n\tif cmd._readTimeout != nil {\n\t\ttimeout := *cmd._readTimeout\n\t\treadTimeout = &timeout\n\t}\n\n\t// Create a copy of args slice\n\targs := make([]interface{}, len(cmd.args))\n\tcopy(args, cmd.args)\n\n\treturn baseCmd{\n\t\tctx:          cmd.ctx,\n\t\targs:         args,\n\t\terr:          cmd.err,\n\t\tkeyPos:       cmd.keyPos,\n\t\t_stepCount:   cmd._stepCount,\n\t\trawVal:       cmd.rawVal,\n\t\t_readTimeout: readTimeout,\n\t\tcmdType:      cmd.cmdType,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype Cmd struct {\n\tbaseCmd\n\n\tval interface{}\n}\n\nfunc NewCmd(ctx context.Context, args ...interface{}) *Cmd {\n\treturn &Cmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeGeneric,\n\t\t},\n\t}\n}\n\nfunc (cmd *Cmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *Cmd) SetVal(val interface{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *Cmd) Val() interface{} {\n\treturn cmd.val\n}\n\nfunc (cmd *Cmd) Result() (interface{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *Cmd) Text() (string, error) {\n\tif cmd.err != nil {\n\t\treturn \"\", cmd.err\n\t}\n\treturn toString(cmd.val)\n}\n\nfunc toString(val interface{}) (string, error) {\n\tswitch val := val.(type) {\n\tcase string:\n\t\treturn val, nil\n\tdefault:\n\t\terr := fmt.Errorf(\"redis: unexpected type=%T for String\", val)\n\t\treturn \"\", err\n\t}\n}\n\nfunc (cmd *Cmd) Int() (int, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\tswitch val := cmd.val.(type) {\n\tcase int64:\n\t\treturn int(val), nil\n\tcase string:\n\t\treturn strconv.Atoi(val)\n\tdefault:\n\t\terr := fmt.Errorf(\"redis: unexpected type=%T for Int\", val)\n\t\treturn 0, err\n\t}\n}\n\nfunc (cmd *Cmd) Int64() (int64, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn toInt64(cmd.val)\n}\n\nfunc toInt64(val interface{}) (int64, error) {\n\tswitch val := val.(type) {\n\tcase int64:\n\t\treturn val, nil\n\tcase string:\n\t\treturn strconv.ParseInt(val, 10, 64)\n\tdefault:\n\t\terr := fmt.Errorf(\"redis: unexpected type=%T for Int64\", val)\n\t\treturn 0, err\n\t}\n}\n\nfunc (cmd *Cmd) Uint64() (uint64, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn toUint64(cmd.val)\n}\n\nfunc toUint64(val interface{}) (uint64, error) {\n\tswitch val := val.(type) {\n\tcase int64:\n\t\treturn uint64(val), nil\n\tcase string:\n\t\treturn strconv.ParseUint(val, 10, 64)\n\tdefault:\n\t\terr := fmt.Errorf(\"redis: unexpected type=%T for Uint64\", val)\n\t\treturn 0, err\n\t}\n}\n\nfunc (cmd *Cmd) Float32() (float32, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn toFloat32(cmd.val)\n}\n\nfunc toFloat32(val interface{}) (float32, error) {\n\tswitch val := val.(type) {\n\tcase int64:\n\t\treturn float32(val), nil\n\tcase string:\n\t\tf, err := strconv.ParseFloat(val, 32)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn float32(f), nil\n\tdefault:\n\t\terr := fmt.Errorf(\"redis: unexpected type=%T for Float32\", val)\n\t\treturn 0, err\n\t}\n}\n\nfunc (cmd *Cmd) Float64() (float64, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn toFloat64(cmd.val)\n}\n\nfunc toFloat64(val interface{}) (float64, error) {\n\tswitch val := val.(type) {\n\tcase int64:\n\t\treturn float64(val), nil\n\tcase string:\n\t\treturn strconv.ParseFloat(val, 64)\n\tdefault:\n\t\terr := fmt.Errorf(\"redis: unexpected type=%T for Float64\", val)\n\t\treturn 0, err\n\t}\n}\n\nfunc (cmd *Cmd) Bool() (bool, error) {\n\tif cmd.err != nil {\n\t\treturn false, cmd.err\n\t}\n\treturn toBool(cmd.val)\n}\n\nfunc toBool(val interface{}) (bool, error) {\n\tswitch val := val.(type) {\n\tcase bool:\n\t\treturn val, nil\n\tcase int64:\n\t\treturn val != 0, nil\n\tcase string:\n\t\treturn strconv.ParseBool(val)\n\tdefault:\n\t\terr := fmt.Errorf(\"redis: unexpected type=%T for Bool\", val)\n\t\treturn false, err\n\t}\n}\n\nfunc (cmd *Cmd) Slice() ([]interface{}, error) {\n\tif cmd.err != nil {\n\t\treturn nil, cmd.err\n\t}\n\tswitch val := cmd.val.(type) {\n\tcase []interface{}:\n\t\treturn val, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: unexpected type=%T for Slice\", val)\n\t}\n}\n\nfunc (cmd *Cmd) StringSlice() ([]string, error) {\n\tslice, err := cmd.Slice()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tss := make([]string, len(slice))\n\tfor i, iface := range slice {\n\t\tval, err := toString(iface)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tss[i] = val\n\t}\n\treturn ss, nil\n}\n\nfunc (cmd *Cmd) Int64Slice() ([]int64, error) {\n\tslice, err := cmd.Slice()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnums := make([]int64, len(slice))\n\tfor i, iface := range slice {\n\t\tval, err := toInt64(iface)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnums[i] = val\n\t}\n\treturn nums, nil\n}\n\nfunc (cmd *Cmd) Uint64Slice() ([]uint64, error) {\n\tslice, err := cmd.Slice()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnums := make([]uint64, len(slice))\n\tfor i, iface := range slice {\n\t\tval, err := toUint64(iface)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnums[i] = val\n\t}\n\treturn nums, nil\n}\n\nfunc (cmd *Cmd) Float32Slice() ([]float32, error) {\n\tslice, err := cmd.Slice()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfloats := make([]float32, len(slice))\n\tfor i, iface := range slice {\n\t\tval, err := toFloat32(iface)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfloats[i] = val\n\t}\n\treturn floats, nil\n}\n\nfunc (cmd *Cmd) Float64Slice() ([]float64, error) {\n\tslice, err := cmd.Slice()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfloats := make([]float64, len(slice))\n\tfor i, iface := range slice {\n\t\tval, err := toFloat64(iface)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfloats[i] = val\n\t}\n\treturn floats, nil\n}\n\nfunc (cmd *Cmd) BoolSlice() ([]bool, error) {\n\tslice, err := cmd.Slice()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbools := make([]bool, len(slice))\n\tfor i, iface := range slice {\n\t\tval, err := toBool(iface)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbools[i] = val\n\t}\n\treturn bools, nil\n}\n\nfunc (cmd *Cmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = rd.ReadReply()\n\treturn err\n}\n\nfunc (cmd *Cmd) Clone() Cmder {\n\treturn &Cmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype SliceCmd struct {\n\tbaseCmd\n\n\tval []interface{}\n}\n\nvar _ Cmder = (*SliceCmd)(nil)\n\nfunc NewSliceCmd(ctx context.Context, args ...interface{}) *SliceCmd {\n\treturn &SliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *SliceCmd) SetVal(val []interface{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *SliceCmd) Val() []interface{} {\n\treturn cmd.val\n}\n\nfunc (cmd *SliceCmd) Result() ([]interface{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *SliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\n// Scan scans the results from the map into a destination struct. The map keys\n// are matched in the Redis struct fields by the `redis:\"field\"` tag.\nfunc (cmd *SliceCmd) Scan(dst interface{}) error {\n\tif cmd.err != nil {\n\t\treturn cmd.err\n\t}\n\n\t// Pass the list of keys and values.\n\t// Skip the first two args for: HMGET key\n\tvar args []interface{}\n\tif cmd.args[0] == \"hmget\" {\n\t\targs = cmd.args[2:]\n\t} else {\n\t\t// Otherwise, it's: MGET field field ...\n\t\targs = cmd.args[1:]\n\t}\n\n\treturn hscan.Scan(dst, args, cmd.val)\n}\n\nfunc (cmd *SliceCmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = rd.ReadSlice()\n\treturn err\n}\n\nfunc (cmd *SliceCmd) Clone() Cmder {\n\tvar val []interface{}\n\tif cmd.val != nil {\n\t\tval = make([]interface{}, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &SliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype StatusCmd struct {\n\tbaseCmd\n\n\tval string\n}\n\nvar _ Cmder = (*StatusCmd)(nil)\n\nfunc NewStatusCmd(ctx context.Context, args ...interface{}) *StatusCmd {\n\treturn &StatusCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeStatus,\n\t\t},\n\t}\n}\n\nfunc (cmd *StatusCmd) SetVal(val string) {\n\tcmd.val = val\n}\n\nfunc (cmd *StatusCmd) Val() string {\n\treturn cmd.val\n}\n\nfunc (cmd *StatusCmd) Result() (string, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *StatusCmd) Bytes() ([]byte, error) {\n\treturn util.StringToBytes(cmd.val), cmd.err\n}\n\nfunc (cmd *StatusCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *StatusCmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = rd.ReadString()\n\treturn err\n}\n\nfunc (cmd *StatusCmd) Clone() Cmder {\n\treturn &StatusCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype IntCmd struct {\n\tbaseCmd\n\n\tval int64\n}\n\nvar _ Cmder = (*IntCmd)(nil)\n\nfunc NewIntCmd(ctx context.Context, args ...interface{}) *IntCmd {\n\treturn &IntCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeInt,\n\t\t},\n\t}\n}\n\nfunc (cmd *IntCmd) SetVal(val int64) {\n\tcmd.val = val\n}\n\nfunc (cmd *IntCmd) Val() int64 {\n\treturn cmd.val\n}\n\nfunc (cmd *IntCmd) Result() (int64, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *IntCmd) Uint64() (uint64, error) {\n\treturn uint64(cmd.val), cmd.err\n}\n\nfunc (cmd *IntCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *IntCmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = rd.ReadInt()\n\treturn err\n}\n\nfunc (cmd *IntCmd) Clone() Cmder {\n\treturn &IntCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\n// DigestCmd is a command that returns a uint64 xxh3 hash digest.\n//\n// This command is specifically designed for the Redis DIGEST command,\n// which returns the xxh3 hash of a key's value as a hex string.\n// The hex string is automatically parsed to a uint64 value.\n//\n// The digest can be used for optimistic locking with SetIFDEQ, SetIFDNE,\n// and DelExArgs commands.\n//\n// For examples of client-side digest generation and usage patterns, see:\n// example/digest-optimistic-locking/\n//\n// Redis 8.4+. See https://redis.io/commands/digest/\ntype DigestCmd struct {\n\tbaseCmd\n\n\tval uint64\n}\n\nvar _ Cmder = (*DigestCmd)(nil)\n\nfunc NewDigestCmd(ctx context.Context, args ...interface{}) *DigestCmd {\n\treturn &DigestCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:  ctx,\n\t\t\targs: args,\n\t\t},\n\t}\n}\n\nfunc (cmd *DigestCmd) SetVal(val uint64) {\n\tcmd.val = val\n}\n\nfunc (cmd *DigestCmd) Val() uint64 {\n\treturn cmd.val\n}\n\nfunc (cmd *DigestCmd) Result() (uint64, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *DigestCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *DigestCmd) Clone() Cmder {\n\treturn &DigestCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\nfunc (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) {\n\t// Redis DIGEST command returns a hex string (e.g., \"a1b2c3d4e5f67890\")\n\t// We parse it as a uint64 xxh3 hash value\n\tvar hexStr string\n\thexStr, err = rd.ReadString()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Parse hex string to uint64\n\tcmd.val, err = strconv.ParseUint(hexStr, 16, 64)\n\treturn err\n}\n\n//------------------------------------------------------------------------------\n\ntype IntSliceCmd struct {\n\tbaseCmd\n\n\tval []int64\n}\n\nvar _ Cmder = (*IntSliceCmd)(nil)\n\nfunc NewIntSliceCmd(ctx context.Context, args ...interface{}) *IntSliceCmd {\n\treturn &IntSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeIntSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *IntSliceCmd) SetVal(val []int64) {\n\tcmd.val = val\n}\n\nfunc (cmd *IntSliceCmd) Val() []int64 {\n\treturn cmd.val\n}\n\nfunc (cmd *IntSliceCmd) Result() ([]int64, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *IntSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *IntSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]int64, n)\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tif cmd.val[i], err = rd.ReadInt(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *IntSliceCmd) Clone() Cmder {\n\tvar val []int64\n\tif cmd.val != nil {\n\t\tval = make([]int64, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &IntSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype DurationCmd struct {\n\tbaseCmd\n\n\tval       time.Duration\n\tprecision time.Duration\n}\n\nvar _ Cmder = (*DurationCmd)(nil)\n\nfunc NewDurationCmd(ctx context.Context, precision time.Duration, args ...interface{}) *DurationCmd {\n\treturn &DurationCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeDuration,\n\t\t},\n\t\tprecision: precision,\n\t}\n}\n\nfunc (cmd *DurationCmd) SetVal(val time.Duration) {\n\tcmd.val = val\n}\n\nfunc (cmd *DurationCmd) Val() time.Duration {\n\treturn cmd.val\n}\n\nfunc (cmd *DurationCmd) Result() (time.Duration, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *DurationCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *DurationCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadInt()\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch n {\n\t// -2 if the key does not exist\n\t// -1 if the key exists but has no associated expire\n\tcase -2, -1:\n\t\tcmd.val = time.Duration(n)\n\tdefault:\n\t\tcmd.val = time.Duration(n) * cmd.precision\n\t}\n\treturn nil\n}\n\nfunc (cmd *DurationCmd) Clone() Cmder {\n\treturn &DurationCmd{\n\t\tbaseCmd:   cmd.cloneBaseCmd(),\n\t\tval:       cmd.val,\n\t\tprecision: cmd.precision,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype TimeCmd struct {\n\tbaseCmd\n\n\tval time.Time\n}\n\nvar _ Cmder = (*TimeCmd)(nil)\n\nfunc NewTimeCmd(ctx context.Context, args ...interface{}) *TimeCmd {\n\treturn &TimeCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeTime,\n\t\t},\n\t}\n}\n\nfunc (cmd *TimeCmd) SetVal(val time.Time) {\n\tcmd.val = val\n}\n\nfunc (cmd *TimeCmd) Val() time.Time {\n\treturn cmd.val\n}\n\nfunc (cmd *TimeCmd) Result() (time.Time, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *TimeCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *TimeCmd) readReply(rd *proto.Reader) error {\n\tif err := rd.ReadFixedArrayLen(2); err != nil {\n\t\treturn err\n\t}\n\tsecond, err := rd.ReadInt()\n\tif err != nil {\n\t\treturn err\n\t}\n\tmicrosecond, err := rd.ReadInt()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = time.Unix(second, microsecond*1000)\n\treturn nil\n}\n\nfunc (cmd *TimeCmd) Clone() Cmder {\n\treturn &TimeCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype BoolCmd struct {\n\tbaseCmd\n\n\tval bool\n}\n\nvar _ Cmder = (*BoolCmd)(nil)\n\nfunc NewBoolCmd(ctx context.Context, args ...interface{}) *BoolCmd {\n\treturn &BoolCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeBool,\n\t\t},\n\t}\n}\n\nfunc (cmd *BoolCmd) SetVal(val bool) {\n\tcmd.val = val\n}\n\nfunc (cmd *BoolCmd) Val() bool {\n\treturn cmd.val\n}\n\nfunc (cmd *BoolCmd) Result() (bool, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *BoolCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *BoolCmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = rd.ReadBool()\n\n\t// `SET key value NX` returns nil when key already exists. But\n\t// `SETNX key value` returns bool (0/1). So convert nil to bool.\n\tif err == Nil {\n\t\tcmd.val = false\n\t\terr = nil\n\t}\n\treturn err\n}\n\nfunc (cmd *BoolCmd) Clone() Cmder {\n\treturn &BoolCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype StringCmd struct {\n\tbaseCmd\n\n\tval string\n}\n\nvar _ Cmder = (*StringCmd)(nil)\n\nfunc NewStringCmd(ctx context.Context, args ...interface{}) *StringCmd {\n\treturn &StringCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeString,\n\t\t},\n\t}\n}\n\nfunc (cmd *StringCmd) SetVal(val string) {\n\tcmd.val = val\n}\n\nfunc (cmd *StringCmd) Val() string {\n\treturn cmd.val\n}\n\nfunc (cmd *StringCmd) Result() (string, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *StringCmd) Bytes() ([]byte, error) {\n\treturn util.StringToBytes(cmd.val), cmd.err\n}\n\nfunc (cmd *StringCmd) Bool() (bool, error) {\n\tif cmd.err != nil {\n\t\treturn false, cmd.err\n\t}\n\treturn strconv.ParseBool(cmd.val)\n}\n\nfunc (cmd *StringCmd) Int() (int, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn strconv.Atoi(cmd.val)\n}\n\nfunc (cmd *StringCmd) Int64() (int64, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn strconv.ParseInt(cmd.val, 10, 64)\n}\n\nfunc (cmd *StringCmd) Uint64() (uint64, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn strconv.ParseUint(cmd.val, 10, 64)\n}\n\nfunc (cmd *StringCmd) Float32() (float32, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\tf, err := strconv.ParseFloat(cmd.val, 32)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn float32(f), nil\n}\n\nfunc (cmd *StringCmd) Float64() (float64, error) {\n\tif cmd.err != nil {\n\t\treturn 0, cmd.err\n\t}\n\treturn strconv.ParseFloat(cmd.val, 64)\n}\n\nfunc (cmd *StringCmd) Time() (time.Time, error) {\n\tif cmd.err != nil {\n\t\treturn time.Time{}, cmd.err\n\t}\n\treturn time.Parse(time.RFC3339Nano, cmd.val)\n}\n\nfunc (cmd *StringCmd) Scan(val interface{}) error {\n\tif cmd.err != nil {\n\t\treturn cmd.err\n\t}\n\treturn proto.Scan([]byte(cmd.val), val)\n}\n\nfunc (cmd *StringCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *StringCmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = rd.ReadString()\n\treturn err\n}\n\nfunc (cmd *StringCmd) Clone() Cmder {\n\treturn &StringCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype FloatCmd struct {\n\tbaseCmd\n\n\tval float64\n}\n\nvar _ Cmder = (*FloatCmd)(nil)\n\nfunc NewFloatCmd(ctx context.Context, args ...interface{}) *FloatCmd {\n\treturn &FloatCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFloat,\n\t\t},\n\t}\n}\n\nfunc (cmd *FloatCmd) SetVal(val float64) {\n\tcmd.val = val\n}\n\nfunc (cmd *FloatCmd) Val() float64 {\n\treturn cmd.val\n}\n\nfunc (cmd *FloatCmd) Result() (float64, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FloatCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FloatCmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = rd.ReadFloat()\n\treturn err\n}\n\nfunc (cmd *FloatCmd) Clone() Cmder {\n\treturn &FloatCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype FloatSliceCmd struct {\n\tbaseCmd\n\n\tval []float64\n}\n\nvar _ Cmder = (*FloatSliceCmd)(nil)\n\nfunc NewFloatSliceCmd(ctx context.Context, args ...interface{}) *FloatSliceCmd {\n\treturn &FloatSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFloatSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *FloatSliceCmd) SetVal(val []float64) {\n\tcmd.val = val\n}\n\nfunc (cmd *FloatSliceCmd) Val() []float64 {\n\treturn cmd.val\n}\n\nfunc (cmd *FloatSliceCmd) Result() ([]float64, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FloatSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FloatSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make([]float64, n)\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tswitch num, err := rd.ReadFloat(); {\n\t\tcase err == Nil:\n\t\t\tcmd.val[i] = 0\n\t\tcase err != nil:\n\t\t\treturn err\n\t\tdefault:\n\t\t\tcmd.val[i] = num\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *FloatSliceCmd) Clone() Cmder {\n\tvar val []float64\n\tif cmd.val != nil {\n\t\tval = make([]float64, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &FloatSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype StringSliceCmd struct {\n\tbaseCmd\n\n\tval []string\n}\n\nvar _ Cmder = (*StringSliceCmd)(nil)\n\nfunc NewStringSliceCmd(ctx context.Context, args ...interface{}) *StringSliceCmd {\n\treturn &StringSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeStringSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *StringSliceCmd) SetVal(val []string) {\n\tcmd.val = val\n}\n\nfunc (cmd *StringSliceCmd) Val() []string {\n\treturn cmd.val\n}\n\nfunc (cmd *StringSliceCmd) Result() ([]string, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *StringSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *StringSliceCmd) ScanSlice(container interface{}) error {\n\treturn proto.ScanSlice(cmd.val, container)\n}\n\nfunc (cmd *StringSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]string, n)\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tswitch s, err := rd.ReadString(); {\n\t\tcase err == Nil:\n\t\t\tcmd.val[i] = \"\"\n\t\tcase err != nil:\n\t\t\treturn err\n\t\tdefault:\n\t\t\tcmd.val[i] = s\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *StringSliceCmd) Clone() Cmder {\n\tvar val []string\n\tif cmd.val != nil {\n\t\tval = make([]string, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &StringSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype KeyValue struct {\n\tKey   string\n\tValue string\n}\n\ntype KeyValueSliceCmd struct {\n\tbaseCmd\n\n\tval []KeyValue\n}\n\nvar _ Cmder = (*KeyValueSliceCmd)(nil)\n\nfunc NewKeyValueSliceCmd(ctx context.Context, args ...interface{}) *KeyValueSliceCmd {\n\treturn &KeyValueSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeKeyValueSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *KeyValueSliceCmd) SetVal(val []KeyValue) {\n\tcmd.val = val\n}\n\nfunc (cmd *KeyValueSliceCmd) Val() []KeyValue {\n\treturn cmd.val\n}\n\nfunc (cmd *KeyValueSliceCmd) Result() ([]KeyValue, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *KeyValueSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\n// Many commands will respond to two formats:\n//  1. 1) \"one\"\n//  2. (double) 1\n//  2. 1) \"two\"\n//  2. (double) 2\n//\n// OR:\n//  1. \"two\"\n//  2. (double) 2\n//  3. \"one\"\n//  4. (double) 1\nfunc (cmd *KeyValueSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If the n is 0, can't continue reading.\n\tif n == 0 {\n\t\tcmd.val = make([]KeyValue, 0)\n\t\treturn nil\n\t}\n\n\ttyp, err := rd.PeekReplyType()\n\tif err != nil {\n\t\treturn err\n\t}\n\tarray := typ == proto.RespArray\n\n\tif array {\n\t\tcmd.val = make([]KeyValue, n)\n\t} else {\n\t\tcmd.val = make([]KeyValue, n/2)\n\t}\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tif array {\n\t\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif cmd.val[i].Key, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cmd.val[i].Value, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *KeyValueSliceCmd) Clone() Cmder {\n\tvar val []KeyValue\n\tif cmd.val != nil {\n\t\tval = make([]KeyValue, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &KeyValueSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype BoolSliceCmd struct {\n\tbaseCmd\n\n\tval []bool\n}\n\nvar _ Cmder = (*BoolSliceCmd)(nil)\n\nfunc NewBoolSliceCmd(ctx context.Context, args ...interface{}) *BoolSliceCmd {\n\treturn &BoolSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeBoolSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *BoolSliceCmd) SetVal(val []bool) {\n\tcmd.val = val\n}\n\nfunc (cmd *BoolSliceCmd) Val() []bool {\n\treturn cmd.val\n}\n\nfunc (cmd *BoolSliceCmd) Result() ([]bool, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *BoolSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *BoolSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]bool, n)\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tif cmd.val[i], err = rd.ReadBool(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *BoolSliceCmd) Clone() Cmder {\n\tvar val []bool\n\tif cmd.val != nil {\n\t\tval = make([]bool, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &BoolSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype MapStringStringCmd struct {\n\tbaseCmd\n\n\tval map[string]string\n}\n\nvar _ Cmder = (*MapStringStringCmd)(nil)\n\nfunc NewMapStringStringCmd(ctx context.Context, args ...interface{}) *MapStringStringCmd {\n\treturn &MapStringStringCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeMapStringString,\n\t\t},\n\t}\n}\n\nfunc (cmd *MapStringStringCmd) Val() map[string]string {\n\treturn cmd.val\n}\n\nfunc (cmd *MapStringStringCmd) SetVal(val map[string]string) {\n\tcmd.val = val\n}\n\nfunc (cmd *MapStringStringCmd) Result() (map[string]string, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *MapStringStringCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\n// Scan scans the results from the map into a destination struct. The map keys\n// are matched in the Redis struct fields by the `redis:\"field\"` tag.\nfunc (cmd *MapStringStringCmd) Scan(dest interface{}) error {\n\tif cmd.err != nil {\n\t\treturn cmd.err\n\t}\n\n\tstrct, err := hscan.Struct(dest)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor k, v := range cmd.val {\n\t\tif err := strct.Scan(k, v); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *MapStringStringCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make(map[string]string, n)\n\tfor i := 0; i < n; i++ {\n\t\tkey, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvalue, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcmd.val[key] = value\n\t}\n\treturn nil\n}\n\nfunc (cmd *MapStringStringCmd) Clone() Cmder {\n\tvar val map[string]string\n\tif cmd.val != nil {\n\t\tval = make(map[string]string, len(cmd.val))\n\t\tfor k, v := range cmd.val {\n\t\t\tval[k] = v\n\t\t}\n\t}\n\treturn &MapStringStringCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype MapStringIntCmd struct {\n\tbaseCmd\n\n\tval map[string]int64\n}\n\nvar _ Cmder = (*MapStringIntCmd)(nil)\n\nfunc NewMapStringIntCmd(ctx context.Context, args ...interface{}) *MapStringIntCmd {\n\treturn &MapStringIntCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeMapStringInt,\n\t\t},\n\t}\n}\n\nfunc (cmd *MapStringIntCmd) SetVal(val map[string]int64) {\n\tcmd.val = val\n}\n\nfunc (cmd *MapStringIntCmd) Val() map[string]int64 {\n\treturn cmd.val\n}\n\nfunc (cmd *MapStringIntCmd) Result() (map[string]int64, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *MapStringIntCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *MapStringIntCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make(map[string]int64, n)\n\tfor i := 0; i < n; i++ {\n\t\tkey, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tnn, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[key] = nn\n\t}\n\treturn nil\n}\n\nfunc (cmd *MapStringIntCmd) Clone() Cmder {\n\tvar val map[string]int64\n\tif cmd.val != nil {\n\t\tval = make(map[string]int64, len(cmd.val))\n\t\tfor k, v := range cmd.val {\n\t\t\tval[k] = v\n\t\t}\n\t}\n\treturn &MapStringIntCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// ------------------------------------------------------------------------------\ntype MapStringSliceInterfaceCmd struct {\n\tbaseCmd\n\tval map[string][]interface{}\n}\n\nfunc NewMapStringSliceInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringSliceInterfaceCmd {\n\treturn &MapStringSliceInterfaceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeMapStringInterfaceSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *MapStringSliceInterfaceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *MapStringSliceInterfaceCmd) SetVal(val map[string][]interface{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *MapStringSliceInterfaceCmd) Result() (map[string][]interface{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *MapStringSliceInterfaceCmd) Val() map[string][]interface{} {\n\treturn cmd.val\n}\n\nfunc (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) {\n\treadType, err := rd.PeekReplyType()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make(map[string][]interface{})\n\n\tswitch readType {\n\tcase proto.RespMap:\n\t\tn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i := 0; i < n; i++ {\n\t\t\tk, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tnn, err := rd.ReadArrayLen()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcmd.val[k] = make([]interface{}, nn)\n\t\t\tfor j := 0; j < nn; j++ {\n\t\t\t\tvalue, err := rd.ReadReply()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tcmd.val[k][j] = value\n\t\t\t}\n\t\t}\n\tcase proto.RespArray:\n\t\t// RESP2 response\n\t\tn, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < n; i++ {\n\t\t\t// Each entry in this array is itself an array with key details\n\t\t\titemLen, err := rd.ReadArrayLen()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcmd.val[key] = make([]interface{}, 0, itemLen-1)\n\t\t\tfor j := 1; j < itemLen; j++ {\n\t\t\t\t// Read the inner array for timestamp-value pairs\n\t\t\t\tdata, err := rd.ReadReply()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tcmd.val[key] = append(cmd.val[key], data)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *MapStringSliceInterfaceCmd) Clone() Cmder {\n\tvar val map[string][]interface{}\n\tif cmd.val != nil {\n\t\tval = make(map[string][]interface{}, len(cmd.val))\n\t\tfor k, v := range cmd.val {\n\t\t\tif v != nil {\n\t\t\t\tnewSlice := make([]interface{}, len(v))\n\t\t\t\tcopy(newSlice, v)\n\t\t\t\tval[k] = newSlice\n\t\t\t}\n\t\t}\n\t}\n\treturn &MapStringSliceInterfaceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype StringStructMapCmd struct {\n\tbaseCmd\n\n\tval map[string]struct{}\n}\n\nvar _ Cmder = (*StringStructMapCmd)(nil)\n\nfunc NewStringStructMapCmd(ctx context.Context, args ...interface{}) *StringStructMapCmd {\n\treturn &StringStructMapCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeStringStructMap,\n\t\t},\n\t}\n}\n\nfunc (cmd *StringStructMapCmd) SetVal(val map[string]struct{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *StringStructMapCmd) Val() map[string]struct{} {\n\treturn cmd.val\n}\n\nfunc (cmd *StringStructMapCmd) Result() (map[string]struct{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *StringStructMapCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *StringStructMapCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make(map[string]struct{}, n)\n\tfor i := 0; i < n; i++ {\n\t\tkey, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[key] = struct{}{}\n\t}\n\treturn nil\n}\n\nfunc (cmd *StringStructMapCmd) Clone() Cmder {\n\tvar val map[string]struct{}\n\tif cmd.val != nil {\n\t\tval = maps.Clone(cmd.val)\n\t}\n\treturn &StringStructMapCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XMessage struct {\n\tID     string\n\tValues map[string]interface{}\n\t// MillisElapsedFromDelivery is the number of milliseconds since the entry was last delivered.\n\t// Only populated when using XREADGROUP with CLAIM argument for claimed entries.\n\tMillisElapsedFromDelivery int64\n\t// DeliveredCount is the number of times the entry was delivered.\n\t// Only populated when using XREADGROUP with CLAIM argument for claimed entries.\n\tDeliveredCount int64\n}\n\ntype XMessageSliceCmd struct {\n\tbaseCmd\n\n\tval []XMessage\n}\n\nvar _ Cmder = (*XMessageSliceCmd)(nil)\n\nfunc NewXMessageSliceCmd(ctx context.Context, args ...interface{}) *XMessageSliceCmd {\n\treturn &XMessageSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeXMessageSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *XMessageSliceCmd) SetVal(val []XMessage) {\n\tcmd.val = val\n}\n\nfunc (cmd *XMessageSliceCmd) Val() []XMessage {\n\treturn cmd.val\n}\n\nfunc (cmd *XMessageSliceCmd) Result() ([]XMessage, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XMessageSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XMessageSliceCmd) readReply(rd *proto.Reader) (err error) {\n\tcmd.val, err = readXMessageSlice(rd)\n\treturn err\n}\n\nfunc (cmd *XMessageSliceCmd) Clone() Cmder {\n\tvar val []XMessage\n\tif cmd.val != nil {\n\t\tval = make([]XMessage, len(cmd.val))\n\t\tfor i, msg := range cmd.val {\n\t\t\tval[i] = XMessage{\n\t\t\t\tID: msg.ID,\n\t\t\t}\n\t\t\tif msg.Values != nil {\n\t\t\t\tval[i].Values = make(map[string]interface{}, len(msg.Values))\n\t\t\t\tfor k, v := range msg.Values {\n\t\t\t\t\tval[i].Values[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &XMessageSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\nfunc readXMessageSlice(rd *proto.Reader) ([]XMessage, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmsgs := make([]XMessage, n)\n\tfor i := 0; i < len(msgs); i++ {\n\t\tif msgs[i], err = readXMessage(rd); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn msgs, nil\n}\n\nfunc readXMessage(rd *proto.Reader) (XMessage, error) {\n\t// Read array length can be 2 or 4 (with CLAIM metadata)\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn XMessage{}, err\n\t}\n\n\tif n != 2 && n != 4 {\n\t\treturn XMessage{}, fmt.Errorf(\"redis: got %d elements in the XMessage array, expected 2 or 4\", n)\n\t}\n\n\tid, err := rd.ReadString()\n\tif err != nil {\n\t\treturn XMessage{}, err\n\t}\n\n\tv, err := stringInterfaceMapParser(rd)\n\tif err != nil {\n\t\tif err != proto.Nil {\n\t\t\treturn XMessage{}, err\n\t\t}\n\t}\n\n\tmsg := XMessage{\n\t\tID:     id,\n\t\tValues: v,\n\t}\n\n\tif n == 4 {\n\t\tmsg.MillisElapsedFromDelivery, err = rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn XMessage{}, err\n\t\t}\n\n\t\tmsg.DeliveredCount, err = rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn XMessage{}, err\n\t\t}\n\t}\n\n\treturn msg, nil\n}\n\nfunc stringInterfaceMapParser(rd *proto.Reader) (map[string]interface{}, error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tm := make(map[string]interface{}, n)\n\tfor i := 0; i < n; i++ {\n\t\tkey, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvalue, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tm[key] = value\n\t}\n\treturn m, nil\n}\n\n//------------------------------------------------------------------------------\n\ntype XStream struct {\n\tStream   string\n\tMessages []XMessage\n}\n\ntype XStreamSliceCmd struct {\n\tbaseCmd\n\n\tval []XStream\n}\n\nvar _ Cmder = (*XStreamSliceCmd)(nil)\n\nfunc NewXStreamSliceCmd(ctx context.Context, args ...interface{}) *XStreamSliceCmd {\n\treturn &XStreamSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeXStreamSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *XStreamSliceCmd) SetVal(val []XStream) {\n\tcmd.val = val\n}\n\nfunc (cmd *XStreamSliceCmd) Val() []XStream {\n\treturn cmd.val\n}\n\nfunc (cmd *XStreamSliceCmd) Result() ([]XStream, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XStreamSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XStreamSliceCmd) readReply(rd *proto.Reader) error {\n\ttyp, err := rd.PeekReplyType()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar n int\n\tif typ == proto.RespMap {\n\t\tn, err = rd.ReadMapLen()\n\t} else {\n\t\tn, err = rd.ReadArrayLen()\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]XStream, n)\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tif typ != proto.RespMap {\n\t\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif cmd.val[i].Stream, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif cmd.val[i].Messages, err = readXMessageSlice(rd); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *XStreamSliceCmd) Clone() Cmder {\n\tvar val []XStream\n\tif cmd.val != nil {\n\t\tval = make([]XStream, len(cmd.val))\n\t\tfor i, stream := range cmd.val {\n\t\t\tval[i] = XStream{\n\t\t\t\tStream: stream.Stream,\n\t\t\t}\n\t\t\tif stream.Messages != nil {\n\t\t\t\tval[i].Messages = make([]XMessage, len(stream.Messages))\n\t\t\t\tfor j, msg := range stream.Messages {\n\t\t\t\t\tval[i].Messages[j] = XMessage{\n\t\t\t\t\t\tID: msg.ID,\n\t\t\t\t\t}\n\t\t\t\t\tif msg.Values != nil {\n\t\t\t\t\t\tval[i].Messages[j].Values = make(map[string]interface{}, len(msg.Values))\n\t\t\t\t\t\tfor k, v := range msg.Values {\n\t\t\t\t\t\t\tval[i].Messages[j].Values[k] = v\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &XStreamSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XPending struct {\n\tCount     int64\n\tLower     string\n\tHigher    string\n\tConsumers map[string]int64\n}\n\ntype XPendingCmd struct {\n\tbaseCmd\n\tval *XPending\n}\n\nvar _ Cmder = (*XPendingCmd)(nil)\n\nfunc NewXPendingCmd(ctx context.Context, args ...interface{}) *XPendingCmd {\n\treturn &XPendingCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeXPending,\n\t\t},\n\t}\n}\n\nfunc (cmd *XPendingCmd) SetVal(val *XPending) {\n\tcmd.val = val\n}\n\nfunc (cmd *XPendingCmd) Val() *XPending {\n\treturn cmd.val\n}\n\nfunc (cmd *XPendingCmd) Result() (*XPending, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XPendingCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XPendingCmd) readReply(rd *proto.Reader) error {\n\tvar err error\n\tif err = rd.ReadFixedArrayLen(4); err != nil {\n\t\treturn err\n\t}\n\tcmd.val = &XPending{}\n\n\tif cmd.val.Count, err = rd.ReadInt(); err != nil {\n\t\treturn err\n\t}\n\n\tif cmd.val.Lower, err = rd.ReadString(); err != nil && err != Nil {\n\t\treturn err\n\t}\n\n\tif cmd.val.Higher, err = rd.ReadString(); err != nil && err != Nil {\n\t\treturn err\n\t}\n\n\tn, err := rd.ReadArrayLen()\n\tif err != nil && err != Nil {\n\t\treturn err\n\t}\n\tcmd.val.Consumers = make(map[string]int64, n)\n\tfor i := 0; i < n; i++ {\n\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tconsumerName, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconsumerPending, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val.Consumers[consumerName] = consumerPending\n\t}\n\treturn nil\n}\n\nfunc (cmd *XPendingCmd) Clone() Cmder {\n\tvar val *XPending\n\tif cmd.val != nil {\n\t\tval = &XPending{\n\t\t\tCount:  cmd.val.Count,\n\t\t\tLower:  cmd.val.Lower,\n\t\t\tHigher: cmd.val.Higher,\n\t\t}\n\t\tif cmd.val.Consumers != nil {\n\t\t\tval.Consumers = make(map[string]int64, len(cmd.val.Consumers))\n\t\t\tfor k, v := range cmd.val.Consumers {\n\t\t\t\tval.Consumers[k] = v\n\t\t\t}\n\t\t}\n\t}\n\treturn &XPendingCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XPendingExt struct {\n\tID         string\n\tConsumer   string\n\tIdle       time.Duration\n\tRetryCount int64\n}\n\ntype XPendingExtCmd struct {\n\tbaseCmd\n\tval []XPendingExt\n}\n\nvar _ Cmder = (*XPendingExtCmd)(nil)\n\nfunc NewXPendingExtCmd(ctx context.Context, args ...interface{}) *XPendingExtCmd {\n\treturn &XPendingExtCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeXPendingExt,\n\t\t},\n\t}\n}\n\nfunc (cmd *XPendingExtCmd) SetVal(val []XPendingExt) {\n\tcmd.val = val\n}\n\nfunc (cmd *XPendingExtCmd) Val() []XPendingExt {\n\treturn cmd.val\n}\n\nfunc (cmd *XPendingExtCmd) Result() ([]XPendingExt, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XPendingExtCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XPendingExtCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]XPendingExt, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tif err = rd.ReadFixedArrayLen(4); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cmd.val[i].ID, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cmd.val[i].Consumer, err = rd.ReadString(); err != nil && err != Nil {\n\t\t\treturn err\n\t\t}\n\n\t\tidle, err := rd.ReadInt()\n\t\tif err != nil && err != Nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Idle = time.Duration(idle) * time.Millisecond\n\n\t\tif cmd.val[i].RetryCount, err = rd.ReadInt(); err != nil && err != Nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *XPendingExtCmd) Clone() Cmder {\n\tvar val []XPendingExt\n\tif cmd.val != nil {\n\t\tval = make([]XPendingExt, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &XPendingExtCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XAutoClaimCmd struct {\n\tbaseCmd\n\n\tstart string\n\tval   []XMessage\n}\n\nvar _ Cmder = (*XAutoClaimCmd)(nil)\n\nfunc NewXAutoClaimCmd(ctx context.Context, args ...interface{}) *XAutoClaimCmd {\n\treturn &XAutoClaimCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeXAutoClaim,\n\t\t},\n\t}\n}\n\nfunc (cmd *XAutoClaimCmd) SetVal(val []XMessage, start string) {\n\tcmd.val = val\n\tcmd.start = start\n}\n\nfunc (cmd *XAutoClaimCmd) Val() (messages []XMessage, start string) {\n\treturn cmd.val, cmd.start\n}\n\nfunc (cmd *XAutoClaimCmd) Result() (messages []XMessage, start string, err error) {\n\treturn cmd.val, cmd.start, cmd.err\n}\n\nfunc (cmd *XAutoClaimCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XAutoClaimCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch n {\n\tcase 2, // Redis 6\n\t\t3: // Redis 7:\n\t\t// ok\n\tdefault:\n\t\treturn fmt.Errorf(\"redis: got %d elements in XAutoClaim reply, wanted 2/3\", n)\n\t}\n\n\tcmd.start, err = rd.ReadString()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val, err = readXMessageSlice(rd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif n >= 3 {\n\t\tif err := rd.DiscardNext(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *XAutoClaimCmd) Clone() Cmder {\n\tvar val []XMessage\n\tif cmd.val != nil {\n\t\tval = make([]XMessage, len(cmd.val))\n\t\tfor i, msg := range cmd.val {\n\t\t\tval[i] = XMessage{\n\t\t\t\tID: msg.ID,\n\t\t\t}\n\t\t\tif msg.Values != nil {\n\t\t\t\tval[i].Values = make(map[string]interface{}, len(msg.Values))\n\t\t\t\tfor k, v := range msg.Values {\n\t\t\t\t\tval[i].Values[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &XAutoClaimCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tstart:   cmd.start,\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XAutoClaimJustIDCmd struct {\n\tbaseCmd\n\n\tstart string\n\tval   []string\n}\n\nvar _ Cmder = (*XAutoClaimJustIDCmd)(nil)\n\nfunc NewXAutoClaimJustIDCmd(ctx context.Context, args ...interface{}) *XAutoClaimJustIDCmd {\n\treturn &XAutoClaimJustIDCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeXAutoClaimJustID,\n\t\t},\n\t}\n}\n\nfunc (cmd *XAutoClaimJustIDCmd) SetVal(val []string, start string) {\n\tcmd.val = val\n\tcmd.start = start\n}\n\nfunc (cmd *XAutoClaimJustIDCmd) Val() (ids []string, start string) {\n\treturn cmd.val, cmd.start\n}\n\nfunc (cmd *XAutoClaimJustIDCmd) Result() (ids []string, start string, err error) {\n\treturn cmd.val, cmd.start, cmd.err\n}\n\nfunc (cmd *XAutoClaimJustIDCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch n {\n\tcase 2, // Redis 6\n\t\t3: // Redis 7:\n\t\t// ok\n\tdefault:\n\t\treturn fmt.Errorf(\"redis: got %d elements in XAutoClaimJustID reply, wanted 2/3\", n)\n\t}\n\n\tcmd.start, err = rd.ReadString()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make([]string, nn)\n\tfor i := 0; i < nn; i++ {\n\t\tcmd.val[i], err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif n >= 3 {\n\t\tif err := rd.DiscardNext(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *XAutoClaimJustIDCmd) Clone() Cmder {\n\tvar val []string\n\tif cmd.val != nil {\n\t\tval = make([]string, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &XAutoClaimJustIDCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tstart:   cmd.start,\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XInfoConsumersCmd struct {\n\tbaseCmd\n\tval []XInfoConsumer\n}\n\ntype XInfoConsumer struct {\n\tName     string\n\tPending  int64\n\tIdle     time.Duration\n\tInactive time.Duration\n}\n\nvar _ Cmder = (*XInfoConsumersCmd)(nil)\n\nfunc NewXInfoConsumersCmd(ctx context.Context, stream string, group string) *XInfoConsumersCmd {\n\treturn &XInfoConsumersCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    []interface{}{\"xinfo\", \"consumers\", stream, group},\n\t\t\tcmdType: CmdTypeXInfoConsumers,\n\t\t},\n\t}\n}\n\nfunc (cmd *XInfoConsumersCmd) SetVal(val []XInfoConsumer) {\n\tcmd.val = val\n}\n\nfunc (cmd *XInfoConsumersCmd) Val() []XInfoConsumer {\n\treturn cmd.val\n}\n\nfunc (cmd *XInfoConsumersCmd) Result() ([]XInfoConsumer, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XInfoConsumersCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XInfoConsumersCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]XInfoConsumer, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar key string\n\t\tfor f := 0; f < nn; f++ {\n\t\t\tkey, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"name\":\n\t\t\t\tcmd.val[i].Name, err = rd.ReadString()\n\t\t\tcase \"pending\":\n\t\t\t\tcmd.val[i].Pending, err = rd.ReadInt()\n\t\t\tcase \"idle\":\n\t\t\t\tvar idle int64\n\t\t\t\tidle, err = rd.ReadInt()\n\t\t\t\tcmd.val[i].Idle = time.Duration(idle) * time.Millisecond\n\t\t\tcase \"inactive\":\n\t\t\t\tvar inactive int64\n\t\t\t\tinactive, err = rd.ReadInt()\n\t\t\t\tcmd.val[i].Inactive = time.Duration(inactive) * time.Millisecond\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"redis: unexpected content %s in XINFO CONSUMERS reply\", key)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *XInfoConsumersCmd) Clone() Cmder {\n\tvar val []XInfoConsumer\n\tif cmd.val != nil {\n\t\tval = make([]XInfoConsumer, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &XInfoConsumersCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XInfoGroupsCmd struct {\n\tbaseCmd\n\tval []XInfoGroup\n}\n\ntype XInfoGroup struct {\n\tName            string\n\tConsumers       int64\n\tPending         int64\n\tLastDeliveredID string\n\tEntriesRead     int64\n\t// Lag represents the number of pending messages in the stream not yet\n\t// delivered to this consumer group. Returns -1 when the lag cannot be determined.\n\tLag int64\n}\n\nvar _ Cmder = (*XInfoGroupsCmd)(nil)\n\nfunc NewXInfoGroupsCmd(ctx context.Context, stream string) *XInfoGroupsCmd {\n\treturn &XInfoGroupsCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    []interface{}{\"xinfo\", \"groups\", stream},\n\t\t\tcmdType: CmdTypeXInfoGroups,\n\t\t},\n\t}\n}\n\nfunc (cmd *XInfoGroupsCmd) SetVal(val []XInfoGroup) {\n\tcmd.val = val\n}\n\nfunc (cmd *XInfoGroupsCmd) Val() []XInfoGroup {\n\treturn cmd.val\n}\n\nfunc (cmd *XInfoGroupsCmd) Result() ([]XInfoGroup, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XInfoGroupsCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]XInfoGroup, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tgroup := &cmd.val[i]\n\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar key string\n\t\tfor j := 0; j < nn; j++ {\n\t\t\tkey, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"name\":\n\t\t\t\tgroup.Name, err = rd.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase \"consumers\":\n\t\t\t\tgroup.Consumers, err = rd.ReadInt()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase \"pending\":\n\t\t\t\tgroup.Pending, err = rd.ReadInt()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase \"last-delivered-id\":\n\t\t\t\tgroup.LastDeliveredID, err = rd.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase \"entries-read\":\n\t\t\t\tgroup.EntriesRead, err = rd.ReadInt()\n\t\t\t\tif err != nil && err != Nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase \"lag\":\n\t\t\t\tgroup.Lag, err = rd.ReadInt()\n\n\t\t\t\t// lag: the number of entries in the stream that are still waiting to be delivered\n\t\t\t\t// to the group's consumers, or a NULL(Nil) when that number can't be determined.\n\t\t\t\t// In that case, we return -1.\n\t\t\t\tif err != nil && err != Nil {\n\t\t\t\t\treturn err\n\t\t\t\t} else if err == Nil {\n\t\t\t\t\tgroup.Lag = -1\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"redis: unexpected key %q in XINFO GROUPS reply\", key)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *XInfoGroupsCmd) Clone() Cmder {\n\tvar val []XInfoGroup\n\tif cmd.val != nil {\n\t\tval = make([]XInfoGroup, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &XInfoGroupsCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XInfoStreamCmd struct {\n\tbaseCmd\n\tval *XInfoStream\n}\n\ntype XInfoStream struct {\n\tLength               int64\n\tRadixTreeKeys        int64\n\tRadixTreeNodes       int64\n\tGroups               int64\n\tLastGeneratedID      string\n\tMaxDeletedEntryID    string\n\tEntriesAdded         int64\n\tFirstEntry           XMessage\n\tLastEntry            XMessage\n\tRecordedFirstEntryID string\n\n\tIDMPDuration   int64\n\tIDMPMaxSize    int64\n\tPIDsTracked    int64\n\tIIDsTracked    int64\n\tIIDsAdded      int64\n\tIIDsDuplicates int64\n}\n\nvar _ Cmder = (*XInfoStreamCmd)(nil)\n\nfunc NewXInfoStreamCmd(ctx context.Context, stream string) *XInfoStreamCmd {\n\treturn &XInfoStreamCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    []interface{}{\"xinfo\", \"stream\", stream},\n\t\t\tcmdType: CmdTypeXInfoStream,\n\t\t},\n\t}\n}\n\nfunc (cmd *XInfoStreamCmd) SetVal(val *XInfoStream) {\n\tcmd.val = val\n}\n\nfunc (cmd *XInfoStreamCmd) Val() *XInfoStream {\n\treturn cmd.val\n}\n\nfunc (cmd *XInfoStreamCmd) Result() (*XInfoStream, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XInfoStreamCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XInfoStreamCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = &XInfoStream{}\n\n\tfor i := 0; i < n; i++ {\n\t\tkey, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch key {\n\t\tcase \"length\":\n\t\t\tcmd.val.Length, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"radix-tree-keys\":\n\t\t\tcmd.val.RadixTreeKeys, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"radix-tree-nodes\":\n\t\t\tcmd.val.RadixTreeNodes, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"groups\":\n\t\t\tcmd.val.Groups, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"last-generated-id\":\n\t\t\tcmd.val.LastGeneratedID, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"max-deleted-entry-id\":\n\t\t\tcmd.val.MaxDeletedEntryID, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"entries-added\":\n\t\t\tcmd.val.EntriesAdded, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"first-entry\":\n\t\t\tcmd.val.FirstEntry, err = readXMessage(rd)\n\t\t\tif err != nil && err != Nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"last-entry\":\n\t\t\tcmd.val.LastEntry, err = readXMessage(rd)\n\t\t\tif err != nil && err != Nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"recorded-first-entry-id\":\n\t\t\tcmd.val.RecordedFirstEntryID, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"idmp-duration\":\n\t\t\tcmd.val.IDMPDuration, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"idmp-maxsize\":\n\t\t\tcmd.val.IDMPMaxSize, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"pids-tracked\":\n\t\t\tcmd.val.PIDsTracked, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"iids-tracked\":\n\t\t\tcmd.val.IIDsTracked, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"iids-added\":\n\t\t\tcmd.val.IIDsAdded, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"iids-duplicates\":\n\t\t\tcmd.val.IIDsDuplicates, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: unexpected key %q in XINFO STREAM reply\", key)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *XInfoStreamCmd) Clone() Cmder {\n\tvar val *XInfoStream\n\tif cmd.val != nil {\n\t\tval = &XInfoStream{\n\t\t\tLength:               cmd.val.Length,\n\t\t\tRadixTreeKeys:        cmd.val.RadixTreeKeys,\n\t\t\tRadixTreeNodes:       cmd.val.RadixTreeNodes,\n\t\t\tGroups:               cmd.val.Groups,\n\t\t\tLastGeneratedID:      cmd.val.LastGeneratedID,\n\t\t\tMaxDeletedEntryID:    cmd.val.MaxDeletedEntryID,\n\t\t\tEntriesAdded:         cmd.val.EntriesAdded,\n\t\t\tRecordedFirstEntryID: cmd.val.RecordedFirstEntryID,\n\t\t}\n\t\t// Clone XMessage fields\n\t\tval.FirstEntry = XMessage{\n\t\t\tID: cmd.val.FirstEntry.ID,\n\t\t}\n\t\tif cmd.val.FirstEntry.Values != nil {\n\t\t\tval.FirstEntry.Values = make(map[string]interface{}, len(cmd.val.FirstEntry.Values))\n\t\t\tfor k, v := range cmd.val.FirstEntry.Values {\n\t\t\t\tval.FirstEntry.Values[k] = v\n\t\t\t}\n\t\t}\n\t\tval.LastEntry = XMessage{\n\t\t\tID: cmd.val.LastEntry.ID,\n\t\t}\n\t\tif cmd.val.LastEntry.Values != nil {\n\t\t\tval.LastEntry.Values = make(map[string]interface{}, len(cmd.val.LastEntry.Values))\n\t\t\tfor k, v := range cmd.val.LastEntry.Values {\n\t\t\t\tval.LastEntry.Values[k] = v\n\t\t\t}\n\t\t}\n\t}\n\treturn &XInfoStreamCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype XInfoStreamFullCmd struct {\n\tbaseCmd\n\tval *XInfoStreamFull\n}\n\ntype XInfoStreamFull struct {\n\tLength               int64\n\tRadixTreeKeys        int64\n\tRadixTreeNodes       int64\n\tLastGeneratedID      string\n\tMaxDeletedEntryID    string\n\tEntriesAdded         int64\n\tEntries              []XMessage\n\tGroups               []XInfoStreamGroup\n\tRecordedFirstEntryID string\n\tIDMPDuration         int64\n\tIDMPMaxSize          int64\n\tPIDsTracked          int64\n\tIIDsTracked          int64\n\tIIDsAdded            int64\n\tIIDsDuplicates       int64\n}\n\ntype XInfoStreamGroup struct {\n\tName            string\n\tLastDeliveredID string\n\tEntriesRead     int64\n\tLag             int64\n\tPelCount        int64\n\tPending         []XInfoStreamGroupPending\n\tConsumers       []XInfoStreamConsumer\n}\n\ntype XInfoStreamGroupPending struct {\n\tID            string\n\tConsumer      string\n\tDeliveryTime  time.Time\n\tDeliveryCount int64\n}\n\ntype XInfoStreamConsumer struct {\n\tName       string\n\tSeenTime   time.Time\n\tActiveTime time.Time\n\tPelCount   int64\n\tPending    []XInfoStreamConsumerPending\n}\n\ntype XInfoStreamConsumerPending struct {\n\tID            string\n\tDeliveryTime  time.Time\n\tDeliveryCount int64\n}\n\nvar _ Cmder = (*XInfoStreamFullCmd)(nil)\n\nfunc NewXInfoStreamFullCmd(ctx context.Context, args ...interface{}) *XInfoStreamFullCmd {\n\treturn &XInfoStreamFullCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeXInfoStreamFull,\n\t\t},\n\t}\n}\n\nfunc (cmd *XInfoStreamFullCmd) SetVal(val *XInfoStreamFull) {\n\tcmd.val = val\n}\n\nfunc (cmd *XInfoStreamFullCmd) Val() *XInfoStreamFull {\n\treturn cmd.val\n}\n\nfunc (cmd *XInfoStreamFullCmd) Result() (*XInfoStreamFull, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *XInfoStreamFullCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *XInfoStreamFullCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = &XInfoStreamFull{}\n\n\tfor i := 0; i < n; i++ {\n\t\tkey, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"length\":\n\t\t\tcmd.val.Length, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"radix-tree-keys\":\n\t\t\tcmd.val.RadixTreeKeys, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"radix-tree-nodes\":\n\t\t\tcmd.val.RadixTreeNodes, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"last-generated-id\":\n\t\t\tcmd.val.LastGeneratedID, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"entries-added\":\n\t\t\tcmd.val.EntriesAdded, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"entries\":\n\t\t\tcmd.val.Entries, err = readXMessageSlice(rd)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"groups\":\n\t\t\tcmd.val.Groups, err = readStreamGroups(rd)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"max-deleted-entry-id\":\n\t\t\tcmd.val.MaxDeletedEntryID, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"recorded-first-entry-id\":\n\t\t\tcmd.val.RecordedFirstEntryID, err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"idmp-duration\":\n\t\t\tcmd.val.IDMPDuration, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"idmp-maxsize\":\n\t\t\tcmd.val.IDMPMaxSize, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"pids-tracked\":\n\t\t\tcmd.val.PIDsTracked, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"iids-tracked\":\n\t\t\tcmd.val.IIDsTracked, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"iids-added\":\n\t\t\tcmd.val.IIDsAdded, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"iids-duplicates\":\n\t\t\tcmd.val.IIDsDuplicates, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: unexpected key %q in XINFO STREAM FULL reply\", key)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc readStreamGroups(rd *proto.Reader) ([]XInfoStreamGroup, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroups := make([]XInfoStreamGroup, 0, n)\n\tfor i := 0; i < n; i++ {\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tgroup := XInfoStreamGroup{}\n\n\t\tfor j := 0; j < nn; j++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"name\":\n\t\t\t\tgroup.Name, err = rd.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"last-delivered-id\":\n\t\t\t\tgroup.LastDeliveredID, err = rd.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"entries-read\":\n\t\t\t\tgroup.EntriesRead, err = rd.ReadInt()\n\t\t\t\tif err != nil && err != Nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"lag\":\n\t\t\t\t// lag: the number of entries in the stream that are still waiting to be delivered\n\t\t\t\t// to the group's consumers, or a NULL(Nil) when that number can't be determined.\n\t\t\t\tgroup.Lag, err = rd.ReadInt()\n\t\t\t\tif err != nil && err != Nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"pel-count\":\n\t\t\t\tgroup.PelCount, err = rd.ReadInt()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"pending\":\n\t\t\t\tgroup.Pending, err = readXInfoStreamGroupPending(rd)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"consumers\":\n\t\t\t\tgroup.Consumers, err = readXInfoStreamConsumers(rd)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"redis: unexpected key %q in XINFO STREAM FULL reply\", key)\n\t\t\t}\n\t\t}\n\n\t\tgroups = append(groups, group)\n\t}\n\n\treturn groups, nil\n}\n\nfunc readXInfoStreamGroupPending(rd *proto.Reader) ([]XInfoStreamGroupPending, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpending := make([]XInfoStreamGroupPending, 0, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tif err = rd.ReadFixedArrayLen(4); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tp := XInfoStreamGroupPending{}\n\n\t\tp.ID, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tp.Consumer, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdelivery, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.DeliveryTime = time.Unix(delivery/1000, delivery%1000*int64(time.Millisecond))\n\n\t\tp.DeliveryCount, err = rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tpending = append(pending, p)\n\t}\n\n\treturn pending, nil\n}\n\nfunc readXInfoStreamConsumers(rd *proto.Reader) ([]XInfoStreamConsumer, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconsumers := make([]XInfoStreamConsumer, 0, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tc := XInfoStreamConsumer{}\n\n\t\tfor f := 0; f < nn; f++ {\n\t\t\tcKey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tswitch cKey {\n\t\t\tcase \"name\":\n\t\t\t\tc.Name, err = rd.ReadString()\n\t\t\tcase \"seen-time\":\n\t\t\t\tseen, err := rd.ReadInt()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tc.SeenTime = time.UnixMilli(seen)\n\t\t\tcase \"active-time\":\n\t\t\t\tactive, err := rd.ReadInt()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tc.ActiveTime = time.UnixMilli(active)\n\t\t\tcase \"pel-count\":\n\t\t\t\tc.PelCount, err = rd.ReadInt()\n\t\t\tcase \"pending\":\n\t\t\t\tpendingNumber, err := rd.ReadArrayLen()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tc.Pending = make([]XInfoStreamConsumerPending, 0, pendingNumber)\n\n\t\t\t\tfor pn := 0; pn < pendingNumber; pn++ {\n\t\t\t\t\tif err = rd.ReadFixedArrayLen(3); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tp := XInfoStreamConsumerPending{}\n\n\t\t\t\t\tp.ID, err = rd.ReadString()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tdelivery, err := rd.ReadInt()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tp.DeliveryTime = time.Unix(delivery/1000, delivery%1000*int64(time.Millisecond))\n\n\t\t\t\t\tp.DeliveryCount, err = rd.ReadInt()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tc.Pending = append(c.Pending, p)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"redis: unexpected content %s \"+\n\t\t\t\t\t\"in XINFO STREAM FULL reply\", cKey)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tconsumers = append(consumers, c)\n\t}\n\n\treturn consumers, nil\n}\n\nfunc (cmd *XInfoStreamFullCmd) Clone() Cmder {\n\tvar val *XInfoStreamFull\n\tif cmd.val != nil {\n\t\tval = &XInfoStreamFull{\n\t\t\tLength:               cmd.val.Length,\n\t\t\tRadixTreeKeys:        cmd.val.RadixTreeKeys,\n\t\t\tRadixTreeNodes:       cmd.val.RadixTreeNodes,\n\t\t\tLastGeneratedID:      cmd.val.LastGeneratedID,\n\t\t\tMaxDeletedEntryID:    cmd.val.MaxDeletedEntryID,\n\t\t\tEntriesAdded:         cmd.val.EntriesAdded,\n\t\t\tRecordedFirstEntryID: cmd.val.RecordedFirstEntryID,\n\t\t}\n\t\t// Clone Entries\n\t\tif cmd.val.Entries != nil {\n\t\t\tval.Entries = make([]XMessage, len(cmd.val.Entries))\n\t\t\tfor i, msg := range cmd.val.Entries {\n\t\t\t\tval.Entries[i] = XMessage{\n\t\t\t\t\tID: msg.ID,\n\t\t\t\t}\n\t\t\t\tif msg.Values != nil {\n\t\t\t\t\tval.Entries[i].Values = make(map[string]interface{}, len(msg.Values))\n\t\t\t\t\tfor k, v := range msg.Values {\n\t\t\t\t\t\tval.Entries[i].Values[k] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Clone Groups - simplified copy for now due to complexity\n\t\tif cmd.val.Groups != nil {\n\t\t\tval.Groups = make([]XInfoStreamGroup, len(cmd.val.Groups))\n\t\t\tcopy(val.Groups, cmd.val.Groups)\n\t\t}\n\t}\n\treturn &XInfoStreamFullCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype ZSliceCmd struct {\n\tbaseCmd\n\n\tval []Z\n}\n\nvar _ Cmder = (*ZSliceCmd)(nil)\n\nfunc NewZSliceCmd(ctx context.Context, args ...interface{}) *ZSliceCmd {\n\treturn &ZSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeZSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *ZSliceCmd) SetVal(val []Z) {\n\tcmd.val = val\n}\n\nfunc (cmd *ZSliceCmd) Val() []Z {\n\treturn cmd.val\n}\n\nfunc (cmd *ZSliceCmd) Result() ([]Z, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ZSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ZSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If the n is 0, can't continue reading.\n\tif n == 0 {\n\t\tcmd.val = make([]Z, 0)\n\t\treturn nil\n\t}\n\n\ttyp, err := rd.PeekReplyType()\n\tif err != nil {\n\t\treturn err\n\t}\n\tarray := typ == proto.RespArray\n\n\tif array {\n\t\tcmd.val = make([]Z, n)\n\t} else {\n\t\tcmd.val = make([]Z, n/2)\n\t}\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tif array {\n\t\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif cmd.val[i].Member, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cmd.val[i].Score, err = rd.ReadFloat(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ZSliceCmd) Clone() Cmder {\n\tvar val []Z\n\tif cmd.val != nil {\n\t\tval = make([]Z, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &ZSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype ZWithKeyCmd struct {\n\tbaseCmd\n\n\tval *ZWithKey\n}\n\nvar _ Cmder = (*ZWithKeyCmd)(nil)\n\nfunc NewZWithKeyCmd(ctx context.Context, args ...interface{}) *ZWithKeyCmd {\n\treturn &ZWithKeyCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeZWithKey,\n\t\t},\n\t}\n}\n\nfunc (cmd *ZWithKeyCmd) SetVal(val *ZWithKey) {\n\tcmd.val = val\n}\n\nfunc (cmd *ZWithKeyCmd) Val() *ZWithKey {\n\treturn cmd.val\n}\n\nfunc (cmd *ZWithKeyCmd) Result() (*ZWithKey, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ZWithKeyCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ZWithKeyCmd) readReply(rd *proto.Reader) (err error) {\n\tif err = rd.ReadFixedArrayLen(3); err != nil {\n\t\treturn err\n\t}\n\tcmd.val = &ZWithKey{}\n\n\tif cmd.val.Key, err = rd.ReadString(); err != nil {\n\t\treturn err\n\t}\n\tif cmd.val.Member, err = rd.ReadString(); err != nil {\n\t\treturn err\n\t}\n\tif cmd.val.Score, err = rd.ReadFloat(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ZWithKeyCmd) Clone() Cmder {\n\tvar val *ZWithKey\n\tif cmd.val != nil {\n\t\tval = &ZWithKey{\n\t\t\tZ: Z{\n\t\t\t\tScore:  cmd.val.Score,\n\t\t\t\tMember: cmd.val.Member,\n\t\t\t},\n\t\t\tKey: cmd.val.Key,\n\t\t}\n\t}\n\treturn &ZWithKeyCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype ScanCmd struct {\n\tbaseCmd\n\n\tpage   []string\n\tcursor uint64\n\n\tprocess cmdable\n}\n\nvar _ Cmder = (*ScanCmd)(nil)\n\nfunc NewScanCmd(ctx context.Context, process cmdable, args ...interface{}) *ScanCmd {\n\treturn &ScanCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeScan,\n\t\t},\n\t\tprocess: process,\n\t}\n}\n\nfunc (cmd *ScanCmd) SetVal(page []string, cursor uint64) {\n\tcmd.page = page\n\tcmd.cursor = cursor\n}\n\nfunc (cmd *ScanCmd) Val() (keys []string, cursor uint64) {\n\treturn cmd.page, cmd.cursor\n}\n\nfunc (cmd *ScanCmd) Result() (keys []string, cursor uint64, err error) {\n\treturn cmd.page, cmd.cursor, cmd.err\n}\n\nfunc (cmd *ScanCmd) String() string {\n\treturn cmdString(cmd, cmd.page)\n}\n\nfunc (cmd *ScanCmd) readReply(rd *proto.Reader) error {\n\tif err := rd.ReadFixedArrayLen(2); err != nil {\n\t\treturn err\n\t}\n\n\tcursor, err := rd.ReadUint()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.cursor = cursor\n\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.page = make([]string, n)\n\n\tfor i := 0; i < len(cmd.page); i++ {\n\t\tif cmd.page[i], err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *ScanCmd) Clone() Cmder {\n\tvar page []string\n\tif cmd.page != nil {\n\t\tpage = make([]string, len(cmd.page))\n\t\tcopy(page, cmd.page)\n\t}\n\treturn &ScanCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tpage:    page,\n\t\tcursor:  cmd.cursor,\n\t\tprocess: cmd.process,\n\t}\n}\n\n// Iterator creates a new ScanIterator.\nfunc (cmd *ScanCmd) Iterator() *ScanIterator {\n\treturn &ScanIterator{\n\t\tcmd: cmd,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype ClusterNode struct {\n\tID                 string\n\tAddr               string\n\tNetworkingMetadata map[string]string\n}\n\ntype ClusterSlot struct {\n\tStart int\n\tEnd   int\n\tNodes []ClusterNode\n}\n\ntype ClusterSlotsCmd struct {\n\tbaseCmd\n\n\tval []ClusterSlot\n}\n\nvar _ Cmder = (*ClusterSlotsCmd)(nil)\n\nfunc NewClusterSlotsCmd(ctx context.Context, args ...interface{}) *ClusterSlotsCmd {\n\treturn &ClusterSlotsCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeClusterSlots,\n\t\t},\n\t}\n}\n\nfunc (cmd *ClusterSlotsCmd) SetVal(val []ClusterSlot) {\n\tcmd.val = val\n}\n\nfunc (cmd *ClusterSlotsCmd) Val() []ClusterSlot {\n\treturn cmd.val\n}\n\nfunc (cmd *ClusterSlotsCmd) Result() ([]ClusterSlot, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ClusterSlotsCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ClusterSlotsCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]ClusterSlot, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tn, err = rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif n < 2 {\n\t\t\treturn fmt.Errorf(\"redis: got %d elements in cluster info, expected at least 2\", n)\n\t\t}\n\n\t\tstart, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tend, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// subtract start and end.\n\t\tnodes := make([]ClusterNode, n-2)\n\n\t\tfor j := 0; j < len(nodes); j++ {\n\t\t\tnn, err := rd.ReadArrayLen()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif nn < 2 || nn > 4 {\n\t\t\t\treturn fmt.Errorf(\"got %d elements in cluster info address, expected 2, 3, or 4\", n)\n\t\t\t}\n\n\t\t\tip, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tport, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tnodes[j].Addr = net.JoinHostPort(ip, port)\n\n\t\t\tif nn >= 3 {\n\t\t\t\tid, err := rd.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tnodes[j].ID = id\n\t\t\t}\n\n\t\t\tif nn >= 4 {\n\t\t\t\tmetadataLength, err := rd.ReadMapLen()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tnetworkingMetadata := make(map[string]string, metadataLength)\n\n\t\t\t\tfor i := 0; i < metadataLength; i++ {\n\t\t\t\t\tkey, err := rd.ReadString()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tvalue, err := rd.ReadString()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tnetworkingMetadata[key] = value\n\t\t\t\t}\n\n\t\t\t\tnodes[j].NetworkingMetadata = networkingMetadata\n\t\t\t}\n\t\t}\n\n\t\tcmd.val[i] = ClusterSlot{\n\t\t\tStart: int(start),\n\t\t\tEnd:   int(end),\n\t\t\tNodes: nodes,\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ClusterSlotsCmd) Clone() Cmder {\n\tvar val []ClusterSlot\n\tif cmd.val != nil {\n\t\tval = make([]ClusterSlot, len(cmd.val))\n\t\tfor i, slot := range cmd.val {\n\t\t\tval[i] = ClusterSlot{\n\t\t\t\tStart: slot.Start,\n\t\t\t\tEnd:   slot.End,\n\t\t\t}\n\t\t\tif slot.Nodes != nil {\n\t\t\t\tval[i].Nodes = make([]ClusterNode, len(slot.Nodes))\n\t\t\t\tfor j, node := range slot.Nodes {\n\t\t\t\t\tval[i].Nodes[j] = ClusterNode{\n\t\t\t\t\t\tID:   node.ID,\n\t\t\t\t\t\tAddr: node.Addr,\n\t\t\t\t\t}\n\t\t\t\t\tif node.NetworkingMetadata != nil {\n\t\t\t\t\t\tval[i].Nodes[j].NetworkingMetadata = make(map[string]string, len(node.NetworkingMetadata))\n\t\t\t\t\t\tfor k, v := range node.NetworkingMetadata {\n\t\t\t\t\t\t\tval[i].Nodes[j].NetworkingMetadata[k] = v\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &ClusterSlotsCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\n// GeoLocation is used with GeoAdd to add geospatial location.\ntype GeoLocation struct {\n\tName                      string\n\tLongitude, Latitude, Dist float64\n\tGeoHash                   int64\n}\n\n// GeoRadiusQuery is used with GeoRadius to query geospatial index.\ntype GeoRadiusQuery struct {\n\tRadius float64\n\t// Can be m, km, ft, or mi. Default is km.\n\tUnit        string\n\tWithCoord   bool\n\tWithDist    bool\n\tWithGeoHash bool\n\tCount       int\n\t// Can be ASC or DESC. Default is no sort order.\n\tSort      string\n\tStore     string\n\tStoreDist string\n\n\t// WithCoord+WithDist+WithGeoHash\n\twithLen int\n}\n\ntype GeoLocationCmd struct {\n\tbaseCmd\n\n\tq         *GeoRadiusQuery\n\tlocations []GeoLocation\n}\n\nvar _ Cmder = (*GeoLocationCmd)(nil)\n\nfunc NewGeoLocationCmd(ctx context.Context, q *GeoRadiusQuery, args ...interface{}) *GeoLocationCmd {\n\treturn &GeoLocationCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    geoLocationArgs(q, args...),\n\t\t\tcmdType: CmdTypeGeoLocation,\n\t\t},\n\t\tq: q,\n\t}\n}\n\nfunc geoLocationArgs(q *GeoRadiusQuery, args ...interface{}) []interface{} {\n\targs = append(args, q.Radius)\n\tif q.Unit != \"\" {\n\t\targs = append(args, q.Unit)\n\t} else {\n\t\targs = append(args, \"km\")\n\t}\n\tif q.WithCoord {\n\t\targs = append(args, \"withcoord\")\n\t\tq.withLen++\n\t}\n\tif q.WithDist {\n\t\targs = append(args, \"withdist\")\n\t\tq.withLen++\n\t}\n\tif q.WithGeoHash {\n\t\targs = append(args, \"withhash\")\n\t\tq.withLen++\n\t}\n\tif q.Count > 0 {\n\t\targs = append(args, \"count\", q.Count)\n\t}\n\tif q.Sort != \"\" {\n\t\targs = append(args, q.Sort)\n\t}\n\tif q.Store != \"\" {\n\t\targs = append(args, \"store\")\n\t\targs = append(args, q.Store)\n\t}\n\tif q.StoreDist != \"\" {\n\t\targs = append(args, \"storedist\")\n\t\targs = append(args, q.StoreDist)\n\t}\n\treturn args\n}\n\nfunc (cmd *GeoLocationCmd) SetVal(locations []GeoLocation) {\n\tcmd.locations = locations\n}\n\nfunc (cmd *GeoLocationCmd) Val() []GeoLocation {\n\treturn cmd.locations\n}\n\nfunc (cmd *GeoLocationCmd) Result() ([]GeoLocation, error) {\n\treturn cmd.locations, cmd.err\n}\n\nfunc (cmd *GeoLocationCmd) String() string {\n\treturn cmdString(cmd, cmd.locations)\n}\n\nfunc (cmd *GeoLocationCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.locations = make([]GeoLocation, n)\n\n\tfor i := 0; i < len(cmd.locations); i++ {\n\t\t// only name\n\t\tif cmd.q.withLen == 0 {\n\t\t\tif cmd.locations[i].Name, err = rd.ReadString(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// +name\n\t\tif err = rd.ReadFixedArrayLen(cmd.q.withLen + 1); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cmd.locations[i].Name, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif cmd.q.WithDist {\n\t\t\tif cmd.locations[i].Dist, err = rd.ReadFloat(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif cmd.q.WithGeoHash {\n\t\t\tif cmd.locations[i].GeoHash, err = rd.ReadInt(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif cmd.q.WithCoord {\n\t\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif cmd.locations[i].Longitude, err = rd.ReadFloat(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif cmd.locations[i].Latitude, err = rd.ReadFloat(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *GeoLocationCmd) Clone() Cmder {\n\tvar q *GeoRadiusQuery\n\tif cmd.q != nil {\n\t\tq = &GeoRadiusQuery{\n\t\t\tRadius:      cmd.q.Radius,\n\t\t\tUnit:        cmd.q.Unit,\n\t\t\tWithCoord:   cmd.q.WithCoord,\n\t\t\tWithDist:    cmd.q.WithDist,\n\t\t\tWithGeoHash: cmd.q.WithGeoHash,\n\t\t\tCount:       cmd.q.Count,\n\t\t\tSort:        cmd.q.Sort,\n\t\t\tStore:       cmd.q.Store,\n\t\t\tStoreDist:   cmd.q.StoreDist,\n\t\t\twithLen:     cmd.q.withLen,\n\t\t}\n\t}\n\tvar locations []GeoLocation\n\tif cmd.locations != nil {\n\t\tlocations = make([]GeoLocation, len(cmd.locations))\n\t\tcopy(locations, cmd.locations)\n\t}\n\treturn &GeoLocationCmd{\n\t\tbaseCmd:   cmd.cloneBaseCmd(),\n\t\tq:         q,\n\t\tlocations: locations,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\n// GeoSearchQuery is used for GEOSearch/GEOSearchStore command query.\ntype GeoSearchQuery struct {\n\tMember string\n\n\t// Latitude and Longitude when using FromLonLat option.\n\tLongitude float64\n\tLatitude  float64\n\n\t// Distance and unit when using ByRadius option.\n\t// Can use m, km, ft, or mi. Default is km.\n\tRadius     float64\n\tRadiusUnit string\n\n\t// Height, width and unit when using ByBox option.\n\t// Can be m, km, ft, or mi. Default is km.\n\tBoxWidth  float64\n\tBoxHeight float64\n\tBoxUnit   string\n\n\t// Can be ASC or DESC. Default is no sort order.\n\tSort     string\n\tCount    int\n\tCountAny bool\n}\n\ntype GeoSearchLocationQuery struct {\n\tGeoSearchQuery\n\n\tWithCoord bool\n\tWithDist  bool\n\tWithHash  bool\n}\n\ntype GeoSearchStoreQuery struct {\n\tGeoSearchQuery\n\n\t// When using the StoreDist option, the command stores the items in a\n\t// sorted set populated with their distance from the center of the circle or box,\n\t// as a floating-point number, in the same unit specified for that shape.\n\tStoreDist bool\n}\n\nfunc geoSearchLocationArgs(q *GeoSearchLocationQuery, args []interface{}) []interface{} {\n\targs = geoSearchArgs(&q.GeoSearchQuery, args)\n\n\tif q.WithCoord {\n\t\targs = append(args, \"withcoord\")\n\t}\n\tif q.WithDist {\n\t\targs = append(args, \"withdist\")\n\t}\n\tif q.WithHash {\n\t\targs = append(args, \"withhash\")\n\t}\n\n\treturn args\n}\n\nfunc geoSearchArgs(q *GeoSearchQuery, args []interface{}) []interface{} {\n\tif q.Member != \"\" {\n\t\targs = append(args, \"frommember\", q.Member)\n\t} else {\n\t\targs = append(args, \"fromlonlat\", q.Longitude, q.Latitude)\n\t}\n\n\tif q.Radius > 0 {\n\t\tif q.RadiusUnit == \"\" {\n\t\t\tq.RadiusUnit = \"km\"\n\t\t}\n\t\targs = append(args, \"byradius\", q.Radius, q.RadiusUnit)\n\t} else {\n\t\tif q.BoxUnit == \"\" {\n\t\t\tq.BoxUnit = \"km\"\n\t\t}\n\t\targs = append(args, \"bybox\", q.BoxWidth, q.BoxHeight, q.BoxUnit)\n\t}\n\n\tif q.Sort != \"\" {\n\t\targs = append(args, q.Sort)\n\t}\n\n\tif q.Count > 0 {\n\t\targs = append(args, \"count\", q.Count)\n\t\tif q.CountAny {\n\t\t\targs = append(args, \"any\")\n\t\t}\n\t}\n\n\treturn args\n}\n\ntype GeoSearchLocationCmd struct {\n\tbaseCmd\n\n\topt *GeoSearchLocationQuery\n\tval []GeoLocation\n}\n\nvar _ Cmder = (*GeoSearchLocationCmd)(nil)\n\nfunc NewGeoSearchLocationCmd(\n\tctx context.Context, opt *GeoSearchLocationQuery, args ...interface{},\n) *GeoSearchLocationCmd {\n\treturn &GeoSearchLocationCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    geoSearchLocationArgs(opt, args),\n\t\t\tcmdType: CmdTypeGeoSearchLocation,\n\t\t},\n\t\topt: opt,\n\t}\n}\n\nfunc (cmd *GeoSearchLocationCmd) SetVal(val []GeoLocation) {\n\tcmd.val = val\n}\n\nfunc (cmd *GeoSearchLocationCmd) Val() []GeoLocation {\n\treturn cmd.val\n}\n\nfunc (cmd *GeoSearchLocationCmd) Result() ([]GeoLocation, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *GeoSearchLocationCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *GeoSearchLocationCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make([]GeoLocation, n)\n\tfor i := 0; i < n; i++ {\n\t\t_, err = rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar loc GeoLocation\n\n\t\tloc.Name, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif cmd.opt.WithDist {\n\t\t\tloc.Dist, err = rd.ReadFloat()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif cmd.opt.WithHash {\n\t\t\tloc.GeoHash, err = rd.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif cmd.opt.WithCoord {\n\t\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tloc.Longitude, err = rd.ReadFloat()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tloc.Latitude, err = rd.ReadFloat()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tcmd.val[i] = loc\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *GeoSearchLocationCmd) Clone() Cmder {\n\tvar opt *GeoSearchLocationQuery\n\tif cmd.opt != nil {\n\t\topt = &GeoSearchLocationQuery{\n\t\t\tGeoSearchQuery: GeoSearchQuery{\n\t\t\t\tMember:     cmd.opt.Member,\n\t\t\t\tLongitude:  cmd.opt.Longitude,\n\t\t\t\tLatitude:   cmd.opt.Latitude,\n\t\t\t\tRadius:     cmd.opt.Radius,\n\t\t\t\tRadiusUnit: cmd.opt.RadiusUnit,\n\t\t\t\tBoxWidth:   cmd.opt.BoxWidth,\n\t\t\t\tBoxHeight:  cmd.opt.BoxHeight,\n\t\t\t\tBoxUnit:    cmd.opt.BoxUnit,\n\t\t\t\tSort:       cmd.opt.Sort,\n\t\t\t\tCount:      cmd.opt.Count,\n\t\t\t\tCountAny:   cmd.opt.CountAny,\n\t\t\t},\n\t\t\tWithCoord: cmd.opt.WithCoord,\n\t\t\tWithDist:  cmd.opt.WithDist,\n\t\t\tWithHash:  cmd.opt.WithHash,\n\t\t}\n\t}\n\tvar val []GeoLocation\n\tif cmd.val != nil {\n\t\tval = make([]GeoLocation, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &GeoSearchLocationCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\topt:     opt,\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype GeoPos struct {\n\tLongitude, Latitude float64\n}\n\ntype GeoPosCmd struct {\n\tbaseCmd\n\n\tval []*GeoPos\n}\n\nvar _ Cmder = (*GeoPosCmd)(nil)\n\nfunc NewGeoPosCmd(ctx context.Context, args ...interface{}) *GeoPosCmd {\n\treturn &GeoPosCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeGeoPos,\n\t\t},\n\t}\n}\n\nfunc (cmd *GeoPosCmd) SetVal(val []*GeoPos) {\n\tcmd.val = val\n}\n\nfunc (cmd *GeoPosCmd) Val() []*GeoPos {\n\treturn cmd.val\n}\n\nfunc (cmd *GeoPosCmd) Result() ([]*GeoPos, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *GeoPosCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *GeoPosCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]*GeoPos, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\terr = rd.ReadFixedArrayLen(2)\n\t\tif err != nil {\n\t\t\tif err == Nil {\n\t\t\t\tcmd.val[i] = nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tlongitude, err := rd.ReadFloat()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlatitude, err := rd.ReadFloat()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcmd.val[i] = &GeoPos{\n\t\t\tLongitude: longitude,\n\t\t\tLatitude:  latitude,\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *GeoPosCmd) Clone() Cmder {\n\tvar val []*GeoPos\n\tif cmd.val != nil {\n\t\tval = make([]*GeoPos, len(cmd.val))\n\t\tfor i, pos := range cmd.val {\n\t\t\tif pos != nil {\n\t\t\t\tval[i] = &GeoPos{\n\t\t\t\t\tLongitude: pos.Longitude,\n\t\t\t\t\tLatitude:  pos.Latitude,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &GeoPosCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype CommandInfo struct {\n\tName          string\n\tArity         int8\n\tFlags         []string\n\tACLFlags      []string\n\tFirstKeyPos   int8\n\tLastKeyPos    int8\n\tStepCount     int8\n\tReadOnly      bool\n\tCommandPolicy *routing.CommandPolicy\n}\n\ntype CommandsInfoCmd struct {\n\tbaseCmd\n\n\tval map[string]*CommandInfo\n}\n\nvar _ Cmder = (*CommandsInfoCmd)(nil)\n\nfunc NewCommandsInfoCmd(ctx context.Context, args ...interface{}) *CommandsInfoCmd {\n\treturn &CommandsInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeCommandsInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *CommandsInfoCmd) SetVal(val map[string]*CommandInfo) {\n\tcmd.val = val\n}\n\nfunc (cmd *CommandsInfoCmd) Val() map[string]*CommandInfo {\n\treturn cmd.val\n}\n\nfunc (cmd *CommandsInfoCmd) Result() (map[string]*CommandInfo, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *CommandsInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error {\n\tconst numArgRedis5 = 6\n\tconst numArgRedis6 = 7\n\tconst numArgRedis7 = 10 // Also matches redis 8\n\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make(map[string]*CommandInfo, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tnn, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch nn {\n\t\tcase numArgRedis5, numArgRedis6, numArgRedis7:\n\t\t\t// ok\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: got %d elements in COMMAND reply, wanted 6/7/10\", nn)\n\t\t}\n\n\t\tcmdInfo := &CommandInfo{}\n\t\tif cmdInfo.Name, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tarity, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmdInfo.Arity = int8(arity)\n\n\t\tflagLen, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmdInfo.Flags = make([]string, flagLen)\n\t\tfor f := 0; f < len(cmdInfo.Flags); f++ {\n\t\t\tswitch s, err := rd.ReadString(); {\n\t\t\tcase err == Nil:\n\t\t\t\tcmdInfo.Flags[f] = \"\"\n\t\t\tcase err != nil:\n\t\t\t\treturn err\n\t\t\tdefault:\n\t\t\t\tif !cmdInfo.ReadOnly && s == \"readonly\" {\n\t\t\t\t\tcmdInfo.ReadOnly = true\n\t\t\t\t}\n\t\t\t\tcmdInfo.Flags[f] = s\n\t\t\t}\n\t\t}\n\n\t\tfirstKeyPos, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmdInfo.FirstKeyPos = int8(firstKeyPos)\n\n\t\tlastKeyPos, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmdInfo.LastKeyPos = int8(lastKeyPos)\n\n\t\tstepCount, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmdInfo.StepCount = int8(stepCount)\n\n\t\tif nn >= numArgRedis6 {\n\t\t\taclFlagLen, err := rd.ReadArrayLen()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcmdInfo.ACLFlags = make([]string, aclFlagLen)\n\t\t\tfor f := 0; f < len(cmdInfo.ACLFlags); f++ {\n\t\t\t\tswitch s, err := rd.ReadString(); {\n\t\t\t\tcase err == Nil:\n\t\t\t\t\tcmdInfo.ACLFlags[f] = \"\"\n\t\t\t\tcase err != nil:\n\t\t\t\t\treturn err\n\t\t\t\tdefault:\n\t\t\t\t\tcmdInfo.ACLFlags[f] = s\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif nn >= numArgRedis7 {\n\t\t\t// The 8th argument is an array of tips.\n\t\t\ttipsLen, err := rd.ReadArrayLen()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trawTips := make(map[string]string, tipsLen)\n\t\t\tif cmdInfo.ReadOnly {\n\t\t\t\trawTips[routing.ReadOnlyCMD] = \"\"\n\t\t\t}\n\t\t\tfor f := 0; f < tipsLen; f++ {\n\t\t\t\ttip, err := rd.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tk, v, ok := strings.Cut(tip, \":\")\n\t\t\t\tif !ok {\n\t\t\t\t\t// Handle tips that don't have a colon (like \"nondeterministic_output\")\n\t\t\t\t\trawTips[tip] = \"\"\n\t\t\t\t} else {\n\t\t\t\t\t// Handle normal key:value tips\n\t\t\t\t\trawTips[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\tcmdInfo.CommandPolicy = parseCommandPolicies(rawTips, cmdInfo.FirstKeyPos)\n\n\t\t\tif err := rd.DiscardNext(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := rd.DiscardNext(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tcmd.val[cmdInfo.Name] = cmdInfo\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *CommandsInfoCmd) Clone() Cmder {\n\tvar val map[string]*CommandInfo\n\tif cmd.val != nil {\n\t\tval = make(map[string]*CommandInfo, len(cmd.val))\n\t\tfor k, v := range cmd.val {\n\t\t\tif v != nil {\n\t\t\t\tnewInfo := &CommandInfo{\n\t\t\t\t\tName:          v.Name,\n\t\t\t\t\tArity:         v.Arity,\n\t\t\t\t\tFirstKeyPos:   v.FirstKeyPos,\n\t\t\t\t\tLastKeyPos:    v.LastKeyPos,\n\t\t\t\t\tStepCount:     v.StepCount,\n\t\t\t\t\tReadOnly:      v.ReadOnly,\n\t\t\t\t\tCommandPolicy: v.CommandPolicy, // CommandPolicy can be shared as it's immutable\n\t\t\t\t}\n\t\t\t\tif v.Flags != nil {\n\t\t\t\t\tnewInfo.Flags = make([]string, len(v.Flags))\n\t\t\t\t\tcopy(newInfo.Flags, v.Flags)\n\t\t\t\t}\n\t\t\t\tif v.ACLFlags != nil {\n\t\t\t\t\tnewInfo.ACLFlags = make([]string, len(v.ACLFlags))\n\t\t\t\t\tcopy(newInfo.ACLFlags, v.ACLFlags)\n\t\t\t\t}\n\t\t\t\tval[k] = newInfo\n\t\t\t}\n\t\t}\n\t}\n\treturn &CommandsInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype cmdsInfoCache struct {\n\tfn func(ctx context.Context) (map[string]*CommandInfo, error)\n\n\tonce        internal.Once\n\trefreshLock sync.Mutex\n\tcmds        map[string]*CommandInfo\n}\n\nfunc newCmdsInfoCache(fn func(ctx context.Context) (map[string]*CommandInfo, error)) *cmdsInfoCache {\n\treturn &cmdsInfoCache{\n\t\tfn: fn,\n\t}\n}\n\nfunc (c *cmdsInfoCache) Get(ctx context.Context) (map[string]*CommandInfo, error) {\n\tc.refreshLock.Lock()\n\tdefer c.refreshLock.Unlock()\n\n\terr := c.once.Do(func() error {\n\t\tcmds, err := c.fn(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlowerCmds := make(map[string]*CommandInfo, len(cmds))\n\n\t\t// Extensions have cmd names in upper case. Convert them to lower case.\n\t\tfor k, v := range cmds {\n\t\t\tlowerCmds[internal.ToLower(k)] = v\n\t\t}\n\n\t\tc.cmds = lowerCmds\n\t\treturn nil\n\t})\n\treturn c.cmds, err\n}\n\nfunc (c *cmdsInfoCache) Refresh() {\n\tc.refreshLock.Lock()\n\tdefer c.refreshLock.Unlock()\n\n\tc.once = internal.Once{}\n}\n\n// ------------------------------------------------------------------------------\nconst requestPolicy = \"request_policy\"\nconst responsePolicy = \"response_policy\"\n\nfunc parseCommandPolicies(commandInfoTips map[string]string, firstKeyPos int8) *routing.CommandPolicy {\n\treq := routing.ReqDefault\n\tresp := routing.RespDefaultKeyless\n\tif firstKeyPos > 0 {\n\t\tresp = routing.RespDefaultHashSlot\n\t}\n\n\ttips := make(map[string]string, len(commandInfoTips))\n\tfor k, v := range commandInfoTips {\n\t\tif k == requestPolicy {\n\t\t\tif p, err := routing.ParseRequestPolicy(v); err == nil {\n\t\t\t\treq = p\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif k == responsePolicy {\n\t\t\tif p, err := routing.ParseResponsePolicy(v); err == nil {\n\t\t\t\tresp = p\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\ttips[k] = v\n\t}\n\n\treturn &routing.CommandPolicy{Request: req, Response: resp, Tips: tips}\n}\n\n//------------------------------------------------------------------------------\n\ntype SlowLog struct {\n\tID       int64\n\tTime     time.Time\n\tDuration time.Duration\n\tArgs     []string\n\t// These are also optional fields emitted only by Redis 4.0 or greater:\n\t// https://redis.io/commands/slowlog#output-format\n\tClientAddr string\n\tClientName string\n}\n\ntype SlowLogCmd struct {\n\tbaseCmd\n\n\tval []SlowLog\n}\n\nvar _ Cmder = (*SlowLogCmd)(nil)\n\nfunc NewSlowLogCmd(ctx context.Context, args ...interface{}) *SlowLogCmd {\n\treturn &SlowLogCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeSlowLog,\n\t\t},\n\t}\n}\n\nfunc (cmd *SlowLogCmd) SetVal(val []SlowLog) {\n\tcmd.val = val\n}\n\nfunc (cmd *SlowLogCmd) Val() []SlowLog {\n\treturn cmd.val\n}\n\nfunc (cmd *SlowLogCmd) Result() ([]SlowLog, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *SlowLogCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *SlowLogCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]SlowLog, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tnn, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif nn < 4 {\n\t\t\treturn fmt.Errorf(\"redis: got %d elements in slowlog get, expected at least 4\", nn)\n\t\t}\n\n\t\tif cmd.val[i].ID, err = rd.ReadInt(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcreatedAt, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Time = time.Unix(createdAt, 0)\n\n\t\tcosts, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Duration = time.Duration(costs) * time.Microsecond\n\n\t\tcmdLen, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif cmdLen < 1 {\n\t\t\treturn fmt.Errorf(\"redis: got %d elements commands reply in slowlog get, expected at least 1\", cmdLen)\n\t\t}\n\n\t\tcmd.val[i].Args = make([]string, cmdLen)\n\t\tfor f := 0; f < len(cmd.val[i].Args); f++ {\n\t\t\tcmd.val[i].Args[f], err = rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif nn >= 5 {\n\t\t\tif cmd.val[i].ClientAddr, err = rd.ReadString(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif nn >= 6 {\n\t\t\tif cmd.val[i].ClientName, err = rd.ReadString(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *SlowLogCmd) Clone() Cmder {\n\tvar val []SlowLog\n\tif cmd.val != nil {\n\t\tval = make([]SlowLog, len(cmd.val))\n\t\tfor i, log := range cmd.val {\n\t\t\tval[i] = SlowLog{\n\t\t\t\tID:         log.ID,\n\t\t\t\tTime:       log.Time,\n\t\t\t\tDuration:   log.Duration,\n\t\t\t\tClientAddr: log.ClientAddr,\n\t\t\t\tClientName: log.ClientName,\n\t\t\t}\n\t\t\tif log.Args != nil {\n\t\t\t\tval[i].Args = make([]string, len(log.Args))\n\t\t\t\tcopy(val[i].Args, log.Args)\n\t\t\t}\n\t\t}\n\t}\n\treturn &SlowLogCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//-----------------------------------------------------------------------\n\ntype Latency struct {\n\tName   string\n\tTime   time.Time\n\tLatest time.Duration\n\tMax    time.Duration\n}\n\ntype LatencyCmd struct {\n\tbaseCmd\n\tval []Latency\n}\n\nvar _ Cmder = (*LatencyCmd)(nil)\n\nfunc NewLatencyCmd(ctx context.Context, args ...interface{}) *LatencyCmd {\n\treturn &LatencyCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:  ctx,\n\t\t\targs: args,\n\t\t},\n\t}\n}\n\nfunc (cmd *LatencyCmd) SetVal(val []Latency) {\n\tcmd.val = val\n}\n\nfunc (cmd *LatencyCmd) Val() []Latency {\n\treturn cmd.val\n}\n\nfunc (cmd *LatencyCmd) Result() ([]Latency, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *LatencyCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *LatencyCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]Latency, n)\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tnn, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif nn < 3 {\n\t\t\treturn fmt.Errorf(\"redis: got %d elements in latency get, expected at least 3\", nn)\n\t\t}\n\t\tif cmd.val[i].Name, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcreatedAt, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Time = time.Unix(createdAt, 0)\n\t\tlatest, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Latest = time.Duration(latest) * time.Millisecond\n\t\tmaximum, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Max = time.Duration(maximum) * time.Millisecond\n\t}\n\treturn nil\n}\n\nfunc (cmd *LatencyCmd) Clone() Cmder {\n\tvar val []Latency\n\tif cmd.val != nil {\n\t\tval = make([]Latency, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &LatencyCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//-----------------------------------------------------------------------\n\n// HotKeysSlotRange represents a slot or slot range in the response.\n// Single element slice = individual slot, two element slice = slot range [start, end].\ntype HotKeysSlotRange []int64\n\n// HotKeysKeyEntry represents a hot key entry with its metric value.\ntype HotKeysKeyEntry struct {\n\tKey   string\n\tValue interface{} // Can be int64 or string\n}\n\n// HotKeysResult represents the response data from HOTKEYS GET command.\n// Field names match the Redis response format.\ntype HotKeysResult struct {\n\tTrackingActive                       bool\n\tSampleRatio                          uint8\n\tSelectedSlots                        []HotKeysSlotRange\n\tSampledCommandsSelectedSlots         time.Duration // Present when sample-ratio > 1 and selected-slots is not empty\n\tAllCommandsSelectedSlots             time.Duration // Present when selected-slots is not empty\n\tAllCommandsAllSlots                  time.Duration\n\tNetBytesSampledCommandsSelectedSlots int64 // Present when sample-ratio > 1 and selected-slots is not empty\n\tNetBytesAllCommandsSelectedSlots     int64 // Present when selected-slots is not empty\n\tNetBytesAllCommandsAllSlots          int64\n\tCollectionStartTime                  time.Time\n\tCollectionDuration                   time.Duration\n\tUsedCPUSys                           time.Duration\n\tUsedCPUUser                          time.Duration\n\tTotalNetBytes                        int64\n\tByCPUTime                            []HotKeysKeyEntry\n\tByNetBytes                           []HotKeysKeyEntry\n}\n\ntype HotKeysCmd struct {\n\tbaseCmd\n\n\tval *HotKeysResult\n}\n\nvar _ Cmder = (*HotKeysCmd)(nil)\n\nfunc NewHotKeysCmd(ctx context.Context, args ...interface{}) *HotKeysCmd {\n\treturn &HotKeysCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeHotKeys,\n\t\t},\n\t}\n}\n\nfunc (cmd *HotKeysCmd) SetVal(val *HotKeysResult) {\n\tcmd.val = val\n}\n\nfunc (cmd *HotKeysCmd) Val() *HotKeysResult {\n\treturn cmd.val\n}\n\nfunc (cmd *HotKeysCmd) Result() (*HotKeysResult, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *HotKeysCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *HotKeysCmd) readReply(rd *proto.Reader) error {\n\t// HOTKEYS GET response is wrapped in an array for aggregation support\n\tarrayLen, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif arrayLen == 0 {\n\t\t// Empty array means no tracking was started or after reset\n\t\tcmd.val = nil\n\t\treturn nil\n\t}\n\n\t// Read the first (and typically only) element which is a map\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresult := &HotKeysResult{}\n\tdata := make(map[string]interface{}, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tk, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tv, err := rd.ReadReply()\n\t\tif err != nil {\n\t\t\tif err == Nil {\n\t\t\t\tdata[k] = Nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err, ok := err.(proto.RedisError); ok {\n\t\t\t\tdata[k] = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tdata[k] = v\n\t}\n\n\tif v, ok := data[\"tracking-active\"].(int64); ok {\n\t\tresult.TrackingActive = v == 1\n\t}\n\tif v, ok := data[\"sample-ratio\"].(int64); ok {\n\t\tresult.SampleRatio = uint8(v)\n\t}\n\tif v, ok := data[\"selected-slots\"].([]interface{}); ok {\n\t\tresult.SelectedSlots = make([]HotKeysSlotRange, 0, len(v))\n\t\tfor _, slot := range v {\n\t\t\tswitch s := slot.(type) {\n\t\t\tcase int64:\n\t\t\t\t// Single slot\n\t\t\t\tresult.SelectedSlots = append(result.SelectedSlots, HotKeysSlotRange{s})\n\t\t\tcase []interface{}:\n\t\t\t\t// Slot range\n\t\t\t\tslotRange := make(HotKeysSlotRange, 0, len(s))\n\t\t\t\tfor _, sr := range s {\n\t\t\t\t\tif val, ok := sr.(int64); ok {\n\t\t\t\t\t\tslotRange = append(slotRange, val)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tresult.SelectedSlots = append(result.SelectedSlots, slotRange)\n\t\t\t}\n\t\t}\n\t}\n\tif v, ok := data[\"sampled-commands-selected-slots-us\"].(int64); ok {\n\t\tresult.SampledCommandsSelectedSlots = time.Duration(v) * time.Microsecond\n\t}\n\tif v, ok := data[\"all-commands-selected-slots-us\"].(int64); ok {\n\t\tresult.AllCommandsSelectedSlots = time.Duration(v) * time.Microsecond\n\t}\n\tif v, ok := data[\"all-commands-all-slots-us\"].(int64); ok {\n\t\tresult.AllCommandsAllSlots = time.Duration(v) * time.Microsecond\n\t}\n\tif v, ok := data[\"net-bytes-sampled-commands-selected-slots\"].(int64); ok {\n\t\tresult.NetBytesSampledCommandsSelectedSlots = v\n\t}\n\tif v, ok := data[\"net-bytes-all-commands-selected-slots\"].(int64); ok {\n\t\tresult.NetBytesAllCommandsSelectedSlots = v\n\t}\n\tif v, ok := data[\"net-bytes-all-commands-all-slots\"].(int64); ok {\n\t\tresult.NetBytesAllCommandsAllSlots = v\n\t}\n\tif v, ok := data[\"collection-start-time-unix-ms\"].(int64); ok {\n\t\tresult.CollectionStartTime = time.UnixMilli(v)\n\t}\n\tif v, ok := data[\"collection-duration-ms\"].(int64); ok {\n\t\tresult.CollectionDuration = time.Duration(v) * time.Millisecond\n\t}\n\tif v, ok := data[\"used-cpu-sys-ms\"].(int64); ok {\n\t\tresult.UsedCPUSys = time.Duration(v) * time.Millisecond\n\t}\n\tif v, ok := data[\"used-cpu-user-ms\"].(int64); ok {\n\t\tresult.UsedCPUUser = time.Duration(v) * time.Millisecond\n\t}\n\tif v, ok := data[\"total-net-bytes\"].(int64); ok {\n\t\tresult.TotalNetBytes = v\n\t}\n\n\tif v, ok := data[\"by-cpu-time-us\"].([]interface{}); ok {\n\t\tresult.ByCPUTime = parseHotKeysKeyEntries(v)\n\t}\n\n\tif v, ok := data[\"by-net-bytes\"].([]interface{}); ok {\n\t\tresult.ByNetBytes = parseHotKeysKeyEntries(v)\n\t}\n\n\tcmd.val = result\n\treturn nil\n}\n\n// parseHotKeysKeyEntries parses the key-value pairs from HOTKEYS GET response.\nfunc parseHotKeysKeyEntries(v []interface{}) []HotKeysKeyEntry {\n\tentries := make([]HotKeysKeyEntry, 0, len(v)/2)\n\tfor i := 0; i < len(v); i += 2 {\n\t\tif i+1 < len(v) {\n\t\t\tkey, keyOk := v[i].(string)\n\t\t\tif keyOk {\n\t\t\t\tentries = append(entries, HotKeysKeyEntry{\n\t\t\t\t\tKey:   key,\n\t\t\t\t\tValue: v[i+1], // Can be int64 or string\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\treturn entries\n}\n\nfunc (cmd *HotKeysCmd) Clone() Cmder {\n\tvar val *HotKeysResult\n\tif cmd.val != nil {\n\t\tval = &HotKeysResult{\n\t\t\tTrackingActive:                       cmd.val.TrackingActive,\n\t\t\tSampleRatio:                          cmd.val.SampleRatio,\n\t\t\tSampledCommandsSelectedSlots:         cmd.val.SampledCommandsSelectedSlots,\n\t\t\tAllCommandsSelectedSlots:             cmd.val.AllCommandsSelectedSlots,\n\t\t\tAllCommandsAllSlots:                  cmd.val.AllCommandsAllSlots,\n\t\t\tNetBytesSampledCommandsSelectedSlots: cmd.val.NetBytesSampledCommandsSelectedSlots,\n\t\t\tNetBytesAllCommandsSelectedSlots:     cmd.val.NetBytesAllCommandsSelectedSlots,\n\t\t\tNetBytesAllCommandsAllSlots:          cmd.val.NetBytesAllCommandsAllSlots,\n\t\t\tCollectionStartTime:                  cmd.val.CollectionStartTime,\n\t\t\tCollectionDuration:                   cmd.val.CollectionDuration,\n\t\t\tUsedCPUSys:                           cmd.val.UsedCPUSys,\n\t\t\tUsedCPUUser:                          cmd.val.UsedCPUUser,\n\t\t\tTotalNetBytes:                        cmd.val.TotalNetBytes,\n\t\t}\n\t\tif cmd.val.SelectedSlots != nil {\n\t\t\tval.SelectedSlots = make([]HotKeysSlotRange, len(cmd.val.SelectedSlots))\n\t\t\tfor i, sr := range cmd.val.SelectedSlots {\n\t\t\t\tval.SelectedSlots[i] = make(HotKeysSlotRange, len(sr))\n\t\t\t\tcopy(val.SelectedSlots[i], sr)\n\t\t\t}\n\t\t}\n\t\tif cmd.val.ByCPUTime != nil {\n\t\t\tval.ByCPUTime = make([]HotKeysKeyEntry, len(cmd.val.ByCPUTime))\n\t\t\tcopy(val.ByCPUTime, cmd.val.ByCPUTime)\n\t\t}\n\t\tif cmd.val.ByNetBytes != nil {\n\t\t\tval.ByNetBytes = make([]HotKeysKeyEntry, len(cmd.val.ByNetBytes))\n\t\t\tcopy(val.ByNetBytes, cmd.val.ByNetBytes)\n\t\t}\n\t}\n\treturn &HotKeysCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//-----------------------------------------------------------------------\n\ntype MapStringInterfaceCmd struct {\n\tbaseCmd\n\n\tval map[string]interface{}\n}\n\nvar _ Cmder = (*MapStringInterfaceCmd)(nil)\n\nfunc NewMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceCmd {\n\treturn &MapStringInterfaceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeMapStringInterface,\n\t\t},\n\t}\n}\n\nfunc (cmd *MapStringInterfaceCmd) SetVal(val map[string]interface{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *MapStringInterfaceCmd) Val() map[string]interface{} {\n\treturn cmd.val\n}\n\nfunc (cmd *MapStringInterfaceCmd) Result() (map[string]interface{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *MapStringInterfaceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *MapStringInterfaceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make(map[string]interface{}, n)\n\tfor i := 0; i < n; i++ {\n\t\tk, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tv, err := rd.ReadReply()\n\t\tif err != nil {\n\t\t\tif err == Nil {\n\t\t\t\tcmd.val[k] = Nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err, ok := err.(proto.RedisError); ok {\n\t\t\t\tcmd.val[k] = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[k] = v\n\t}\n\treturn nil\n}\n\nfunc (cmd *MapStringInterfaceCmd) Clone() Cmder {\n\tvar val map[string]interface{}\n\tif cmd.val != nil {\n\t\tval = make(map[string]interface{}, len(cmd.val))\n\t\tfor k, v := range cmd.val {\n\t\t\tval[k] = v\n\t\t}\n\t}\n\treturn &MapStringInterfaceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//-----------------------------------------------------------------------\n\ntype MapStringStringSliceCmd struct {\n\tbaseCmd\n\n\tval []map[string]string\n}\n\nvar _ Cmder = (*MapStringStringSliceCmd)(nil)\n\nfunc NewMapStringStringSliceCmd(ctx context.Context, args ...interface{}) *MapStringStringSliceCmd {\n\treturn &MapStringStringSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeMapStringStringSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *MapStringStringSliceCmd) SetVal(val []map[string]string) {\n\tcmd.val = val\n}\n\nfunc (cmd *MapStringStringSliceCmd) Val() []map[string]string {\n\treturn cmd.val\n}\n\nfunc (cmd *MapStringStringSliceCmd) Result() ([]map[string]string, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *MapStringStringSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make([]map[string]string, n)\n\tfor i := 0; i < n; i++ {\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i] = make(map[string]string, nn)\n\t\tfor f := 0; f < nn; f++ {\n\t\t\tk, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tv, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcmd.val[i][k] = v\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *MapStringStringSliceCmd) Clone() Cmder {\n\tvar val []map[string]string\n\tif cmd.val != nil {\n\t\tval = make([]map[string]string, len(cmd.val))\n\t\tfor i, m := range cmd.val {\n\t\t\tif m != nil {\n\t\t\t\tval[i] = make(map[string]string, len(m))\n\t\t\t\tfor k, v := range m {\n\t\t\t\t\tval[i][k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &MapStringStringSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// -----------------------------------------------------------------------\n\n// MapMapStringInterfaceCmd represents a command that returns a map of strings to interface{}.\ntype MapMapStringInterfaceCmd struct {\n\tbaseCmd\n\tval map[string]interface{}\n}\n\nfunc NewMapMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapMapStringInterfaceCmd {\n\treturn &MapMapStringInterfaceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeMapMapStringInterface,\n\t\t},\n\t}\n}\n\nfunc (cmd *MapMapStringInterfaceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *MapMapStringInterfaceCmd) SetVal(val map[string]interface{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *MapMapStringInterfaceCmd) Result() (map[string]interface{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} {\n\treturn cmd.val\n}\n\n// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3\nfunc (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) {\n\tdata, err := rd.ReadReply()\n\tif err != nil {\n\t\treturn err\n\t}\n\tresultMap := map[string]interface{}{}\n\n\tswitch midResponse := data.(type) {\n\tcase map[interface{}]interface{}: // resp3 will return map\n\t\tfor k, v := range midResponse {\n\t\t\tstringKey, ok := k.(string)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"redis: invalid map key %#v\", k)\n\t\t\t}\n\t\t\tresultMap[stringKey] = v\n\t\t}\n\tcase []interface{}: // resp2 will return array of arrays\n\t\tn := len(midResponse)\n\t\tfor i := 0; i < n; i++ {\n\t\t\tfinalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"redis: unexpected response %#v\", data)\n\t\t\t}\n\t\t\tm := len(finalArr)\n\t\t\tif m%2 != 0 { // since this should be map, keys should be even number\n\t\t\t\treturn fmt.Errorf(\"redis: unexpected response %#v\", data)\n\t\t\t}\n\n\t\t\tfor j := 0; j < m; j += 2 {\n\t\t\t\tstringKey, ok := finalArr[j].(string) // the first one\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"redis: invalid map key %#v\", finalArr[i])\n\t\t\t\t}\n\t\t\t\tresultMap[stringKey] = finalArr[j+1] // second one is value\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"redis: unexpected response %#v\", data)\n\t}\n\n\tcmd.val = resultMap\n\treturn nil\n}\n\nfunc (cmd *MapMapStringInterfaceCmd) Clone() Cmder {\n\tvar val map[string]interface{}\n\tif cmd.val != nil {\n\t\tval = make(map[string]interface{}, len(cmd.val))\n\t\tfor k, v := range cmd.val {\n\t\t\tval[k] = v\n\t\t}\n\t}\n\treturn &MapMapStringInterfaceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//-----------------------------------------------------------------------\n\ntype MapStringInterfaceSliceCmd struct {\n\tbaseCmd\n\n\tval []map[string]interface{}\n}\n\nvar _ Cmder = (*MapStringInterfaceSliceCmd)(nil)\n\nfunc NewMapStringInterfaceSliceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceSliceCmd {\n\treturn &MapStringInterfaceSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeMapStringInterfaceSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *MapStringInterfaceSliceCmd) SetVal(val []map[string]interface{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *MapStringInterfaceSliceCmd) Val() []map[string]interface{} {\n\treturn cmd.val\n}\n\nfunc (cmd *MapStringInterfaceSliceCmd) Result() ([]map[string]interface{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *MapStringInterfaceSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *MapStringInterfaceSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make([]map[string]interface{}, n)\n\tfor i := 0; i < n; i++ {\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i] = make(map[string]interface{}, nn)\n\t\tfor f := 0; f < nn; f++ {\n\t\t\tk, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tv, err := rd.ReadReply()\n\t\t\tif err != nil {\n\t\t\t\tif err != Nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tcmd.val[i][k] = v\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *MapStringInterfaceSliceCmd) Clone() Cmder {\n\tvar val []map[string]interface{}\n\tif cmd.val != nil {\n\t\tval = make([]map[string]interface{}, len(cmd.val))\n\t\tfor i, m := range cmd.val {\n\t\t\tif m != nil {\n\t\t\t\tval[i] = make(map[string]interface{}, len(m))\n\t\t\t\tfor k, v := range m {\n\t\t\t\t\tval[i][k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &MapStringInterfaceSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype KeyValuesCmd struct {\n\tbaseCmd\n\n\tkey string\n\tval []string\n}\n\nvar _ Cmder = (*KeyValuesCmd)(nil)\n\nfunc NewKeyValuesCmd(ctx context.Context, args ...interface{}) *KeyValuesCmd {\n\treturn &KeyValuesCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeKeyValues,\n\t\t},\n\t}\n}\n\nfunc (cmd *KeyValuesCmd) SetVal(key string, val []string) {\n\tcmd.key = key\n\tcmd.val = val\n}\n\nfunc (cmd *KeyValuesCmd) Val() (string, []string) {\n\treturn cmd.key, cmd.val\n}\n\nfunc (cmd *KeyValuesCmd) Result() (string, []string, error) {\n\treturn cmd.key, cmd.val, cmd.err\n}\n\nfunc (cmd *KeyValuesCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *KeyValuesCmd) readReply(rd *proto.Reader) (err error) {\n\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\treturn err\n\t}\n\n\tcmd.key, err = rd.ReadString()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]string, n)\n\tfor i := 0; i < n; i++ {\n\t\tcmd.val[i], err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *KeyValuesCmd) Clone() Cmder {\n\tvar val []string\n\tif cmd.val != nil {\n\t\tval = make([]string, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &KeyValuesCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tkey:     cmd.key,\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype ZSliceWithKeyCmd struct {\n\tbaseCmd\n\n\tkey string\n\tval []Z\n}\n\nvar _ Cmder = (*ZSliceWithKeyCmd)(nil)\n\nfunc NewZSliceWithKeyCmd(ctx context.Context, args ...interface{}) *ZSliceWithKeyCmd {\n\treturn &ZSliceWithKeyCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeZSliceWithKey,\n\t\t},\n\t}\n}\n\nfunc (cmd *ZSliceWithKeyCmd) SetVal(key string, val []Z) {\n\tcmd.key = key\n\tcmd.val = val\n}\n\nfunc (cmd *ZSliceWithKeyCmd) Val() (string, []Z) {\n\treturn cmd.key, cmd.val\n}\n\nfunc (cmd *ZSliceWithKeyCmd) Result() (string, []Z, error) {\n\treturn cmd.key, cmd.val, cmd.err\n}\n\nfunc (cmd *ZSliceWithKeyCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ZSliceWithKeyCmd) readReply(rd *proto.Reader) (err error) {\n\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\treturn err\n\t}\n\n\tcmd.key, err = rd.ReadString()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttyp, err := rd.PeekReplyType()\n\tif err != nil {\n\t\treturn err\n\t}\n\tarray := typ == proto.RespArray\n\n\tif array {\n\t\tcmd.val = make([]Z, n)\n\t} else {\n\t\tcmd.val = make([]Z, n/2)\n\t}\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tif array {\n\t\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif cmd.val[i].Member, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cmd.val[i].Score, err = rd.ReadFloat(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ZSliceWithKeyCmd) Clone() Cmder {\n\tvar val []Z\n\tif cmd.val != nil {\n\t\tval = make([]Z, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &ZSliceWithKeyCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tkey:     cmd.key,\n\t\tval:     val,\n\t}\n}\n\ntype Function struct {\n\tName        string\n\tDescription string\n\tFlags       []string\n}\n\ntype Library struct {\n\tName      string\n\tEngine    string\n\tFunctions []Function\n\tCode      string\n}\n\ntype FunctionListCmd struct {\n\tbaseCmd\n\n\tval []Library\n}\n\nvar _ Cmder = (*FunctionListCmd)(nil)\n\nfunc NewFunctionListCmd(ctx context.Context, args ...interface{}) *FunctionListCmd {\n\treturn &FunctionListCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFunctionList,\n\t\t},\n\t}\n}\n\nfunc (cmd *FunctionListCmd) SetVal(val []Library) {\n\tcmd.val = val\n}\n\nfunc (cmd *FunctionListCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FunctionListCmd) Val() []Library {\n\treturn cmd.val\n}\n\nfunc (cmd *FunctionListCmd) Result() ([]Library, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FunctionListCmd) First() (*Library, error) {\n\tif cmd.err != nil {\n\t\treturn nil, cmd.err\n\t}\n\tif len(cmd.val) > 0 {\n\t\treturn &cmd.val[0], nil\n\t}\n\treturn nil, Nil\n}\n\nfunc (cmd *FunctionListCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlibraries := make([]Library, n)\n\tfor i := 0; i < n; i++ {\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlibrary := Library{}\n\t\tfor f := 0; f < nn; f++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"library_name\":\n\t\t\t\tlibrary.Name, err = rd.ReadString()\n\t\t\tcase \"engine\":\n\t\t\t\tlibrary.Engine, err = rd.ReadString()\n\t\t\tcase \"functions\":\n\t\t\t\tlibrary.Functions, err = cmd.readFunctions(rd)\n\t\t\tcase \"library_code\":\n\t\t\t\tlibrary.Code, err = rd.ReadString()\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"redis: function list unexpected key %s\", key)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tlibraries[i] = library\n\t}\n\tcmd.val = libraries\n\treturn nil\n}\n\nfunc (cmd *FunctionListCmd) readFunctions(rd *proto.Reader) ([]Function, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfunctions := make([]Function, n)\n\tfor i := 0; i < n; i++ {\n\t\tnn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfunction := Function{}\n\t\tfor f := 0; f < nn; f++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"name\":\n\t\t\t\tif function.Name, err = rd.ReadString(); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"description\":\n\t\t\t\tif function.Description, err = rd.ReadString(); err != nil && err != Nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\tcase \"flags\":\n\t\t\t\t// resp set\n\t\t\t\tnx, err := rd.ReadArrayLen()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tfunction.Flags = make([]string, nx)\n\t\t\t\tfor j := 0; j < nx; j++ {\n\t\t\t\t\tif function.Flags[j], err = rd.ReadString(); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"redis: function list unexpected key %s\", key)\n\t\t\t}\n\t\t}\n\n\t\tfunctions[i] = function\n\t}\n\treturn functions, nil\n}\n\nfunc (cmd *FunctionListCmd) Clone() Cmder {\n\tvar val []Library\n\tif cmd.val != nil {\n\t\tval = make([]Library, len(cmd.val))\n\t\tfor i, lib := range cmd.val {\n\t\t\tval[i] = Library{\n\t\t\t\tName:   lib.Name,\n\t\t\t\tEngine: lib.Engine,\n\t\t\t\tCode:   lib.Code,\n\t\t\t}\n\t\t\tif lib.Functions != nil {\n\t\t\t\tval[i].Functions = make([]Function, len(lib.Functions))\n\t\t\t\tfor j, fn := range lib.Functions {\n\t\t\t\t\tval[i].Functions[j] = Function{\n\t\t\t\t\t\tName:        fn.Name,\n\t\t\t\t\t\tDescription: fn.Description,\n\t\t\t\t\t}\n\t\t\t\t\tif fn.Flags != nil {\n\t\t\t\t\t\tval[i].Functions[j].Flags = make([]string, len(fn.Flags))\n\t\t\t\t\t\tcopy(val[i].Functions[j].Flags, fn.Flags)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &FunctionListCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// FunctionStats contains information about the scripts currently executing on the server, and the available engines\n//   - Engines:\n//     Statistics about the engine like number of functions and number of libraries\n//   - RunningScript:\n//     The script currently running on the shard we're connecting to.\n//     For Redis Enterprise and Redis Cloud, this represents the\n//     function with the longest running time, across all the running functions, on all shards\n//   - RunningScripts\n//     All scripts currently running in a Redis Enterprise clustered database.\n//     Only available on Redis Enterprise\ntype FunctionStats struct {\n\tEngines   []Engine\n\tisRunning bool\n\trs        RunningScript\n\tallrs     []RunningScript\n}\n\nfunc (fs *FunctionStats) Running() bool {\n\treturn fs.isRunning\n}\n\nfunc (fs *FunctionStats) RunningScript() (RunningScript, bool) {\n\treturn fs.rs, fs.isRunning\n}\n\n// AllRunningScripts returns all scripts currently running in a Redis Enterprise clustered database.\n// Only available on Redis Enterprise\nfunc (fs *FunctionStats) AllRunningScripts() []RunningScript {\n\treturn fs.allrs\n}\n\ntype RunningScript struct {\n\tName     string\n\tCommand  []string\n\tDuration time.Duration\n}\n\ntype Engine struct {\n\tLanguage       string\n\tLibrariesCount int64\n\tFunctionsCount int64\n}\n\ntype FunctionStatsCmd struct {\n\tbaseCmd\n\tval FunctionStats\n}\n\nvar _ Cmder = (*FunctionStatsCmd)(nil)\n\nfunc NewFunctionStatsCmd(ctx context.Context, args ...interface{}) *FunctionStatsCmd {\n\treturn &FunctionStatsCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFunctionStats,\n\t\t},\n\t}\n}\n\nfunc (cmd *FunctionStatsCmd) SetVal(val FunctionStats) {\n\tcmd.val = val\n}\n\nfunc (cmd *FunctionStatsCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FunctionStatsCmd) Val() FunctionStats {\n\treturn cmd.val\n}\n\nfunc (cmd *FunctionStatsCmd) Result() (FunctionStats, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FunctionStatsCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar key string\n\tvar result FunctionStats\n\tfor f := 0; f < n; f++ {\n\t\tkey, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"running_script\":\n\t\t\tresult.rs, result.isRunning, err = cmd.readRunningScript(rd)\n\t\tcase \"engines\":\n\t\t\tresult.Engines, err = cmd.readEngines(rd)\n\t\tcase \"all_running_scripts\": // Redis Enterprise only\n\t\t\tresult.allrs, result.isRunning, err = cmd.readRunningScripts(rd)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: function stats unexpected key %s\", key)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcmd.val = result\n\treturn nil\n}\n\nfunc (cmd *FunctionStatsCmd) readRunningScript(rd *proto.Reader) (RunningScript, bool, error) {\n\terr := rd.ReadFixedMapLen(3)\n\tif err != nil {\n\t\tif err == Nil {\n\t\t\treturn RunningScript{}, false, nil\n\t\t}\n\t\treturn RunningScript{}, false, err\n\t}\n\n\tvar runningScript RunningScript\n\tfor i := 0; i < 3; i++ {\n\t\tkey, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn RunningScript{}, false, err\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"name\":\n\t\t\trunningScript.Name, err = rd.ReadString()\n\t\tcase \"duration_ms\":\n\t\t\trunningScript.Duration, err = cmd.readDuration(rd)\n\t\tcase \"command\":\n\t\t\trunningScript.Command, err = cmd.readCommand(rd)\n\t\tdefault:\n\t\t\treturn RunningScript{}, false, fmt.Errorf(\"redis: function stats unexpected running_script key %s\", key)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn RunningScript{}, false, err\n\t\t}\n\t}\n\n\treturn runningScript, true, nil\n}\n\nfunc (cmd *FunctionStatsCmd) readEngines(rd *proto.Reader) ([]Engine, error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tengines := make([]Engine, 0, n)\n\tfor i := 0; i < n; i++ {\n\t\tengine := Engine{}\n\t\tengine.Language, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = rd.ReadFixedMapLen(2)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"redis: function stats unexpected %s engine map length\", engine.Language)\n\t\t}\n\n\t\tfor i := 0; i < 2; i++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tswitch key {\n\t\t\tcase \"libraries_count\":\n\t\t\t\tengine.LibrariesCount, err = rd.ReadInt()\n\t\t\tcase \"functions_count\":\n\t\t\t\tengine.FunctionsCount, err = rd.ReadInt()\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tengines = append(engines, engine)\n\t}\n\treturn engines, nil\n}\n\nfunc (cmd *FunctionStatsCmd) readDuration(rd *proto.Reader) (time.Duration, error) {\n\tt, err := rd.ReadInt()\n\tif err != nil {\n\t\treturn time.Duration(0), err\n\t}\n\treturn time.Duration(t) * time.Millisecond, nil\n}\n\nfunc (cmd *FunctionStatsCmd) readCommand(rd *proto.Reader) ([]string, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcommand := make([]string, 0, n)\n\tfor i := 0; i < n; i++ {\n\t\tx, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcommand = append(command, x)\n\t}\n\n\treturn command, nil\n}\n\nfunc (cmd *FunctionStatsCmd) readRunningScripts(rd *proto.Reader) ([]RunningScript, bool, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\trunningScripts := make([]RunningScript, 0, n)\n\tfor i := 0; i < n; i++ {\n\t\trs, _, err := cmd.readRunningScript(rd)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\trunningScripts = append(runningScripts, rs)\n\t}\n\n\treturn runningScripts, len(runningScripts) > 0, nil\n}\n\nfunc (cmd *FunctionStatsCmd) Clone() Cmder {\n\tval := FunctionStats{\n\t\tisRunning: cmd.val.isRunning,\n\t\trs:        cmd.val.rs, // RunningScript is a simple struct, can be copied directly\n\t}\n\tif cmd.val.Engines != nil {\n\t\tval.Engines = make([]Engine, len(cmd.val.Engines))\n\t\tcopy(val.Engines, cmd.val.Engines)\n\t}\n\tif cmd.val.allrs != nil {\n\t\tval.allrs = make([]RunningScript, len(cmd.val.allrs))\n\t\tfor i, rs := range cmd.val.allrs {\n\t\t\tval.allrs[i] = RunningScript{\n\t\t\t\tName:     rs.Name,\n\t\t\t\tDuration: rs.Duration,\n\t\t\t}\n\t\t\tif rs.Command != nil {\n\t\t\t\tval.allrs[i].Command = make([]string, len(rs.Command))\n\t\t\t\tcopy(val.allrs[i].Command, rs.Command)\n\t\t\t}\n\t\t}\n\t}\n\treturn &FunctionStatsCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\n// LCSQuery is a parameter used for the LCS command\ntype LCSQuery struct {\n\tKey1         string\n\tKey2         string\n\tLen          bool\n\tIdx          bool\n\tMinMatchLen  int\n\tWithMatchLen bool\n}\n\n// LCSMatch is the result set of the LCS command.\ntype LCSMatch struct {\n\tMatchString string\n\tMatches     []LCSMatchedPosition\n\tLen         int64\n}\n\ntype LCSMatchedPosition struct {\n\tKey1 LCSPosition\n\tKey2 LCSPosition\n\n\t// only for withMatchLen is true\n\tMatchLen int64\n}\n\ntype LCSPosition struct {\n\tStart int64\n\tEnd   int64\n}\n\ntype LCSCmd struct {\n\tbaseCmd\n\n\t// 1: match string\n\t// 2: match len\n\t// 3: match idx LCSMatch\n\treadType uint8\n\tval      *LCSMatch\n}\n\nfunc NewLCSCmd(ctx context.Context, q *LCSQuery) *LCSCmd {\n\targs := make([]interface{}, 3, 7)\n\targs[0] = \"lcs\"\n\targs[1] = q.Key1\n\targs[2] = q.Key2\n\n\tcmd := &LCSCmd{readType: 1}\n\tif q.Len {\n\t\tcmd.readType = 2\n\t\targs = append(args, \"len\")\n\t} else if q.Idx {\n\t\tcmd.readType = 3\n\t\targs = append(args, \"idx\")\n\t\tif q.MinMatchLen != 0 {\n\t\t\targs = append(args, \"minmatchlen\", q.MinMatchLen)\n\t\t}\n\t\tif q.WithMatchLen {\n\t\t\targs = append(args, \"withmatchlen\")\n\t\t}\n\t}\n\tcmd.baseCmd = baseCmd{\n\t\tctx:     ctx,\n\t\targs:    args,\n\t\tcmdType: CmdTypeLCS,\n\t}\n\n\treturn cmd\n}\n\nfunc (cmd *LCSCmd) SetVal(val *LCSMatch) {\n\tcmd.val = val\n}\n\nfunc (cmd *LCSCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *LCSCmd) Val() *LCSMatch {\n\treturn cmd.val\n}\n\nfunc (cmd *LCSCmd) Result() (*LCSMatch, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *LCSCmd) readReply(rd *proto.Reader) (err error) {\n\tlcs := &LCSMatch{}\n\tswitch cmd.readType {\n\tcase 1:\n\t\t// match string\n\t\tif lcs.MatchString, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase 2:\n\t\t// match len\n\t\tif lcs.Len, err = rd.ReadInt(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase 3:\n\t\t// read LCSMatch\n\t\tif err = rd.ReadFixedMapLen(2); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// read matches or len field\n\t\tfor i := 0; i < 2; i++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"matches\":\n\t\t\t\t// read array of matched positions\n\t\t\t\tif lcs.Matches, err = cmd.readMatchedPositions(rd); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase \"len\":\n\t\t\t\t// read match length\n\t\t\t\tif lcs.Len, err = rd.ReadInt(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tcmd.val = lcs\n\treturn nil\n}\n\nfunc (cmd *LCSCmd) readMatchedPositions(rd *proto.Reader) ([]LCSMatchedPosition, error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpositions := make([]LCSMatchedPosition, n)\n\tfor i := 0; i < n; i++ {\n\t\tpn, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif positions[i].Key1, err = cmd.readPosition(rd); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif positions[i].Key2, err = cmd.readPosition(rd); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// read match length if WithMatchLen is true\n\t\tif pn > 2 {\n\t\t\tif positions[i].MatchLen, err = rd.ReadInt(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn positions, nil\n}\n\nfunc (cmd *LCSCmd) readPosition(rd *proto.Reader) (pos LCSPosition, err error) {\n\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\treturn pos, err\n\t}\n\tif pos.Start, err = rd.ReadInt(); err != nil {\n\t\treturn pos, err\n\t}\n\tif pos.End, err = rd.ReadInt(); err != nil {\n\t\treturn pos, err\n\t}\n\n\treturn pos, nil\n}\n\nfunc (cmd *LCSCmd) Clone() Cmder {\n\tvar val *LCSMatch\n\tif cmd.val != nil {\n\t\tval = &LCSMatch{\n\t\t\tMatchString: cmd.val.MatchString,\n\t\t\tLen:         cmd.val.Len,\n\t\t}\n\t\tif cmd.val.Matches != nil {\n\t\t\tval.Matches = make([]LCSMatchedPosition, len(cmd.val.Matches))\n\t\t\tcopy(val.Matches, cmd.val.Matches)\n\t\t}\n\t}\n\treturn &LCSCmd{\n\t\tbaseCmd:  cmd.cloneBaseCmd(),\n\t\treadType: cmd.readType,\n\t\tval:      val,\n\t}\n}\n\n// ------------------------------------------------------------------------\n\ntype KeyFlags struct {\n\tKey   string\n\tFlags []string\n}\n\ntype KeyFlagsCmd struct {\n\tbaseCmd\n\n\tval []KeyFlags\n}\n\nvar _ Cmder = (*KeyFlagsCmd)(nil)\n\nfunc NewKeyFlagsCmd(ctx context.Context, args ...interface{}) *KeyFlagsCmd {\n\treturn &KeyFlagsCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeKeyFlags,\n\t\t},\n\t}\n}\n\nfunc (cmd *KeyFlagsCmd) SetVal(val []KeyFlags) {\n\tcmd.val = val\n}\n\nfunc (cmd *KeyFlagsCmd) Val() []KeyFlags {\n\treturn cmd.val\n}\n\nfunc (cmd *KeyFlagsCmd) Result() ([]KeyFlags, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *KeyFlagsCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *KeyFlagsCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif n == 0 {\n\t\tcmd.val = make([]KeyFlags, 0)\n\t\treturn nil\n\t}\n\n\tcmd.val = make([]KeyFlags, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\n\t\tif err = rd.ReadFixedArrayLen(2); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cmd.val[i].Key, err = rd.ReadString(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tflagsLen, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Flags = make([]string, flagsLen)\n\n\t\tfor j := 0; j < flagsLen; j++ {\n\t\t\tif cmd.val[i].Flags[j], err = rd.ReadString(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *KeyFlagsCmd) Clone() Cmder {\n\tvar val []KeyFlags\n\tif cmd.val != nil {\n\t\tval = make([]KeyFlags, len(cmd.val))\n\t\tfor i, kf := range cmd.val {\n\t\t\tval[i] = KeyFlags{\n\t\t\t\tKey: kf.Key,\n\t\t\t}\n\t\t\tif kf.Flags != nil {\n\t\t\t\tval[i].Flags = make([]string, len(kf.Flags))\n\t\t\t\tcopy(val[i].Flags, kf.Flags)\n\t\t\t}\n\t\t}\n\t}\n\treturn &KeyFlagsCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// ---------------------------------------------------------------------------------------------------\n\ntype ClusterLink struct {\n\tDirection           string\n\tNode                string\n\tCreateTime          int64\n\tEvents              string\n\tSendBufferAllocated int64\n\tSendBufferUsed      int64\n}\n\ntype ClusterLinksCmd struct {\n\tbaseCmd\n\n\tval []ClusterLink\n}\n\nvar _ Cmder = (*ClusterLinksCmd)(nil)\n\nfunc NewClusterLinksCmd(ctx context.Context, args ...interface{}) *ClusterLinksCmd {\n\treturn &ClusterLinksCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeClusterLinks,\n\t\t},\n\t}\n}\n\nfunc (cmd *ClusterLinksCmd) SetVal(val []ClusterLink) {\n\tcmd.val = val\n}\n\nfunc (cmd *ClusterLinksCmd) Val() []ClusterLink {\n\treturn cmd.val\n}\n\nfunc (cmd *ClusterLinksCmd) Result() ([]ClusterLink, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ClusterLinksCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ClusterLinksCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]ClusterLink, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tm, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor j := 0; j < m; j++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"direction\":\n\t\t\t\tcmd.val[i].Direction, err = rd.ReadString()\n\t\t\tcase \"node\":\n\t\t\t\tcmd.val[i].Node, err = rd.ReadString()\n\t\t\tcase \"create-time\":\n\t\t\t\tcmd.val[i].CreateTime, err = rd.ReadInt()\n\t\t\tcase \"events\":\n\t\t\t\tcmd.val[i].Events, err = rd.ReadString()\n\t\t\tcase \"send-buffer-allocated\":\n\t\t\t\tcmd.val[i].SendBufferAllocated, err = rd.ReadInt()\n\t\t\tcase \"send-buffer-used\":\n\t\t\t\tcmd.val[i].SendBufferUsed, err = rd.ReadInt()\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"redis: unexpected key %q in CLUSTER LINKS reply\", key)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ClusterLinksCmd) Clone() Cmder {\n\tvar val []ClusterLink\n\tif cmd.val != nil {\n\t\tval = make([]ClusterLink, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &ClusterLinksCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// ------------------------------------------------------------------------------------------------------------------\n\ntype SlotRange struct {\n\tStart int64\n\tEnd   int64\n}\n\ntype Node struct {\n\tID                string\n\tEndpoint          string\n\tIP                string\n\tHostname          string\n\tPort              int64\n\tTLSPort           int64\n\tRole              string\n\tReplicationOffset int64\n\tHealth            string\n}\n\ntype ClusterShard struct {\n\tSlots []SlotRange\n\tNodes []Node\n}\n\ntype ClusterShardsCmd struct {\n\tbaseCmd\n\n\tval []ClusterShard\n}\n\nvar _ Cmder = (*ClusterShardsCmd)(nil)\n\nfunc NewClusterShardsCmd(ctx context.Context, args ...interface{}) *ClusterShardsCmd {\n\treturn &ClusterShardsCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeClusterShards,\n\t\t},\n\t}\n}\n\nfunc (cmd *ClusterShardsCmd) SetVal(val []ClusterShard) {\n\tcmd.val = val\n}\n\nfunc (cmd *ClusterShardsCmd) Val() []ClusterShard {\n\treturn cmd.val\n}\n\nfunc (cmd *ClusterShardsCmd) Result() ([]ClusterShard, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ClusterShardsCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]ClusterShard, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tm, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor j := 0; j < m; j++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"slots\":\n\t\t\t\tl, err := rd.ReadArrayLen()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfor k := 0; k < l; k += 2 {\n\t\t\t\t\tstart, err := rd.ReadInt()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tend, err := rd.ReadInt()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tcmd.val[i].Slots = append(cmd.val[i].Slots, SlotRange{Start: start, End: end})\n\t\t\t\t}\n\t\t\tcase \"nodes\":\n\t\t\t\tnodesLen, err := rd.ReadArrayLen()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tcmd.val[i].Nodes = make([]Node, nodesLen)\n\t\t\t\tfor k := 0; k < nodesLen; k++ {\n\t\t\t\t\tnodeMapLen, err := rd.ReadMapLen()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tfor l := 0; l < nodeMapLen; l++ {\n\t\t\t\t\t\tnodeKey, err := rd.ReadString()\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tswitch nodeKey {\n\t\t\t\t\t\tcase \"id\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].ID, err = rd.ReadString()\n\t\t\t\t\t\tcase \"endpoint\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].Endpoint, err = rd.ReadString()\n\t\t\t\t\t\tcase \"ip\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].IP, err = rd.ReadString()\n\t\t\t\t\t\tcase \"hostname\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].Hostname, err = rd.ReadString()\n\t\t\t\t\t\tcase \"port\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].Port, err = rd.ReadInt()\n\t\t\t\t\t\tcase \"tls-port\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].TLSPort, err = rd.ReadInt()\n\t\t\t\t\t\tcase \"role\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].Role, err = rd.ReadString()\n\t\t\t\t\t\tcase \"replication-offset\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].ReplicationOffset, err = rd.ReadInt()\n\t\t\t\t\t\tcase \"health\":\n\t\t\t\t\t\t\tcmd.val[i].Nodes[k].Health, err = rd.ReadString()\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\treturn fmt.Errorf(\"redis: unexpected key %q in CLUSTER SHARDS node reply\", nodeKey)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"redis: unexpected key %q in CLUSTER SHARDS reply\", key)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ClusterShardsCmd) Clone() Cmder {\n\tvar val []ClusterShard\n\tif cmd.val != nil {\n\t\tval = make([]ClusterShard, len(cmd.val))\n\t\tfor i, shard := range cmd.val {\n\t\t\tval[i] = ClusterShard{}\n\t\t\tif shard.Slots != nil {\n\t\t\t\tval[i].Slots = make([]SlotRange, len(shard.Slots))\n\t\t\t\tcopy(val[i].Slots, shard.Slots)\n\t\t\t}\n\t\t\tif shard.Nodes != nil {\n\t\t\t\tval[i].Nodes = make([]Node, len(shard.Nodes))\n\t\t\t\tcopy(val[i].Nodes, shard.Nodes)\n\t\t\t}\n\t\t}\n\t}\n\treturn &ClusterShardsCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// -----------------------------------------\n\ntype RankScore struct {\n\tRank  int64\n\tScore float64\n}\n\ntype RankWithScoreCmd struct {\n\tbaseCmd\n\n\tval RankScore\n}\n\nvar _ Cmder = (*RankWithScoreCmd)(nil)\n\nfunc NewRankWithScoreCmd(ctx context.Context, args ...interface{}) *RankWithScoreCmd {\n\treturn &RankWithScoreCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeRankWithScore,\n\t\t},\n\t}\n}\n\nfunc (cmd *RankWithScoreCmd) SetVal(val RankScore) {\n\tcmd.val = val\n}\n\nfunc (cmd *RankWithScoreCmd) Val() RankScore {\n\treturn cmd.val\n}\n\nfunc (cmd *RankWithScoreCmd) Result() (RankScore, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *RankWithScoreCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *RankWithScoreCmd) readReply(rd *proto.Reader) error {\n\tif err := rd.ReadFixedArrayLen(2); err != nil {\n\t\treturn err\n\t}\n\n\trank, err := rd.ReadInt()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tscore, err := rd.ReadFloat()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = RankScore{Rank: rank, Score: score}\n\n\treturn nil\n}\n\nfunc (cmd *RankWithScoreCmd) Clone() Cmder {\n\treturn &RankWithScoreCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // RankScore is a simple struct, can be copied directly\n\t}\n}\n\n// --------------------------------------------------------------------------------------------------\n\n// ClientFlags is redis-server client flags, copy from redis/src/server.h (redis 7.0)\ntype ClientFlags uint64\n\nconst (\n\tClientSlave            ClientFlags = 1 << 0  /* This client is a replica */\n\tClientMaster           ClientFlags = 1 << 1  /* This client is a master */\n\tClientMonitor          ClientFlags = 1 << 2  /* This client is a slave monitor, see MONITOR */\n\tClientMulti            ClientFlags = 1 << 3  /* This client is in a MULTI context */\n\tClientBlocked          ClientFlags = 1 << 4  /* The client is waiting in a blocking operation */\n\tClientDirtyCAS         ClientFlags = 1 << 5  /* Watched keys modified. EXEC will fail. */\n\tClientCloseAfterReply  ClientFlags = 1 << 6  /* Close after writing entire reply. */\n\tClientUnBlocked        ClientFlags = 1 << 7  /* This client was unblocked and is stored in server.unblocked_clients */\n\tClientScript           ClientFlags = 1 << 8  /* This is a non-connected client used by Lua */\n\tClientAsking           ClientFlags = 1 << 9  /* Client issued the ASKING command */\n\tClientCloseASAP        ClientFlags = 1 << 10 /* Close this client ASAP */\n\tClientUnixSocket       ClientFlags = 1 << 11 /* Client connected via Unix domain socket */\n\tClientDirtyExec        ClientFlags = 1 << 12 /* EXEC will fail for errors while queueing */\n\tClientMasterForceReply ClientFlags = 1 << 13 /* Queue replies even if is master */\n\tClientForceAOF         ClientFlags = 1 << 14 /* Force AOF propagation of current cmd. */\n\tClientForceRepl        ClientFlags = 1 << 15 /* Force replication of current cmd. */\n\tClientPrePSync         ClientFlags = 1 << 16 /* Instance don't understand PSYNC. */\n\tClientReadOnly         ClientFlags = 1 << 17 /* Cluster client is in read-only state. */\n\tClientPubSub           ClientFlags = 1 << 18 /* Client is in Pub/Sub mode. */\n\tClientPreventAOFProp   ClientFlags = 1 << 19 /* Don't propagate to AOF. */\n\tClientPreventReplProp  ClientFlags = 1 << 20 /* Don't propagate to slaves. */\n\tClientPreventProp      ClientFlags = ClientPreventAOFProp | ClientPreventReplProp\n\tClientPendingWrite     ClientFlags = 1 << 21 /* Client has output to send but a-write handler is yet not installed. */\n\tClientReplyOff         ClientFlags = 1 << 22 /* Don't send replies to client. */\n\tClientReplySkipNext    ClientFlags = 1 << 23 /* Set ClientREPLY_SKIP for next cmd */\n\tClientReplySkip        ClientFlags = 1 << 24 /* Don't send just this reply. */\n\tClientLuaDebug         ClientFlags = 1 << 25 /* Run EVAL in debug mode. */\n\tClientLuaDebugSync     ClientFlags = 1 << 26 /* EVAL debugging without fork() */\n\tClientModule           ClientFlags = 1 << 27 /* Non connected client used by some module. */\n\tClientProtected        ClientFlags = 1 << 28 /* Client should not be freed for now. */\n\tClientExecutingCommand ClientFlags = 1 << 29 /* Indicates that the client is currently in the process of handling\n\t   a command. usually this will be marked only during call()\n\t   however, blocked clients might have this flag kept until they\n\t   will try to reprocess the command. */\n\tClientPendingCommand      ClientFlags = 1 << 30 /* Indicates the client has a fully * parsed command ready for execution. */\n\tClientTracking            ClientFlags = 1 << 31 /* Client enabled keys tracking in order to perform client side caching. */\n\tClientTrackingBrokenRedir ClientFlags = 1 << 32 /* Target client is invalid. */\n\tClientTrackingBCAST       ClientFlags = 1 << 33 /* Tracking in BCAST mode. */\n\tClientTrackingOptIn       ClientFlags = 1 << 34 /* Tracking in opt-in mode. */\n\tClientTrackingOptOut      ClientFlags = 1 << 35 /* Tracking in opt-out mode. */\n\tClientTrackingCaching     ClientFlags = 1 << 36 /* CACHING yes/no was given, depending on optin/optout mode. */\n\tClientTrackingNoLoop      ClientFlags = 1 << 37 /* Don't send invalidation messages about writes performed by myself.*/\n\tClientInTimeoutTable      ClientFlags = 1 << 38 /* This client is in the timeout table. */\n\tClientProtocolError       ClientFlags = 1 << 39 /* Protocol error chatting with it. */\n\tClientCloseAfterCommand   ClientFlags = 1 << 40 /* Close after executing commands * and writing entire reply. */\n\tClientDenyBlocking        ClientFlags = 1 << 41 /* Indicate that the client should not be blocked. currently, turned on inside MULTI, Lua, RM_Call, and AOF client */\n\tClientReplRDBOnly         ClientFlags = 1 << 42 /* This client is a replica that only wants RDB without replication buffer. */\n\tClientNoEvict             ClientFlags = 1 << 43 /* This client is protected against client memory eviction. */\n\tClientAllowOOM            ClientFlags = 1 << 44 /* Client used by RM_Call is allowed to fully execute scripts even when in OOM */\n\tClientNoTouch             ClientFlags = 1 << 45 /* This client will not touch LFU/LRU stats. */\n\tClientPushing             ClientFlags = 1 << 46 /* This client is pushing notifications. */\n)\n\n// ClientInfo is redis-server ClientInfo, not go-redis *Client\ntype ClientInfo struct {\n\tID                 int64         // redis version 2.8.12, a unique 64-bit client ID\n\tAddr               string        // address/port of the client\n\tLAddr              string        // address/port of local address client connected to (bind address)\n\tFD                 int64         // file descriptor corresponding to the socket\n\tName               string        // the name set by the client with CLIENT SETNAME\n\tAge                time.Duration // total duration of the connection in seconds\n\tIdle               time.Duration // idle time of the connection in seconds\n\tFlags              ClientFlags   // client flags (see below)\n\tDB                 int           // current database ID\n\tSub                int           // number of channel subscriptions\n\tPSub               int           // number of pattern matching subscriptions\n\tSSub               int           // redis version 7.0.3, number of shard channel subscriptions\n\tMulti              int           // number of commands in a MULTI/EXEC context\n\tWatch              int           // redis version 7.4 RC1, number of keys this client is currently watching.\n\tQueryBuf           int           // qbuf, query buffer length (0 means no query pending)\n\tQueryBufFree       int           // qbuf-free, free space of the query buffer (0 means the buffer is full)\n\tArgvMem            int           // incomplete arguments for the next command (already extracted from query buffer)\n\tMultiMem           int           // redis version 7.0, memory is used up by buffered multi commands\n\tBufferSize         int           // rbs, usable size of buffer\n\tBufferPeak         int           // rbp, peak used size of buffer in last 5 sec interval\n\tOutputBufferLength int           // obl, output buffer length\n\tOutputListLength   int           // oll, output list length (replies are queued in this list when the buffer is full)\n\tOutputMemory       int           // omem, output buffer memory usage\n\tTotalMemory        int           // tot-mem, total memory consumed by this client in its various buffers\n\tTotalNetIn         int           // tot-net-in, total network input\n\tTotalNetOut        int           // tot-net-out, total network output\n\tTotalCmds          int           // tot-cmds, total number of commands processed\n\tIoThread           int           // io-thread id\n\tEvents             string        // file descriptor events (see below)\n\tLastCmd            string        // cmd, last command played\n\tUser               string        // the authenticated username of the client\n\tRedir              int64         // client id of current client tracking redirection\n\tResp               int           // redis version 7.0, client RESP protocol version\n\tLibName            string        // redis version 7.2, client library name\n\tLibVer             string        // redis version 7.2, client library version\n}\n\ntype ClientInfoCmd struct {\n\tbaseCmd\n\n\tval *ClientInfo\n}\n\nvar _ Cmder = (*ClientInfoCmd)(nil)\n\nfunc NewClientInfoCmd(ctx context.Context, args ...interface{}) *ClientInfoCmd {\n\treturn &ClientInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeClientInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *ClientInfoCmd) SetVal(val *ClientInfo) {\n\tcmd.val = val\n}\n\nfunc (cmd *ClientInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ClientInfoCmd) Val() *ClientInfo {\n\treturn cmd.val\n}\n\nfunc (cmd *ClientInfoCmd) Result() (*ClientInfo, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ClientInfoCmd) readReply(rd *proto.Reader) (err error) {\n\ttxt, err := rd.ReadString()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// sds o = catClientInfoString(sdsempty(), c);\n\t// o = sdscatlen(o,\"\\n\",1);\n\t// addReplyVerbatim(c,o,sdslen(o),\"txt\");\n\t// sdsfree(o);\n\tcmd.val, err = parseClientInfo(strings.TrimSpace(txt))\n\treturn err\n}\n\n// fmt.Sscanf() cannot handle null values\nfunc parseClientInfo(txt string) (info *ClientInfo, err error) {\n\tinfo = &ClientInfo{}\n\tfor _, s := range strings.Split(txt, \" \") {\n\t\tkv := strings.Split(s, \"=\")\n\t\tif len(kv) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"redis: unexpected client info data (%s)\", s)\n\t\t}\n\t\tkey, val := kv[0], kv[1]\n\n\t\tswitch key {\n\t\tcase \"id\":\n\t\t\tinfo.ID, err = strconv.ParseInt(val, 10, 64)\n\t\tcase \"addr\":\n\t\t\tinfo.Addr = val\n\t\tcase \"laddr\":\n\t\t\tinfo.LAddr = val\n\t\tcase \"fd\":\n\t\t\tinfo.FD, err = strconv.ParseInt(val, 10, 64)\n\t\tcase \"name\":\n\t\t\tinfo.Name = val\n\t\tcase \"age\":\n\t\t\tvar age int\n\t\t\tif age, err = strconv.Atoi(val); err == nil {\n\t\t\t\tinfo.Age = time.Duration(age) * time.Second\n\t\t\t}\n\t\tcase \"idle\":\n\t\t\tvar idle int\n\t\t\tif idle, err = strconv.Atoi(val); err == nil {\n\t\t\t\tinfo.Idle = time.Duration(idle) * time.Second\n\t\t\t}\n\t\tcase \"flags\":\n\t\t\tif val == \"N\" {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tfor i := 0; i < len(val); i++ {\n\t\t\t\tswitch val[i] {\n\t\t\t\tcase 'S':\n\t\t\t\t\tinfo.Flags |= ClientSlave\n\t\t\t\tcase 'O':\n\t\t\t\t\tinfo.Flags |= ClientSlave | ClientMonitor\n\t\t\t\tcase 'M':\n\t\t\t\t\tinfo.Flags |= ClientMaster\n\t\t\t\tcase 'P':\n\t\t\t\t\tinfo.Flags |= ClientPubSub\n\t\t\t\tcase 'x':\n\t\t\t\t\tinfo.Flags |= ClientMulti\n\t\t\t\tcase 'b':\n\t\t\t\t\tinfo.Flags |= ClientBlocked\n\t\t\t\tcase 't':\n\t\t\t\t\tinfo.Flags |= ClientTracking\n\t\t\t\tcase 'R':\n\t\t\t\t\tinfo.Flags |= ClientTrackingBrokenRedir\n\t\t\t\tcase 'B':\n\t\t\t\t\tinfo.Flags |= ClientTrackingBCAST\n\t\t\t\tcase 'd':\n\t\t\t\t\tinfo.Flags |= ClientDirtyCAS\n\t\t\t\tcase 'c':\n\t\t\t\t\tinfo.Flags |= ClientCloseAfterCommand\n\t\t\t\tcase 'u':\n\t\t\t\t\tinfo.Flags |= ClientUnBlocked\n\t\t\t\tcase 'A':\n\t\t\t\t\tinfo.Flags |= ClientCloseASAP\n\t\t\t\tcase 'U':\n\t\t\t\t\tinfo.Flags |= ClientUnixSocket\n\t\t\t\tcase 'r':\n\t\t\t\t\tinfo.Flags |= ClientReadOnly\n\t\t\t\tcase 'e':\n\t\t\t\t\tinfo.Flags |= ClientNoEvict\n\t\t\t\tcase 'T':\n\t\t\t\t\tinfo.Flags |= ClientNoTouch\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, fmt.Errorf(\"redis: unexpected client info flags(%s)\", string(val[i]))\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"db\":\n\t\t\tinfo.DB, err = strconv.Atoi(val)\n\t\tcase \"sub\":\n\t\t\tinfo.Sub, err = strconv.Atoi(val)\n\t\tcase \"psub\":\n\t\t\tinfo.PSub, err = strconv.Atoi(val)\n\t\tcase \"ssub\":\n\t\t\tinfo.SSub, err = strconv.Atoi(val)\n\t\tcase \"multi\":\n\t\t\tinfo.Multi, err = strconv.Atoi(val)\n\t\tcase \"watch\":\n\t\t\tinfo.Watch, err = strconv.Atoi(val)\n\t\tcase \"qbuf\":\n\t\t\tinfo.QueryBuf, err = strconv.Atoi(val)\n\t\tcase \"qbuf-free\":\n\t\t\tinfo.QueryBufFree, err = strconv.Atoi(val)\n\t\tcase \"argv-mem\":\n\t\t\tinfo.ArgvMem, err = strconv.Atoi(val)\n\t\tcase \"multi-mem\":\n\t\t\tinfo.MultiMem, err = strconv.Atoi(val)\n\t\tcase \"rbs\":\n\t\t\tinfo.BufferSize, err = strconv.Atoi(val)\n\t\tcase \"rbp\":\n\t\t\tinfo.BufferPeak, err = strconv.Atoi(val)\n\t\tcase \"obl\":\n\t\t\tinfo.OutputBufferLength, err = strconv.Atoi(val)\n\t\tcase \"oll\":\n\t\t\tinfo.OutputListLength, err = strconv.Atoi(val)\n\t\tcase \"omem\":\n\t\t\tinfo.OutputMemory, err = strconv.Atoi(val)\n\t\tcase \"tot-mem\":\n\t\t\tinfo.TotalMemory, err = strconv.Atoi(val)\n\t\tcase \"tot-net-in\":\n\t\t\tinfo.TotalNetIn, err = strconv.Atoi(val)\n\t\tcase \"tot-net-out\":\n\t\t\tinfo.TotalNetOut, err = strconv.Atoi(val)\n\t\tcase \"tot-cmds\":\n\t\t\tinfo.TotalCmds, err = strconv.Atoi(val)\n\t\tcase \"events\":\n\t\t\tinfo.Events = val\n\t\tcase \"cmd\":\n\t\t\tinfo.LastCmd = val\n\t\tcase \"user\":\n\t\t\tinfo.User = val\n\t\tcase \"redir\":\n\t\t\tinfo.Redir, err = strconv.ParseInt(val, 10, 64)\n\t\tcase \"resp\":\n\t\t\tinfo.Resp, err = strconv.Atoi(val)\n\t\tcase \"lib-name\":\n\t\t\tinfo.LibName = val\n\t\tcase \"lib-ver\":\n\t\t\tinfo.LibVer = val\n\t\tcase \"io-thread\":\n\t\t\tinfo.IoThread, err = strconv.Atoi(val)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"redis: unexpected client info key(%s)\", key)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn info, nil\n}\n\nfunc (cmd *ClientInfoCmd) Clone() Cmder {\n\tvar val *ClientInfo\n\tif cmd.val != nil {\n\t\tval = &ClientInfo{\n\t\t\tID:                 cmd.val.ID,\n\t\t\tAddr:               cmd.val.Addr,\n\t\t\tLAddr:              cmd.val.LAddr,\n\t\t\tFD:                 cmd.val.FD,\n\t\t\tName:               cmd.val.Name,\n\t\t\tAge:                cmd.val.Age,\n\t\t\tIdle:               cmd.val.Idle,\n\t\t\tFlags:              cmd.val.Flags,\n\t\t\tDB:                 cmd.val.DB,\n\t\t\tSub:                cmd.val.Sub,\n\t\t\tPSub:               cmd.val.PSub,\n\t\t\tSSub:               cmd.val.SSub,\n\t\t\tMulti:              cmd.val.Multi,\n\t\t\tWatch:              cmd.val.Watch,\n\t\t\tQueryBuf:           cmd.val.QueryBuf,\n\t\t\tQueryBufFree:       cmd.val.QueryBufFree,\n\t\t\tArgvMem:            cmd.val.ArgvMem,\n\t\t\tMultiMem:           cmd.val.MultiMem,\n\t\t\tBufferSize:         cmd.val.BufferSize,\n\t\t\tBufferPeak:         cmd.val.BufferPeak,\n\t\t\tOutputBufferLength: cmd.val.OutputBufferLength,\n\t\t\tOutputListLength:   cmd.val.OutputListLength,\n\t\t\tOutputMemory:       cmd.val.OutputMemory,\n\t\t\tTotalMemory:        cmd.val.TotalMemory,\n\t\t\tIoThread:           cmd.val.IoThread,\n\t\t\tEvents:             cmd.val.Events,\n\t\t\tLastCmd:            cmd.val.LastCmd,\n\t\t\tUser:               cmd.val.User,\n\t\t\tRedir:              cmd.val.Redir,\n\t\t\tResp:               cmd.val.Resp,\n\t\t\tLibName:            cmd.val.LibName,\n\t\t\tLibVer:             cmd.val.LibVer,\n\t\t}\n\t}\n\treturn &ClientInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// -------------------------------------------\n\ntype ACLLogEntry struct {\n\tCount                int64\n\tReason               string\n\tContext              string\n\tObject               string\n\tUsername             string\n\tAgeSeconds           float64\n\tClientInfo           *ClientInfo\n\tEntryID              int64\n\tTimestampCreated     int64\n\tTimestampLastUpdated int64\n}\n\ntype ACLLogCmd struct {\n\tbaseCmd\n\n\tval []*ACLLogEntry\n}\n\nvar _ Cmder = (*ACLLogCmd)(nil)\n\nfunc NewACLLogCmd(ctx context.Context, args ...interface{}) *ACLLogCmd {\n\treturn &ACLLogCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeACLLog,\n\t\t},\n\t}\n}\n\nfunc (cmd *ACLLogCmd) SetVal(val []*ACLLogEntry) {\n\tcmd.val = val\n}\n\nfunc (cmd *ACLLogCmd) Val() []*ACLLogEntry {\n\treturn cmd.val\n}\n\nfunc (cmd *ACLLogCmd) Result() ([]*ACLLogEntry, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ACLLogCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ACLLogCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make([]*ACLLogEntry, n)\n\tfor i := 0; i < n; i++ {\n\t\tcmd.val[i] = &ACLLogEntry{}\n\t\tentry := cmd.val[i]\n\t\trespLen, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor j := 0; j < respLen; j++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch key {\n\t\t\tcase \"count\":\n\t\t\t\tentry.Count, err = rd.ReadInt()\n\t\t\tcase \"reason\":\n\t\t\t\tentry.Reason, err = rd.ReadString()\n\t\t\tcase \"context\":\n\t\t\t\tentry.Context, err = rd.ReadString()\n\t\t\tcase \"object\":\n\t\t\t\tentry.Object, err = rd.ReadString()\n\t\t\tcase \"username\":\n\t\t\t\tentry.Username, err = rd.ReadString()\n\t\t\tcase \"age-seconds\":\n\t\t\t\tentry.AgeSeconds, err = rd.ReadFloat()\n\t\t\tcase \"client-info\":\n\t\t\t\ttxt, err := rd.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tentry.ClientInfo, err = parseClientInfo(strings.TrimSpace(txt))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase \"entry-id\":\n\t\t\t\tentry.EntryID, err = rd.ReadInt()\n\t\t\tcase \"timestamp-created\":\n\t\t\t\tentry.TimestampCreated, err = rd.ReadInt()\n\t\t\tcase \"timestamp-last-updated\":\n\t\t\t\tentry.TimestampLastUpdated, err = rd.ReadInt()\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"redis: unexpected key %q in ACL LOG reply\", key)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ACLLogCmd) Clone() Cmder {\n\tvar val []*ACLLogEntry\n\tif cmd.val != nil {\n\t\tval = make([]*ACLLogEntry, len(cmd.val))\n\t\tfor i, entry := range cmd.val {\n\t\t\tif entry != nil {\n\t\t\t\tval[i] = &ACLLogEntry{\n\t\t\t\t\tCount:                entry.Count,\n\t\t\t\t\tReason:               entry.Reason,\n\t\t\t\t\tContext:              entry.Context,\n\t\t\t\t\tObject:               entry.Object,\n\t\t\t\t\tUsername:             entry.Username,\n\t\t\t\t\tAgeSeconds:           entry.AgeSeconds,\n\t\t\t\t\tEntryID:              entry.EntryID,\n\t\t\t\t\tTimestampCreated:     entry.TimestampCreated,\n\t\t\t\t\tTimestampLastUpdated: entry.TimestampLastUpdated,\n\t\t\t\t}\n\t\t\t\t// Clone ClientInfo if present\n\t\t\t\tif entry.ClientInfo != nil {\n\t\t\t\t\tval[i].ClientInfo = &ClientInfo{\n\t\t\t\t\t\tID:                 entry.ClientInfo.ID,\n\t\t\t\t\t\tAddr:               entry.ClientInfo.Addr,\n\t\t\t\t\t\tLAddr:              entry.ClientInfo.LAddr,\n\t\t\t\t\t\tFD:                 entry.ClientInfo.FD,\n\t\t\t\t\t\tName:               entry.ClientInfo.Name,\n\t\t\t\t\t\tAge:                entry.ClientInfo.Age,\n\t\t\t\t\t\tIdle:               entry.ClientInfo.Idle,\n\t\t\t\t\t\tFlags:              entry.ClientInfo.Flags,\n\t\t\t\t\t\tDB:                 entry.ClientInfo.DB,\n\t\t\t\t\t\tSub:                entry.ClientInfo.Sub,\n\t\t\t\t\t\tPSub:               entry.ClientInfo.PSub,\n\t\t\t\t\t\tSSub:               entry.ClientInfo.SSub,\n\t\t\t\t\t\tMulti:              entry.ClientInfo.Multi,\n\t\t\t\t\t\tWatch:              entry.ClientInfo.Watch,\n\t\t\t\t\t\tQueryBuf:           entry.ClientInfo.QueryBuf,\n\t\t\t\t\t\tQueryBufFree:       entry.ClientInfo.QueryBufFree,\n\t\t\t\t\t\tArgvMem:            entry.ClientInfo.ArgvMem,\n\t\t\t\t\t\tMultiMem:           entry.ClientInfo.MultiMem,\n\t\t\t\t\t\tBufferSize:         entry.ClientInfo.BufferSize,\n\t\t\t\t\t\tBufferPeak:         entry.ClientInfo.BufferPeak,\n\t\t\t\t\t\tOutputBufferLength: entry.ClientInfo.OutputBufferLength,\n\t\t\t\t\t\tOutputListLength:   entry.ClientInfo.OutputListLength,\n\t\t\t\t\t\tOutputMemory:       entry.ClientInfo.OutputMemory,\n\t\t\t\t\t\tTotalMemory:        entry.ClientInfo.TotalMemory,\n\t\t\t\t\t\tIoThread:           entry.ClientInfo.IoThread,\n\t\t\t\t\t\tEvents:             entry.ClientInfo.Events,\n\t\t\t\t\t\tLastCmd:            entry.ClientInfo.LastCmd,\n\t\t\t\t\t\tUser:               entry.ClientInfo.User,\n\t\t\t\t\t\tRedir:              entry.ClientInfo.Redir,\n\t\t\t\t\t\tResp:               entry.ClientInfo.Resp,\n\t\t\t\t\t\tLibName:            entry.ClientInfo.LibName,\n\t\t\t\t\t\tLibVer:             entry.ClientInfo.LibVer,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &ACLLogCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// LibraryInfo holds the library info.\ntype LibraryInfo struct {\n\tLibName *string\n\tLibVer  *string\n}\n\n// WithLibraryName returns a valid LibraryInfo with library name only.\nfunc WithLibraryName(libName string) LibraryInfo {\n\treturn LibraryInfo{LibName: &libName}\n}\n\n// WithLibraryVersion returns a valid LibraryInfo with library version only.\nfunc WithLibraryVersion(libVer string) LibraryInfo {\n\treturn LibraryInfo{LibVer: &libVer}\n}\n\n// -------------------------------------------\n\ntype InfoCmd struct {\n\tbaseCmd\n\tval map[string]map[string]string\n}\n\nvar _ Cmder = (*InfoCmd)(nil)\n\nfunc NewInfoCmd(ctx context.Context, args ...interface{}) *InfoCmd {\n\treturn &InfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *InfoCmd) SetVal(val map[string]map[string]string) {\n\tcmd.val = val\n}\n\nfunc (cmd *InfoCmd) Val() map[string]map[string]string {\n\treturn cmd.val\n}\n\nfunc (cmd *InfoCmd) Result() (map[string]map[string]string, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *InfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *InfoCmd) readReply(rd *proto.Reader) error {\n\tval, err := rd.ReadString()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsection := \"\"\n\tscanner := bufio.NewScanner(strings.NewReader(val))\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, \"#\") {\n\t\t\tif cmd.val == nil {\n\t\t\t\tcmd.val = make(map[string]map[string]string)\n\t\t\t}\n\t\t\tsection = strings.TrimPrefix(line, \"# \")\n\t\t\tcmd.val[section] = make(map[string]string)\n\t\t} else if line != \"\" {\n\t\t\tif section == \"Modules\" {\n\t\t\t\tmoduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`)\n\t\t\t\tkv := moduleRe.FindStringSubmatch(line)\n\t\t\t\tif len(kv) == 3 {\n\t\t\t\t\tcmd.val[section][kv[1]] = kv[2]\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tkv := strings.SplitN(line, \":\", 2)\n\t\t\t\tif len(kv) == 2 {\n\t\t\t\t\tcmd.val[section][kv[0]] = kv[1]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *InfoCmd) Item(section, key string) string {\n\tif cmd.val == nil {\n\t\treturn \"\"\n\t} else if cmd.val[section] == nil {\n\t\treturn \"\"\n\t} else {\n\t\treturn cmd.val[section][key]\n\t}\n}\n\nfunc (cmd *InfoCmd) Clone() Cmder {\n\tvar val map[string]map[string]string\n\tif cmd.val != nil {\n\t\tval = make(map[string]map[string]string, len(cmd.val))\n\t\tfor section, sectionMap := range cmd.val {\n\t\t\tif sectionMap != nil {\n\t\t\t\tval[section] = make(map[string]string, len(sectionMap))\n\t\t\t\tfor k, v := range sectionMap {\n\t\t\t\t\tval[section][k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &InfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\ntype MonitorStatus int\n\nconst (\n\tmonitorStatusIdle MonitorStatus = iota\n\tmonitorStatusStart\n\tmonitorStatusStop\n)\n\ntype MonitorCmd struct {\n\tbaseCmd\n\tch     chan string\n\tstatus MonitorStatus\n\tmu     sync.Mutex\n}\n\nfunc newMonitorCmd(ctx context.Context, ch chan string) *MonitorCmd {\n\treturn &MonitorCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    []interface{}{\"monitor\"},\n\t\t\tcmdType: CmdTypeMonitor,\n\t\t},\n\t\tch:     ch,\n\t\tstatus: monitorStatusIdle,\n\t\tmu:     sync.Mutex{},\n\t}\n}\n\nfunc (cmd *MonitorCmd) String() string {\n\treturn cmdString(cmd, nil)\n}\n\nfunc (cmd *MonitorCmd) readReply(rd *proto.Reader) error {\n\tctx, cancel := context.WithCancel(cmd.ctx)\n\tgo func(ctx context.Context) {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\terr := cmd.readMonitor(rd, cancel)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcmd.err = err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}(ctx)\n\treturn nil\n}\n\nfunc (cmd *MonitorCmd) readMonitor(rd *proto.Reader, cancel context.CancelFunc) error {\n\tfor {\n\t\tcmd.mu.Lock()\n\t\tst := cmd.status\n\t\tpk, _ := rd.Peek(1)\n\t\tcmd.mu.Unlock()\n\t\tif len(pk) != 0 && st == monitorStatusStart {\n\t\t\tcmd.mu.Lock()\n\t\t\tline, err := rd.ReadString()\n\t\t\tcmd.mu.Unlock()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcmd.ch <- line\n\t\t}\n\t\tif st == monitorStatusStop {\n\t\t\tcancel()\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *MonitorCmd) Start() {\n\tcmd.mu.Lock()\n\tdefer cmd.mu.Unlock()\n\tcmd.status = monitorStatusStart\n}\n\nfunc (cmd *MonitorCmd) Stop() {\n\tcmd.mu.Lock()\n\tdefer cmd.mu.Unlock()\n\tcmd.status = monitorStatusStop\n}\n\ntype VectorScoreSliceCmd struct {\n\tbaseCmd\n\n\tval []VectorScore\n}\n\nvar _ Cmder = (*VectorScoreSliceCmd)(nil)\n\nfunc NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd {\n\treturn &VectorScoreSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:  ctx,\n\t\t\targs: args,\n\t\t},\n\t}\n}\n\nfunc (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) {\n\tcmd.val = val\n}\n\nfunc (cmd *VectorScoreSliceCmd) Val() []VectorScore {\n\treturn cmd.val\n}\n\nfunc (cmd *VectorScoreSliceCmd) Result() ([]VectorScore, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *VectorScoreSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd.val = make([]VectorScore, n)\n\tfor i := 0; i < n; i++ {\n\t\tname, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Name = name\n\n\t\tscore, err := rd.ReadFloat()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Score = score\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *VectorScoreSliceCmd) Clone() Cmder {\n\treturn &VectorScoreSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val,\n\t}\n}\n\nfunc (cmd *MonitorCmd) Clone() Cmder {\n\t// MonitorCmd cannot be safely cloned due to channels and goroutines\n\t// Return a new MonitorCmd with the same channel\n\treturn newMonitorCmd(cmd.ctx, cmd.ch)\n}\n\n// ExtractCommandValue extracts the value from a command result using the fast enum-based approach\nfunc ExtractCommandValue(cmd interface{}) (interface{}, error) {\n\t// First try to get the command type using the interface\n\tif cmdTypeGetter, ok := cmd.(CmdTypeGetter); ok {\n\t\tcmdType := cmdTypeGetter.GetCmdType()\n\n\t\t// Use fast type-based extraction\n\t\tswitch cmdType {\n\t\tcase CmdTypeGeneric:\n\t\t\tif genericCmd, ok := cmd.(interface {\n\t\t\t\tVal() interface{}\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn genericCmd.Val(), genericCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeString:\n\t\t\tif stringCmd, ok := cmd.(interface {\n\t\t\t\tVal() string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn stringCmd.Val(), stringCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeInt:\n\t\t\tif intCmd, ok := cmd.(interface {\n\t\t\t\tVal() int64\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn intCmd.Val(), intCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeBool:\n\t\t\tif boolCmd, ok := cmd.(interface {\n\t\t\t\tVal() bool\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn boolCmd.Val(), boolCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFloat:\n\t\t\tif floatCmd, ok := cmd.(interface {\n\t\t\t\tVal() float64\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn floatCmd.Val(), floatCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeStatus:\n\t\t\tif statusCmd, ok := cmd.(interface {\n\t\t\t\tVal() string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn statusCmd.Val(), statusCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeDuration:\n\t\t\tif durationCmd, ok := cmd.(interface {\n\t\t\t\tVal() time.Duration\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn durationCmd.Val(), durationCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeTime:\n\t\t\tif timeCmd, ok := cmd.(interface {\n\t\t\t\tVal() time.Time\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn timeCmd.Val(), timeCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeStringStructMap:\n\t\t\tif structMapCmd, ok := cmd.(interface {\n\t\t\t\tVal() map[string]struct{}\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn structMapCmd.Val(), structMapCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXMessageSlice:\n\t\t\tif xMessageSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []XMessage\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xMessageSliceCmd.Val(), xMessageSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXStreamSlice:\n\t\t\tif xStreamSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []XStream\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xStreamSliceCmd.Val(), xStreamSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXPending:\n\t\t\tif xPendingCmd, ok := cmd.(interface {\n\t\t\t\tVal() *XPending\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xPendingCmd.Val(), xPendingCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXPendingExt:\n\t\t\tif xPendingExtCmd, ok := cmd.(interface {\n\t\t\t\tVal() []XPendingExt\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xPendingExtCmd.Val(), xPendingExtCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXAutoClaim:\n\t\t\tif xAutoClaimCmd, ok := cmd.(interface {\n\t\t\t\tVal() ([]XMessage, string)\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\tmessages, start := xAutoClaimCmd.Val()\n\t\t\t\treturn CmdTypeXAutoClaimValue{messages: messages, start: start}, xAutoClaimCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXAutoClaimJustID:\n\t\t\tif xAutoClaimJustIDCmd, ok := cmd.(interface {\n\t\t\t\tVal() ([]string, string)\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\tids, start := xAutoClaimJustIDCmd.Val()\n\t\t\t\treturn CmdTypeXAutoClaimJustIDValue{ids: ids, start: start}, xAutoClaimJustIDCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXInfoConsumers:\n\t\t\tif xInfoConsumersCmd, ok := cmd.(interface {\n\t\t\t\tVal() []XInfoConsumer\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xInfoConsumersCmd.Val(), xInfoConsumersCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXInfoGroups:\n\t\t\tif xInfoGroupsCmd, ok := cmd.(interface {\n\t\t\t\tVal() []XInfoGroup\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xInfoGroupsCmd.Val(), xInfoGroupsCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXInfoStream:\n\t\t\tif xInfoStreamCmd, ok := cmd.(interface {\n\t\t\t\tVal() *XInfoStream\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xInfoStreamCmd.Val(), xInfoStreamCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeXInfoStreamFull:\n\t\t\tif xInfoStreamFullCmd, ok := cmd.(interface {\n\t\t\t\tVal() *XInfoStreamFull\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn xInfoStreamFullCmd.Val(), xInfoStreamFullCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeZSlice:\n\t\t\tif zSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []Z\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn zSliceCmd.Val(), zSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeZWithKey:\n\t\t\tif zWithKeyCmd, ok := cmd.(interface {\n\t\t\t\tVal() *ZWithKey\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn zWithKeyCmd.Val(), zWithKeyCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeScan:\n\t\t\tif scanCmd, ok := cmd.(interface {\n\t\t\t\tVal() ([]string, uint64)\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\tkeys, cursor := scanCmd.Val()\n\t\t\t\treturn CmdTypeScanValue{keys: keys, cursor: cursor}, scanCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeClusterSlots:\n\t\t\tif clusterSlotsCmd, ok := cmd.(interface {\n\t\t\t\tVal() []ClusterSlot\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn clusterSlotsCmd.Val(), clusterSlotsCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeGeoLocation:\n\t\t\tif geoLocationCmd, ok := cmd.(interface {\n\t\t\t\tVal() []GeoLocation\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn geoLocationCmd.Val(), geoLocationCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeGeoSearchLocation:\n\t\t\tif geoSearchLocationCmd, ok := cmd.(interface {\n\t\t\t\tVal() []GeoLocation\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn geoSearchLocationCmd.Val(), geoSearchLocationCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeGeoPos:\n\t\t\tif geoPosCmd, ok := cmd.(interface {\n\t\t\t\tVal() []*GeoPos\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn geoPosCmd.Val(), geoPosCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeCommandsInfo:\n\t\t\tif commandsInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() map[string]*CommandInfo\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn commandsInfoCmd.Val(), commandsInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeSlowLog:\n\t\t\tif slowLogCmd, ok := cmd.(interface {\n\t\t\t\tVal() []SlowLog\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn slowLogCmd.Val(), slowLogCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeHotKeys:\n\t\t\tif hotKeysCmd, ok := cmd.(interface {\n\t\t\t\tVal() *HotKeysResult\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn hotKeysCmd.Val(), hotKeysCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeKeyValues:\n\t\t\tif keyValuesCmd, ok := cmd.(interface {\n\t\t\t\tVal() (string, []string)\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\tkey, values := keyValuesCmd.Val()\n\t\t\t\treturn CmdTypeKeyValuesValue{key: key, values: values}, keyValuesCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeZSliceWithKey:\n\t\t\tif zSliceWithKeyCmd, ok := cmd.(interface {\n\t\t\t\tVal() (string, []Z)\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\tkey, zSlice := zSliceWithKeyCmd.Val()\n\t\t\t\treturn CmdTypeZSliceWithKeyValue{key: key, zSlice: zSlice}, zSliceWithKeyCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFunctionList:\n\t\t\tif functionListCmd, ok := cmd.(interface {\n\t\t\t\tVal() []Library\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn functionListCmd.Val(), functionListCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFunctionStats:\n\t\t\tif functionStatsCmd, ok := cmd.(interface {\n\t\t\t\tVal() FunctionStats\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn functionStatsCmd.Val(), functionStatsCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeLCS:\n\t\t\tif lcsCmd, ok := cmd.(interface {\n\t\t\t\tVal() *LCSMatch\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn lcsCmd.Val(), lcsCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeKeyFlags:\n\t\t\tif keyFlagsCmd, ok := cmd.(interface {\n\t\t\t\tVal() []KeyFlags\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn keyFlagsCmd.Val(), keyFlagsCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeClusterLinks:\n\t\t\tif clusterLinksCmd, ok := cmd.(interface {\n\t\t\t\tVal() []ClusterLink\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn clusterLinksCmd.Val(), clusterLinksCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeClusterShards:\n\t\t\tif clusterShardsCmd, ok := cmd.(interface {\n\t\t\t\tVal() []ClusterShard\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn clusterShardsCmd.Val(), clusterShardsCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeRankWithScore:\n\t\t\tif rankWithScoreCmd, ok := cmd.(interface {\n\t\t\t\tVal() RankScore\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn rankWithScoreCmd.Val(), rankWithScoreCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeClientInfo:\n\t\t\tif clientInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() *ClientInfo\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn clientInfoCmd.Val(), clientInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeACLLog:\n\t\t\tif aclLogCmd, ok := cmd.(interface {\n\t\t\t\tVal() []*ACLLogEntry\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn aclLogCmd.Val(), aclLogCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeInfo:\n\t\t\tif infoCmd, ok := cmd.(interface {\n\t\t\t\tVal() string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn infoCmd.Val(), infoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeMonitor:\n\t\t\tif monitorCmd, ok := cmd.(interface {\n\t\t\t\tVal() string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn monitorCmd.Val(), monitorCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeJSON:\n\t\t\tif jsonCmd, ok := cmd.(interface {\n\t\t\t\tVal() string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn jsonCmd.Val(), jsonCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeJSONSlice:\n\t\t\tif jsonSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []interface{}\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn jsonSliceCmd.Val(), jsonSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeIntPointerSlice:\n\t\t\tif intPointerSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []*int64\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn intPointerSliceCmd.Val(), intPointerSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeScanDump:\n\t\t\tif scanDumpCmd, ok := cmd.(interface {\n\t\t\t\tVal() ScanDump\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn scanDumpCmd.Val(), scanDumpCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeBFInfo:\n\t\t\tif bfInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() BFInfo\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn bfInfoCmd.Val(), bfInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeCFInfo:\n\t\t\tif cfInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() CFInfo\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn cfInfoCmd.Val(), cfInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeCMSInfo:\n\t\t\tif cmsInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() CMSInfo\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn cmsInfoCmd.Val(), cmsInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeTopKInfo:\n\t\t\tif topKInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() TopKInfo\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn topKInfoCmd.Val(), topKInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeTDigestInfo:\n\t\t\tif tDigestInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() TDigestInfo\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn tDigestInfoCmd.Val(), tDigestInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFTSearch:\n\t\t\tif ftSearchCmd, ok := cmd.(interface {\n\t\t\t\tVal() FTSearchResult\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn ftSearchCmd.Val(), ftSearchCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFTInfo:\n\t\t\tif ftInfoCmd, ok := cmd.(interface {\n\t\t\t\tVal() FTInfoResult\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn ftInfoCmd.Val(), ftInfoCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFTSpellCheck:\n\t\t\tif ftSpellCheckCmd, ok := cmd.(interface {\n\t\t\t\tVal() []SpellCheckResult\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn ftSpellCheckCmd.Val(), ftSpellCheckCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFTSynDump:\n\t\t\tif ftSynDumpCmd, ok := cmd.(interface {\n\t\t\t\tVal() []FTSynDumpResult\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn ftSynDumpCmd.Val(), ftSynDumpCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeAggregate:\n\t\t\tif aggregateCmd, ok := cmd.(interface {\n\t\t\t\tVal() *FTAggregateResult\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn aggregateCmd.Val(), aggregateCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeTSTimestampValue:\n\t\t\tif tsTimestampValueCmd, ok := cmd.(interface {\n\t\t\t\tVal() TSTimestampValue\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn tsTimestampValueCmd.Val(), tsTimestampValueCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeTSTimestampValueSlice:\n\t\t\tif tsTimestampValueSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []TSTimestampValue\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn tsTimestampValueSliceCmd.Val(), tsTimestampValueSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeStringSlice:\n\t\t\tif stringSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn stringSliceCmd.Val(), stringSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeIntSlice:\n\t\t\tif intSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []int64\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn intSliceCmd.Val(), intSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeBoolSlice:\n\t\t\tif boolSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []bool\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn boolSliceCmd.Val(), boolSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeFloatSlice:\n\t\t\tif floatSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []float64\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn floatSliceCmd.Val(), floatSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeSlice:\n\t\t\tif sliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []interface{}\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn sliceCmd.Val(), sliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeKeyValueSlice:\n\t\t\tif keyValueSliceCmd, ok := cmd.(interface {\n\t\t\t\tVal() []KeyValue\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn keyValueSliceCmd.Val(), keyValueSliceCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeMapStringString:\n\t\t\tif mapCmd, ok := cmd.(interface {\n\t\t\t\tVal() map[string]string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn mapCmd.Val(), mapCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeMapStringInt:\n\t\t\tif mapCmd, ok := cmd.(interface {\n\t\t\t\tVal() map[string]int64\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn mapCmd.Val(), mapCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeMapStringInterfaceSlice:\n\t\t\tif mapCmd, ok := cmd.(interface {\n\t\t\t\tVal() []map[string]interface{}\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn mapCmd.Val(), mapCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeMapStringInterface:\n\t\t\tif mapCmd, ok := cmd.(interface {\n\t\t\t\tVal() map[string]interface{}\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn mapCmd.Val(), mapCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeMapStringStringSlice:\n\t\t\tif mapCmd, ok := cmd.(interface {\n\t\t\t\tVal() []map[string]string\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn mapCmd.Val(), mapCmd.Err()\n\t\t\t}\n\t\tcase CmdTypeMapMapStringInterface:\n\t\t\tif mapCmd, ok := cmd.(interface {\n\t\t\t\tVal() map[string]interface{}\n\t\t\t\tErr() error\n\t\t\t}); ok {\n\t\t\t\treturn mapCmd.Val(), mapCmd.Err()\n\t\t\t}\n\t\tdefault:\n\t\t\t// For unknown command types, return nil\n\t\t\treturn nil, nil\n\t\t}\n\t}\n\n\t// If we can't get the command type, return nil\n\treturn nil, nil\n}\n"
  },
  {
    "path": "command_digest_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\nfunc TestDigestCmd(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\thexStr   string\n\t\texpected uint64\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname:     \"zero value\",\n\t\t\thexStr:   \"0\",\n\t\t\texpected: 0,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname:     \"small value\",\n\t\t\thexStr:   \"ff\",\n\t\t\texpected: 255,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname:     \"medium value\",\n\t\t\thexStr:   \"1234abcd\",\n\t\t\texpected: 0x1234abcd,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname:     \"large value\",\n\t\t\thexStr:   \"ffffffffffffffff\",\n\t\t\texpected: 0xffffffffffffffff,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname:     \"uppercase hex\",\n\t\t\thexStr:   \"DEADBEEF\",\n\t\t\texpected: 0xdeadbeef,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed case hex\",\n\t\t\thexStr:   \"DeAdBeEf\",\n\t\t\texpected: 0xdeadbeef,\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname:     \"typical xxh3 hash\",\n\t\t\thexStr:   \"a1b2c3d4e5f67890\",\n\t\t\texpected: 0xa1b2c3d4e5f67890,\n\t\t\twantErr:  false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a mock reader that returns the hex string in RESP format\n\t\t\t// Format: $<length>\\r\\n<data>\\r\\n\n\t\t\trespData := []byte(fmt.Sprintf(\"$%d\\r\\n%s\\r\\n\", len(tt.hexStr), tt.hexStr))\n\n\t\t\trd := proto.NewReader(newMockConn(respData))\n\n\t\t\tcmd := NewDigestCmd(context.Background(), \"digest\", \"key\")\n\t\t\terr := cmd.readReply(rd)\n\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"DigestCmd.readReply() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !tt.wantErr && cmd.Val() != tt.expected {\n\t\t\t\tt.Errorf(\"DigestCmd.Val() = %d (0x%x), want %d (0x%x)\", cmd.Val(), cmd.Val(), tt.expected, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDigestCmdResult(t *testing.T) {\n\tcmd := NewDigestCmd(context.Background(), \"digest\", \"key\")\n\texpected := uint64(0xdeadbeefcafebabe)\n\tcmd.SetVal(expected)\n\n\tval, err := cmd.Result()\n\tif err != nil {\n\t\tt.Errorf(\"DigestCmd.Result() error = %v\", err)\n\t}\n\n\tif val != expected {\n\t\tt.Errorf(\"DigestCmd.Result() = %d (0x%x), want %d (0x%x)\", val, val, expected, expected)\n\t}\n}\n\n// mockConn is a simple mock connection for testing\ntype mockConn struct {\n\tdata []byte\n\tpos  int\n}\n\nfunc newMockConn(data []byte) *mockConn {\n\treturn &mockConn{data: data}\n}\n\nfunc (c *mockConn) Read(p []byte) (n int, err error) {\n\tif c.pos >= len(c.data) {\n\t\treturn 0, nil\n\t}\n\tn = copy(p, c.data[c.pos:])\n\tc.pos += n\n\treturn n, nil\n}\n"
  },
  {
    "path": "command_policy_resolver.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9/internal/routing\"\n)\n\ntype (\n\tmodule      = string\n\tcommandName = string\n)\n\nvar defaultPolicies = map[module]map[commandName]*routing.CommandPolicy{\n\t\"ft\": {\n\t\t\"create\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"search\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"aggregate\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"dictadd\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"dictdump\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"dictdel\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"suglen\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultHashSlot,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"cursor\": {\n\t\t\tRequest:  routing.ReqSpecial,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"sugadd\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultHashSlot,\n\t\t},\n\t\t\"sugget\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultHashSlot,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"sugdel\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultHashSlot,\n\t\t},\n\t\t\"spellcheck\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"explain\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"explaincli\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"aliasadd\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"aliasupdate\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"aliasdel\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"info\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"tagvals\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"syndump\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"synupdate\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"profile\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t\tTips: map[string]string{\n\t\t\t\trouting.ReadOnlyCMD: \"\",\n\t\t\t},\n\t\t},\n\t\t\"alter\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"dropindex\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t\t\"drop\": {\n\t\t\tRequest:  routing.ReqDefault,\n\t\t\tResponse: routing.RespDefaultKeyless,\n\t\t},\n\t},\n}\n\ntype CommandInfoResolveFunc func(ctx context.Context, cmd Cmder) *routing.CommandPolicy\n\ntype commandInfoResolver struct {\n\tresolveFunc      CommandInfoResolveFunc\n\tfallBackResolver *commandInfoResolver\n}\n\nfunc NewCommandInfoResolver(resolveFunc CommandInfoResolveFunc) *commandInfoResolver {\n\treturn &commandInfoResolver{\n\t\tresolveFunc: resolveFunc,\n\t}\n}\n\nfunc NewDefaultCommandPolicyResolver() *commandInfoResolver {\n\treturn NewCommandInfoResolver(func(ctx context.Context, cmd Cmder) *routing.CommandPolicy {\n\t\tmodule := \"core\"\n\t\tcommand := cmd.Name()\n\t\tcmdParts := strings.Split(command, \".\")\n\t\tif len(cmdParts) == 2 {\n\t\t\tmodule = cmdParts[0]\n\t\t\tcommand = cmdParts[1]\n\t\t}\n\n\t\tif policy, ok := defaultPolicies[module][command]; ok {\n\t\t\treturn policy\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc (r *commandInfoResolver) GetCommandPolicy(ctx context.Context, cmd Cmder) *routing.CommandPolicy {\n\tif r.resolveFunc == nil {\n\t\treturn nil\n\t}\n\n\tpolicy := r.resolveFunc(ctx, cmd)\n\tif policy != nil {\n\t\treturn policy\n\t}\n\n\tif r.fallBackResolver != nil {\n\t\treturn r.fallBackResolver.GetCommandPolicy(ctx, cmd)\n\t}\n\n\treturn nil\n}\n\nfunc (r *commandInfoResolver) SetFallbackResolver(fallbackResolver *commandInfoResolver) {\n\tr.fallBackResolver = fallbackResolver\n}\n"
  },
  {
    "path": "command_recorder_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// commandRecorder records the last N commands executed by a Redis client.\ntype commandRecorder struct {\n\tmu       sync.Mutex\n\tcommands []string\n\tmaxSize  int\n}\n\n// newCommandRecorder creates a new command recorder with the specified maximum size.\nfunc newCommandRecorder(maxSize int) *commandRecorder {\n\treturn &commandRecorder{\n\t\tcommands: make([]string, 0, maxSize),\n\t\tmaxSize:  maxSize,\n\t}\n}\n\n// Record adds a command to the recorder.\nfunc (r *commandRecorder) Record(cmd string) {\n\tcmd = strings.ToLower(cmd)\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\tr.commands = append(r.commands, cmd)\n\tif len(r.commands) > r.maxSize {\n\t\tr.commands = r.commands[1:]\n\t}\n}\n\n// LastCommands returns a copy of the recorded commands.\nfunc (r *commandRecorder) LastCommands() []string {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\treturn append([]string(nil), r.commands...)\n}\n\n// Contains checks if the recorder contains a specific command.\nfunc (r *commandRecorder) Contains(cmd string) bool {\n\tcmd = strings.ToLower(cmd)\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\tfor _, c := range r.commands {\n\t\tif strings.Contains(c, cmd) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Hook returns a Redis hook that records commands.\nfunc (r *commandRecorder) Hook() redis.Hook {\n\treturn &commandHook{recorder: r}\n}\n\n// commandHook implements the redis.Hook interface to record commands.\ntype commandHook struct {\n\trecorder *commandRecorder\n}\n\nfunc (h *commandHook) DialHook(next redis.DialHook) redis.DialHook {\n\treturn next\n}\n\nfunc (h *commandHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {\n\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\th.recorder.Record(cmd.String())\n\t\treturn next(ctx, cmd)\n\t}\n}\n\nfunc (h *commandHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\tfor _, cmd := range cmds {\n\t\t\th.recorder.Record(cmd.String())\n\t\t}\n\t\treturn next(ctx, cmds)\n\t}\n}\n"
  },
  {
    "path": "command_test.go",
    "content": "package redis_test\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nvar _ = Describe(\"Cmd\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"implements Stringer\", func() {\n\t\tset := client.Set(ctx, \"foo\", \"bar\", 0)\n\t\tExpect(set.String()).To(Equal(\"set foo bar: OK\"))\n\n\t\tget := client.Get(ctx, \"foo\")\n\t\tExpect(get.String()).To(Equal(\"get foo: bar\"))\n\t})\n\n\tIt(\"has val/err\", func() {\n\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\tget := client.Get(ctx, \"key\")\n\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\tExpect(get.Val()).To(Equal(\"hello\"))\n\n\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\t})\n\n\tIt(\"has helpers\", func() {\n\t\tset := client.Set(ctx, \"key\", \"10\", 0)\n\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\n\t\tn, err := client.Get(ctx, \"key\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(10)))\n\n\t\tun, err := client.Get(ctx, \"key\").Uint64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(un).To(Equal(uint64(10)))\n\n\t\tf, err := client.Get(ctx, \"key\").Float64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(f).To(Equal(float64(10)))\n\t})\n\n\tIt(\"supports float32\", func() {\n\t\tf := float32(66.97)\n\n\t\terr := client.Set(ctx, \"float_key\", f, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tval, err := client.Get(ctx, \"float_key\").Float32()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(f))\n\t})\n\n\tIt(\"supports time.Time\", func() {\n\t\ttm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC)\n\n\t\terr := client.Set(ctx, \"time_key\", tm, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\ts, err := client.Get(ctx, \"time_key\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(s).To(Equal(\"2019-01-01T09:45:10.000222125Z\"))\n\n\t\ttm2, err := client.Get(ctx, \"time_key\").Time()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(tm2).To(BeTemporally(\"==\", tm))\n\t})\n\n\tIt(\"allows to set custom error\", func() {\n\t\te := errors.New(\"custom error\")\n\t\tcmd := redis.Cmd{}\n\t\tcmd.SetErr(e)\n\t\t_, err := cmd.Result()\n\t\tExpect(err).To(Equal(e))\n\t})\n})\n"
  },
  {
    "path": "commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n)\n\n// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,\n// otherwise you will receive an error: (error) ERR syntax error.\n// For example:\n//\n//\trdb.Set(ctx, key, value, redis.KeepTTL)\nconst KeepTTL = -1\n\nfunc usePrecise(dur time.Duration) bool {\n\treturn dur < time.Second || dur%time.Second != 0\n}\n\nfunc formatMs(ctx context.Context, dur time.Duration) int64 {\n\tif dur > 0 && dur < time.Millisecond {\n\t\tinternal.Logger.Printf(\n\t\t\tctx,\n\t\t\t\"specified duration is %s, but minimal supported value is %s - truncating to 1ms\",\n\t\t\tdur, time.Millisecond,\n\t\t)\n\t\treturn 1\n\t}\n\treturn int64(dur / time.Millisecond)\n}\n\nfunc formatSec(ctx context.Context, dur time.Duration) int64 {\n\tif dur > 0 && dur < time.Second {\n\t\tinternal.Logger.Printf(\n\t\t\tctx,\n\t\t\t\"specified duration is %s, but minimal supported value is %s - truncating to 1s\",\n\t\t\tdur, time.Second,\n\t\t)\n\t\treturn 1\n\t}\n\treturn int64(dur / time.Second)\n}\n\nfunc appendArgs(dst, src []interface{}) []interface{} {\n\tif len(src) == 1 {\n\t\treturn appendArg(dst, src[0])\n\t}\n\n\tif cap(dst) < len(dst)+len(src) {\n\t\tnewDst := make([]interface{}, len(dst), len(dst)+len(src))\n\t\tcopy(newDst, dst)\n\t\tdst = newDst\n\t}\n\tdst = append(dst, src...)\n\treturn dst\n}\n\nfunc appendArg(dst []interface{}, arg interface{}) []interface{} {\n\tswitch arg := arg.(type) {\n\tcase []string:\n\t\tfor _, s := range arg {\n\t\t\tdst = append(dst, s)\n\t\t}\n\t\treturn dst\n\tcase []interface{}:\n\t\tdst = append(dst, arg...)\n\t\treturn dst\n\tcase map[string]interface{}:\n\t\tfor k, v := range arg {\n\t\t\tdst = append(dst, k, v)\n\t\t}\n\t\treturn dst\n\tcase map[string]string:\n\t\tfor k, v := range arg {\n\t\t\tdst = append(dst, k, v)\n\t\t}\n\t\treturn dst\n\tcase time.Time, time.Duration, encoding.BinaryMarshaler, net.IP:\n\t\treturn append(dst, arg)\n\tcase nil:\n\t\treturn dst\n\tdefault:\n\t\t// scan struct field\n\t\tv := reflect.ValueOf(arg)\n\t\tif v.Type().Kind() == reflect.Ptr {\n\t\t\tif v.IsNil() {\n\t\t\t\t// error: arg is not a valid object\n\t\t\t\treturn dst\n\t\t\t}\n\t\t\tv = v.Elem()\n\t\t}\n\n\t\tif v.Type().Kind() == reflect.Struct {\n\t\t\treturn appendStructField(dst, v)\n\t\t}\n\n\t\treturn append(dst, arg)\n\t}\n}\n\n// appendStructField appends the field and value held by the structure v to dst, and returns the appended dst.\nfunc appendStructField(dst []interface{}, v reflect.Value) []interface{} {\n\ttyp := v.Type()\n\tfor i := 0; i < typ.NumField(); i++ {\n\t\ttag := typ.Field(i).Tag.Get(\"redis\")\n\t\tif tag == \"\" || tag == \"-\" {\n\t\t\tcontinue\n\t\t}\n\t\tname, opt, _ := strings.Cut(tag, \",\")\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tfield := v.Field(i)\n\n\t\t// miss field\n\t\tif omitEmpty(opt) && isEmptyValue(field) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif field.CanInterface() {\n\t\t\tdst = append(dst, name, field.Interface())\n\t\t}\n\t}\n\n\treturn dst\n}\n\nfunc omitEmpty(opt string) bool {\n\tfor opt != \"\" {\n\t\tvar name string\n\t\tname, opt, _ = strings.Cut(opt, \",\")\n\t\tif name == \"omitempty\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isEmptyValue(v reflect.Value) bool {\n\tswitch v.Kind() {\n\tcase reflect.Array, reflect.Map, reflect.Slice, reflect.String:\n\t\treturn v.Len() == 0\n\tcase reflect.Bool:\n\t\treturn !v.Bool()\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn v.Int() == 0\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\treturn v.Uint() == 0\n\tcase reflect.Float32, reflect.Float64:\n\t\treturn v.Float() == 0\n\tcase reflect.Interface, reflect.Pointer:\n\t\treturn v.IsNil()\n\tcase reflect.Struct:\n\t\tif v.Type() == reflect.TypeOf(time.Time{}) {\n\t\t\treturn v.IsZero()\n\t\t}\n\t\t// Only supports the struct time.Time,\n\t\t// subsequent iterations will follow the func Scan support decoder.\n\t}\n\treturn false\n}\n\ntype Cmdable interface {\n\tPipeline() Pipeliner\n\tPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error)\n\n\tTxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error)\n\tTxPipeline() Pipeliner\n\n\tCommand(ctx context.Context) *CommandsInfoCmd\n\tCommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd\n\tCommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd\n\tCommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd\n\tClientGetName(ctx context.Context) *StringCmd\n\tEcho(ctx context.Context, message interface{}) *StringCmd\n\tPing(ctx context.Context) *StatusCmd\n\tQuit(ctx context.Context) *StatusCmd\n\tUnlink(ctx context.Context, keys ...string) *IntCmd\n\n\tBgRewriteAOF(ctx context.Context) *StatusCmd\n\tBgSave(ctx context.Context) *StatusCmd\n\tClientKill(ctx context.Context, ipPort string) *StatusCmd\n\tClientKillByFilter(ctx context.Context, keys ...string) *IntCmd\n\tClientList(ctx context.Context) *StringCmd\n\tClientInfo(ctx context.Context) *ClientInfoCmd\n\tClientPause(ctx context.Context, dur time.Duration) *BoolCmd\n\tClientUnpause(ctx context.Context) *BoolCmd\n\tClientID(ctx context.Context) *IntCmd\n\tClientUnblock(ctx context.Context, id int64) *IntCmd\n\tClientUnblockWithError(ctx context.Context, id int64) *IntCmd\n\tClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *StatusCmd\n\tConfigGet(ctx context.Context, parameter string) *MapStringStringCmd\n\tConfigResetStat(ctx context.Context) *StatusCmd\n\tConfigSet(ctx context.Context, parameter, value string) *StatusCmd\n\tConfigRewrite(ctx context.Context) *StatusCmd\n\tDBSize(ctx context.Context) *IntCmd\n\tFlushAll(ctx context.Context) *StatusCmd\n\tFlushAllAsync(ctx context.Context) *StatusCmd\n\tFlushDB(ctx context.Context) *StatusCmd\n\tFlushDBAsync(ctx context.Context) *StatusCmd\n\tInfo(ctx context.Context, section ...string) *StringCmd\n\tLastSave(ctx context.Context) *IntCmd\n\tSave(ctx context.Context) *StatusCmd\n\tShutdown(ctx context.Context) *StatusCmd\n\tShutdownSave(ctx context.Context) *StatusCmd\n\tShutdownNoSave(ctx context.Context) *StatusCmd\n\tSlaveOf(ctx context.Context, host, port string) *StatusCmd\n\tReplicaOf(ctx context.Context, host, port string) *StatusCmd\n\tSlowLogGet(ctx context.Context, num int64) *SlowLogCmd\n\tSlowLogLen(ctx context.Context) *IntCmd\n\tSlowLogReset(ctx context.Context) *StatusCmd\n\tTime(ctx context.Context) *TimeCmd\n\tDebugObject(ctx context.Context, key string) *StringCmd\n\tMemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd\n\tLatency(ctx context.Context) *LatencyCmd\n\tLatencyReset(ctx context.Context, events ...interface{}) *StatusCmd\n\n\tModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd\n\n\tACLCmdable\n\tBitMapCmdable\n\tClusterCmdable\n\tGenericCmdable\n\tGeoCmdable\n\tHashCmdable\n\tHyperLogLogCmdable\n\tListCmdable\n\tProbabilisticCmdable\n\tPubSubCmdable\n\tScriptingFunctionsCmdable\n\tSearchCmdable\n\tSetCmdable\n\tSortedSetCmdable\n\tStringCmdable\n\tStreamCmdable\n\tTimeseriesCmdable\n\tJSONCmdable\n\tVectorSetCmdable\n}\n\ntype StatefulCmdable interface {\n\tCmdable\n\tAuth(ctx context.Context, password string) *StatusCmd\n\tAuthACL(ctx context.Context, username, password string) *StatusCmd\n\tSelect(ctx context.Context, index int) *StatusCmd\n\tSwapDB(ctx context.Context, index1, index2 int) *StatusCmd\n\tClientSetName(ctx context.Context, name string) *BoolCmd\n\tClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd\n\tHello(ctx context.Context, ver int, username, password, clientName string) *MapStringInterfaceCmd\n}\n\nvar (\n\t_ Cmdable = (*Client)(nil)\n\t_ Cmdable = (*Tx)(nil)\n\t_ Cmdable = (*Ring)(nil)\n\t_ Cmdable = (*ClusterClient)(nil)\n\t_ Cmdable = (*Pipeline)(nil)\n)\n\ntype cmdable func(ctx context.Context, cmd Cmder) error\n\ntype statefulCmdable func(ctx context.Context, cmd Cmder) error\n\n//------------------------------------------------------------------------------\n\nfunc (c statefulCmdable) Auth(ctx context.Context, password string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"auth\", password)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// AuthACL Perform an AUTH command, using the given user and pass.\n// Should be used to authenticate the current connection with one of the connections defined in the ACL list\n// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system.\nfunc (c statefulCmdable) AuthACL(ctx context.Context, username, password string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"auth\", username, password)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Wait(ctx context.Context, numSlaves int, timeout time.Duration) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"wait\", numSlaves, int(timeout/time.Millisecond))\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) WaitAOF(ctx context.Context, numLocal, numSlaves int, timeout time.Duration) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"waitAOF\", numLocal, numSlaves, int(timeout/time.Millisecond))\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c statefulCmdable) Select(ctx context.Context, index int) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"select\", index)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c statefulCmdable) SwapDB(ctx context.Context, index1, index2 int) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"swapdb\", index1, index2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ClientSetName assigns a name to the connection.\nfunc (c statefulCmdable) ClientSetName(ctx context.Context, name string) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"client\", \"setname\", name)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ClientSetInfo sends a CLIENT SETINFO command with the provided info.\nfunc (c statefulCmdable) ClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd {\n\terr := info.Validate()\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tvar cmd *StatusCmd\n\tif info.LibName != nil {\n\t\tlibName := fmt.Sprintf(\"go-redis(%s,%s)\", *info.LibName, internal.ReplaceSpaces(runtime.Version()))\n\t\tcmd = NewStatusCmd(ctx, \"client\", \"setinfo\", \"LIB-NAME\", libName)\n\t} else {\n\t\tcmd = NewStatusCmd(ctx, \"client\", \"setinfo\", \"LIB-VER\", *info.LibVer)\n\t}\n\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Validate checks if only one field in the struct is non-nil.\nfunc (info LibraryInfo) Validate() error {\n\tif info.LibName != nil && info.LibVer != nil {\n\t\treturn errors.New(\"both LibName and LibVer cannot be set at the same time\")\n\t}\n\tif info.LibName == nil && info.LibVer == nil {\n\t\treturn errors.New(\"at least one of LibName and LibVer should be set\")\n\t}\n\treturn nil\n}\n\n// Hello sets the resp protocol used.\nfunc (c statefulCmdable) Hello(ctx context.Context,\n\tver int, username, password, clientName string,\n) *MapStringInterfaceCmd {\n\targs := make([]interface{}, 0, 7)\n\targs = append(args, \"hello\", ver)\n\tif password != \"\" {\n\t\tif username != \"\" {\n\t\t\targs = append(args, \"auth\", username, password)\n\t\t} else {\n\t\t\targs = append(args, \"auth\", \"default\", password)\n\t\t}\n\t}\n\tif clientName != \"\" {\n\t\targs = append(args, \"setname\", clientName)\n\t}\n\tcmd := NewMapStringInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n//------------------------------------------------------------------------------\n\nfunc (c cmdable) Command(ctx context.Context) *CommandsInfoCmd {\n\tcmd := NewCommandsInfoCmd(ctx, \"command\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FilterBy is used for the `CommandList` command parameter.\ntype FilterBy struct {\n\tModule  string\n\tACLCat  string\n\tPattern string\n}\n\nfunc (c cmdable) CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd {\n\targs := make([]interface{}, 0, 5)\n\targs = append(args, \"command\", \"list\")\n\tif filter != nil {\n\t\tif filter.Module != \"\" {\n\t\t\targs = append(args, \"filterby\", \"module\", filter.Module)\n\t\t} else if filter.ACLCat != \"\" {\n\t\t\targs = append(args, \"filterby\", \"aclcat\", filter.ACLCat)\n\t\t} else if filter.Pattern != \"\" {\n\t\t\targs = append(args, \"filterby\", \"pattern\", filter.Pattern)\n\t\t}\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) CommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd {\n\targs := make([]interface{}, 2+len(commands))\n\targs[0] = \"command\"\n\targs[1] = \"getkeys\"\n\tcopy(args[2:], commands)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) CommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd {\n\targs := make([]interface{}, 2+len(commands))\n\targs[0] = \"command\"\n\targs[1] = \"getkeysandflags\"\n\tcopy(args[2:], commands)\n\tcmd := NewKeyFlagsCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ClientGetName returns the name of the connection.\nfunc (c cmdable) ClientGetName(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"client\", \"getname\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Echo(ctx context.Context, message interface{}) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"echo\", message)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Ping(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"ping\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd {\n\tcmd := NewCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Quit closes the connection.\n//\n// Deprecated: Just close the connection instead as of Redis 7.2.0.\nfunc (c cmdable) Quit(_ context.Context) *StatusCmd {\n\tpanic(\"not implemented\")\n}\n\n//------------------------------------------------------------------------------\n\nfunc (c cmdable) BgRewriteAOF(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"bgrewriteaof\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) BgSave(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"bgsave\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientKill(ctx context.Context, ipPort string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"client\", \"kill\", ipPort)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ClientKillByFilter is new style syntax, while the ClientKill is old\n//\n//\tCLIENT KILL <option> [value] ... <option> [value]\nfunc (c cmdable) ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd {\n\targs := make([]interface{}, 2+len(keys))\n\targs[0] = \"client\"\n\targs[1] = \"kill\"\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientList(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"client\", \"list\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientPause(ctx context.Context, dur time.Duration) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"client\", \"pause\", formatMs(ctx, dur))\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientUnpause(ctx context.Context) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"client\", \"unpause\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientID(ctx context.Context) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"client\", \"id\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientUnblock(ctx context.Context, id int64) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"client\", \"unblock\", id)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientUnblockWithError(ctx context.Context, id int64) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"client\", \"unblock\", id, \"error\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ClientInfo(ctx context.Context) *ClientInfoCmd {\n\tcmd := NewClientInfoCmd(ctx, \"client\", \"info\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ClientMaintNotifications enables or disables maintenance notifications for maintenance upgrades.\n// When enabled, the client will receive push notifications about Redis maintenance events.\nfunc (c cmdable) ClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *StatusCmd {\n\targs := []interface{}{\"client\", \"maint_notifications\"}\n\tif enabled {\n\t\tif endpointType == \"\" {\n\t\t\tendpointType = \"none\"\n\t\t}\n\t\targs = append(args, \"on\", \"moving-endpoint-type\", endpointType)\n\t} else {\n\t\targs = append(args, \"off\")\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ------------------------------------------------------------------------------------------------\n\nfunc (c cmdable) ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd {\n\tcmd := NewMapStringStringCmd(ctx, \"config\", \"get\", parameter)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ConfigResetStat(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"config\", \"resetstat\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ConfigSet(ctx context.Context, parameter, value string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"config\", \"set\", parameter, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ConfigRewrite(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"config\", \"rewrite\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) DBSize(ctx context.Context) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"dbsize\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FlushAll(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"flushall\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FlushAllAsync(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"flushall\", \"async\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FlushDB(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"flushdb\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FlushDBAsync(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"flushdb\", \"async\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Info(ctx context.Context, sections ...string) *StringCmd {\n\targs := make([]interface{}, 1+len(sections))\n\targs[0] = \"info\"\n\tfor i, section := range sections {\n\t\targs[i+1] = section\n\t}\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) InfoMap(ctx context.Context, sections ...string) *InfoCmd {\n\targs := make([]interface{}, 1+len(sections))\n\targs[0] = \"info\"\n\tfor i, section := range sections {\n\t\targs[i+1] = section\n\t}\n\tcmd := NewInfoCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LastSave(ctx context.Context) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"lastsave\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Save(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"save\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) shutdown(ctx context.Context, modifier string) *StatusCmd {\n\tvar args []interface{}\n\tif modifier == \"\" {\n\t\targs = []interface{}{\"shutdown\"}\n\t} else {\n\t\targs = []interface{}{\"shutdown\", modifier}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\tif err := cmd.Err(); err != nil {\n\t\tif err == io.EOF {\n\t\t\t// Server quit as expected.\n\t\t\tcmd.err = nil\n\t\t}\n\t} else {\n\t\t// Server did not quit. String reply contains the reason.\n\t\tcmd.err = errors.New(cmd.val)\n\t\tcmd.val = \"\"\n\t}\n\treturn cmd\n}\n\nfunc (c cmdable) Shutdown(ctx context.Context) *StatusCmd {\n\treturn c.shutdown(ctx, \"\")\n}\n\nfunc (c cmdable) ShutdownSave(ctx context.Context) *StatusCmd {\n\treturn c.shutdown(ctx, \"save\")\n}\n\nfunc (c cmdable) ShutdownNoSave(ctx context.Context) *StatusCmd {\n\treturn c.shutdown(ctx, \"nosave\")\n}\n\n// SlaveOf sets a Redis server as a replica of another, or promotes it to being a master.\n//\n// Deprecated: Use ReplicaOf instead as of Redis 5.0.0.\nfunc (c cmdable) SlaveOf(ctx context.Context, host, port string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"slaveof\", host, port)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ReplicaOf sets a Redis server as a replica of another, or promotes it to being a master.\nfunc (c cmdable) ReplicaOf(ctx context.Context, host, port string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"replicaof\", host, port)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) SlowLogGet(ctx context.Context, num int64) *SlowLogCmd {\n\tcmd := NewSlowLogCmd(context.Background(), \"slowlog\", \"get\", num)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) SlowLogLen(ctx context.Context) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"slowlog\", \"len\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) SlowLogReset(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"slowlog\", \"reset\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Latency(ctx context.Context) *LatencyCmd {\n\tcmd := NewLatencyCmd(ctx, \"latency\", \"latest\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LatencyReset(ctx context.Context, events ...interface{}) *StatusCmd {\n\targs := make([]interface{}, 2+len(events))\n\targs[0] = \"latency\"\n\targs[1] = \"reset\"\n\tcopy(args[2:], events)\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Sync(_ context.Context) {\n\tpanic(\"not implemented\")\n}\n\nfunc (c cmdable) Time(ctx context.Context) *TimeCmd {\n\tcmd := NewTimeCmd(ctx, \"time\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) DebugObject(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"debug\", \"object\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd {\n\targs := []interface{}{\"memory\", \"usage\", key}\n\tif len(samples) > 0 {\n\t\tif len(samples) != 1 {\n\t\t\tcmd := NewIntCmd(ctx)\n\t\t\tcmd.SetErr(errors.New(\"MemoryUsage expects single sample count\"))\n\t\t\treturn cmd\n\t\t}\n\t\targs = append(args, \"SAMPLES\", samples[0])\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n//------------------------------------------------------------------------------\n\n// ModuleLoadexConfig struct is used to specify the arguments for the MODULE LOADEX command of redis.\n// `MODULE LOADEX path [CONFIG name value [CONFIG name value ...]] [ARGS args [args ...]]`\ntype ModuleLoadexConfig struct {\n\tPath string\n\tConf map[string]interface{}\n\tArgs []interface{}\n}\n\nfunc (c *ModuleLoadexConfig) toArgs() []interface{} {\n\targs := make([]interface{}, 3, 3+len(c.Conf)*3+len(c.Args)*2)\n\targs[0] = \"MODULE\"\n\targs[1] = \"LOADEX\"\n\targs[2] = c.Path\n\tfor k, v := range c.Conf {\n\t\targs = append(args, \"CONFIG\", k, v)\n\t}\n\tfor _, arg := range c.Args {\n\t\targs = append(args, \"ARGS\", arg)\n\t}\n\treturn args\n}\n\n// ModuleLoadex Redis `MODULE LOADEX path [CONFIG name value [CONFIG name value ...]] [ARGS args [args ...]]` command.\nfunc (c cmdable) ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd {\n\tcmd := NewStringCmd(ctx, conf.toArgs()...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n/*\nMonitor - represents a Redis MONITOR command, allowing the user to capture\nand process all commands sent to a Redis server. This mimics the behavior of\nMONITOR in the redis-cli.\n\nNotes:\n- Using MONITOR blocks the connection to the server for itself. It needs a dedicated connection\n- The user should create a channel of type string\n- This runs concurrently in the background. Trigger via the Start and Stop functions\nSee further: Redis MONITOR command: https://redis.io/commands/monitor\n*/\nfunc (c cmdable) Monitor(ctx context.Context, ch chan string) *MonitorCmd {\n\tcmd := newMonitorCmd(ctx, ch)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "commands_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/routing\"\n)\n\ntype TimeValue struct {\n\ttime.Time\n}\n\nfunc (t *TimeValue) ScanRedis(s string) (err error) {\n\tt.Time, err = time.Parse(time.RFC3339Nano, s)\n\treturn\n}\n\nvar _ = Describe(\"Commands\", func() {\n\tctx := context.TODO()\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tDescribe(\"server\", func() {\n\t\tIt(\"should Auth\", func() {\n\t\t\tcmds, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Auth(ctx, \"password\")\n\t\t\t\tpipe.Auth(ctx, \"\")\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(ContainSubstring(\"ERR AUTH\"))\n\t\t\tExpect(cmds[0].Err().Error()).To(ContainSubstring(\"ERR AUTH\"))\n\t\t\tExpect(cmds[1].Err().Error()).To(ContainSubstring(\"ERR AUTH\"))\n\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats.Hits).To(Equal(uint32(1)))\n\t\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t\t\tExpect(stats.TotalConns).To(Equal(uint32(1)))\n\t\t\tExpect(stats.IdleConns).To(Equal(uint32(1)))\n\t\t})\n\n\t\tIt(\"should hello\", func() {\n\t\t\tcmds, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Hello(ctx, 3, \"\", \"\", \"\")\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tm, err := cmds[0].(*redis.MapStringInterfaceCmd).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(m[\"proto\"]).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should Echo\", func() {\n\t\t\tpipe := client.Pipeline()\n\t\t\techo := pipe.Echo(ctx, \"hello\")\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(echo.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(echo.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should Ping\", func() {\n\t\t\tping := client.Ping(ctx)\n\t\t\tExpect(ping.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ping.Val()).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should Ping with Do method\", func() {\n\t\t\tresult := client.Conn().Do(ctx, \"PING\")\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should Wait\", func() {\n\t\t\tconst wait = 3 * time.Second\n\n\t\t\t// assume testing on single redis instance\n\t\t\tstart := time.Now()\n\t\t\tval, err := client.Wait(ctx, 1, wait).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(int64(0)))\n\t\t\tExpect(time.Now()).To(BeTemporally(\"~\", start.Add(wait), 3*time.Second))\n\t\t})\n\n\t\tIt(\"should WaitAOF\", func() {\n\t\t\tconst waitAOF = 3 * time.Second\n\t\t\tSkip(\"flaky test\")\n\n\t\t\t// assuming that the redis instance doesn't have AOF enabled\n\t\t\tstart := time.Now()\n\t\t\tval, err := client.WaitAOF(ctx, 1, 1, waitAOF).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).NotTo(ContainSubstring(\"ERR WAITAOF cannot be used when numlocal is set but appendonly is disabled\"))\n\t\t\tExpect(time.Now()).To(BeTemporally(\"~\", start.Add(waitAOF), 3*time.Second))\n\t\t})\n\n\t\tIt(\"should Select\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tpipe := client.Pipeline()\n\t\t\tsel := pipe.Select(ctx, 1)\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(sel.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sel.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should SwapDB\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tpipe := client.Pipeline()\n\t\t\tsel := pipe.SwapDB(ctx, 1, 2)\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(sel.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sel.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should BgRewriteAOF\", func() {\n\t\t\tSkip(\"flaky test\")\n\n\t\t\tval, err := client.BgRewriteAOF(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(ContainSubstring(\"Background append only file rewriting\"))\n\t\t})\n\n\t\tIt(\"should BgSave\", func() {\n\t\t\tSkip(\"flaky test\")\n\n\t\t\t// workaround for \"ERR Can't BGSAVE while AOF log rewriting is in progress\"\n\t\t\tEventually(func() string {\n\t\t\t\treturn client.BgSave(ctx).Val()\n\t\t\t}, \"30s\").Should(Equal(\"Background saving started\"))\n\t\t})\n\n\t\tIt(\"Should CommandGetKeys\", func() {\n\t\t\tkeys, err := client.CommandGetKeys(ctx, \"MSET\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).To(Equal([]string{\"a\", \"c\", \"e\"}))\n\n\t\t\tkeys, err = client.CommandGetKeys(ctx, \"EVAL\", \"not consulted\", \"3\", \"key1\", \"key2\", \"key3\", \"arg1\", \"arg2\", \"arg3\", \"argN\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).To(Equal([]string{\"key1\", \"key2\", \"key3\"}))\n\n\t\t\tkeys, err = client.CommandGetKeys(ctx, \"SORT\", \"mylist\", \"ALPHA\", \"STORE\", \"outlist\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).To(Equal([]string{\"mylist\", \"outlist\"}))\n\n\t\t\t_, err = client.CommandGetKeys(ctx, \"FAKECOMMAND\", \"arg1\", \"arg2\").Result()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err).To(MatchError(\"ERR Invalid command specified\"))\n\t\t})\n\n\t\tIt(\"should CommandGetKeysAndFlags\", func() {\n\t\t\tkeysAndFlags, err := client.CommandGetKeysAndFlags(ctx, \"LMOVE\", \"mylist1\", \"mylist2\", \"left\", \"left\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keysAndFlags).To(Equal([]redis.KeyFlags{\n\t\t\t\t{\n\t\t\t\t\tKey:   \"mylist1\",\n\t\t\t\t\tFlags: []string{\"RW\", \"access\", \"delete\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKey:   \"mylist2\",\n\t\t\t\t\tFlags: []string{\"RW\", \"insert\"},\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\t_, err = client.CommandGetKeysAndFlags(ctx, \"FAKECOMMAND\", \"arg1\", \"arg2\").Result()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err).To(MatchError(\"ERR Invalid command specified\"))\n\t\t})\n\n\t\tIt(\"should ClientKill\", func() {\n\t\t\tr := client.ClientKill(ctx, \"1.1.1.1:1111\")\n\t\t\tExpect(r.Err()).To(MatchError(\"ERR No such client\"))\n\t\t\tExpect(r.Val()).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should ClientKillByFilter\", func() {\n\t\t\tr := client.ClientKillByFilter(ctx, \"TYPE\", \"test\")\n\t\t\tExpect(r.Err()).To(MatchError(\"ERR Unknown client type 'test'\"))\n\t\t\tExpect(r.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should ClientKillByFilter with kill myself\", func() {\n\t\t\topt := redisOptions()\n\t\t\topt.ClientName = \"killmyid\"\n\t\t\tdb := redis.NewClient(opt)\n\t\t\tExpect(db.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\t\tdefer func() {\n\t\t\t\tExpect(db.Close()).NotTo(HaveOccurred())\n\t\t\t}()\n\t\t\tval, err := client.ClientList(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).Should(ContainSubstring(\"name=killmyid\"))\n\n\t\t\tmyid := db.ClientID(ctx).Val()\n\t\t\tkilled := client.ClientKillByFilter(ctx, \"ID\", strconv.FormatInt(myid, 10))\n\t\t\tExpect(killed.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(killed.Val()).To(BeNumerically(\"==\", 1))\n\n\t\t\tval, err = client.ClientList(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).ShouldNot(ContainSubstring(\"name=killmyid\"))\n\t\t})\n\n\t\tIt(\"should ClientKillByFilter with MAXAGE\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tvar s []string\n\t\t\tstarted := make(chan bool)\n\t\t\tdone := make(chan bool)\n\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tstarted <- true\n\t\t\t\tblpop := client.BLPop(ctx, 0, \"list\")\n\t\t\t\tExpect(blpop.Val()).To(Equal(s))\n\t\t\t\tdone <- true\n\t\t\t}()\n\t\t\t<-started\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tFail(\"BLPOP is not blocked.\")\n\t\t\tcase <-time.After(1100 * time.Millisecond):\n\t\t\t\t// ok\n\t\t\t}\n\n\t\t\tkilled := client.ClientKillByFilter(ctx, \"MAXAGE\", \"1\")\n\t\t\tExpect(killed.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(killed.Val()).To(BeNumerically(\">=\", 1))\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// ok\n\t\t\tcase <-time.After(200 * time.Millisecond):\n\t\t\t\tFail(\"BLPOP is still blocked.\")\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should ClientID\", func() {\n\t\t\terr := client.ClientID(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(client.ClientID(ctx).Val()).To(BeNumerically(\">=\", 0))\n\t\t})\n\n\t\tIt(\"should ClientUnblock\", func() {\n\t\t\tid := client.ClientID(ctx).Val()\n\t\t\tr, err := client.ClientUnblock(ctx, id).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(r).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should ClientUnblockWithError\", func() {\n\t\t\tid := client.ClientID(ctx).Val()\n\t\t\tr, err := client.ClientUnblockWithError(ctx, id).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(r).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should ClientInfo\", func() {\n\t\t\tinfo, err := client.ClientInfo(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(info).NotTo(BeNil())\n\t\t})\n\n\t\tIt(\"should ClientPause\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ClientPause(ctx, time.Second).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tstart := time.Now()\n\t\t\terr = client.Ping(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(time.Now()).To(BeTemporally(\"~\", start.Add(time.Second), 800*time.Millisecond))\n\t\t})\n\n\t\tIt(\"should ClientSetName and ClientGetName\", func() {\n\t\t\tpipe := client.Pipeline()\n\t\t\tset := pipe.ClientSetName(ctx, \"theclientname\")\n\t\t\tget := pipe.ClientGetName(ctx)\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(BeTrue())\n\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"theclientname\"))\n\t\t})\n\n\t\tIt(\"should ClientSetInfo\", func() {\n\t\t\tpipe := client.Pipeline()\n\n\t\t\t// Test setting the libName\n\t\t\tlibName := \"go-redis\"\n\t\t\tlibInfo := redis.WithLibraryName(libName)\n\t\t\tsetInfo := pipe.ClientSetInfo(ctx, libInfo)\n\t\t\t_, err := pipe.Exec(ctx)\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(setInfo.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(setInfo.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Test setting the libVer\n\t\t\tlibVer := \"vX.x\"\n\t\t\tlibInfo = redis.WithLibraryVersion(libVer)\n\t\t\tsetInfo = pipe.ClientSetInfo(ctx, libInfo)\n\t\t\t_, err = pipe.Exec(ctx)\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(setInfo.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(setInfo.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Test setting both fields, expect a panic\n\t\t\tlibInfo = redis.LibraryInfo{LibName: &libName, LibVer: &libVer}\n\n\t\t\tExpect(func() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\terr := r.(error)\n\t\t\t\t\t\tExpect(err).To(MatchError(\"both LibName and LibVer cannot be set at the same time\"))\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tpipe.ClientSetInfo(ctx, libInfo)\n\t\t\t}).To(Panic())\n\n\t\t\t// Test setting neither field, expect a panic\n\t\t\tlibInfo = redis.LibraryInfo{}\n\n\t\t\tExpect(func() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\terr := r.(error)\n\t\t\t\t\t\tExpect(err).To(MatchError(\"at least one of LibName and LibVer should be set\"))\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tpipe.ClientSetInfo(ctx, libInfo)\n\t\t\t}).To(Panic())\n\t\t\t// Test setting the default options for libName, libName suffix and libVer\n\t\t\tclientInfo := client.ClientInfo(ctx).Val()\n\t\t\tExpect(clientInfo.LibName).To(ContainSubstring(\"go-redis(go-redis,\"))\n\t\t\t// Test setting the libName suffix in options\n\t\t\topt := redisOptions()\n\t\t\topt.IdentitySuffix = \"suffix\"\n\t\t\tclient2 := redis.NewClient(opt)\n\t\t\tdefer client2.Close()\n\t\t\tclientInfo = client2.ClientInfo(ctx).Val()\n\t\t\tExpect(clientInfo.LibName).To(ContainSubstring(\"go-redis(suffix,\"))\n\n\t\t})\n\n\t\tIt(\"should ConfigGet\", func() {\n\t\t\tval, err := client.ConfigGet(ctx, \"*\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).NotTo(BeEmpty())\n\t\t})\n\n\t\tIt(\"should ConfigGet Modules\", func() {\n\t\t\tSkipBeforeRedisVersion(8, \"Config doesn't include modules before Redis 8\")\n\t\t\texpected := map[string]string{\n\t\t\t\t\"search-*\": \"search-timeout\",\n\t\t\t\t\"ts-*\":     \"ts-retention-policy\",\n\t\t\t\t\"bf-*\":     \"bf-error-rate\",\n\t\t\t\t\"cf-*\":     \"cf-initial-size\",\n\t\t\t}\n\n\t\t\tfor prefix, lookup := range expected {\n\t\t\t\tval, err := client.ConfigGet(ctx, prefix).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).NotTo(BeEmpty())\n\t\t\t\tExpect(val[lookup]).NotTo(BeEmpty())\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should ConfigResetStat\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tr := client.ConfigResetStat(ctx)\n\t\t\tExpect(r.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(r.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should ConfigSet\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tconfigGet := client.ConfigGet(ctx, \"maxmemory\")\n\t\t\tExpect(configGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(configGet.Val()).To(HaveLen(1))\n\t\t\t_, ok := configGet.Val()[\"maxmemory\"]\n\t\t\tExpect(ok).To(BeTrue())\n\n\t\t\tconfigSet := client.ConfigSet(ctx, \"maxmemory\", configGet.Val()[\"maxmemory\"])\n\t\t\tExpect(configSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(configSet.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should ConfigGet with Modules\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(8, \"config get won't return modules configs before redis 8\")\n\t\t\tconfigGet := client.ConfigGet(ctx, \"*\")\n\t\t\tExpect(configGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(configGet.Val()).To(HaveKey(\"maxmemory\"))\n\t\t\tExpect(configGet.Val()).To(HaveKey(\"search-timeout\"))\n\t\t\tExpect(configGet.Val()).To(HaveKey(\"ts-retention-policy\"))\n\t\t\tExpect(configGet.Val()).To(HaveKey(\"bf-error-rate\"))\n\t\t\tExpect(configGet.Val()).To(HaveKey(\"cf-initial-size\"))\n\t\t})\n\n\t\tIt(\"should ConfigSet FT DIALECT\", func() {\n\t\t\tSkipBeforeRedisVersion(8, \"config doesn't include modules before Redis 8\")\n\t\t\tdefaultState, err := client.ConfigGet(ctx, \"search-default-dialect\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// set to 3\n\t\t\tres, err := client.ConfigSet(ctx, \"search-default-dialect\", \"3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\n\t\t\tdefDialect, err := client.FTConfigGet(ctx, \"DEFAULT_DIALECT\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(defDialect).To(BeEquivalentTo(map[string]interface{}{\"DEFAULT_DIALECT\": \"3\"}))\n\n\t\t\tresGet, err := client.ConfigGet(ctx, \"search-default-dialect\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(resGet).To(BeEquivalentTo(map[string]string{\"search-default-dialect\": \"3\"}))\n\n\t\t\t// set to 2\n\t\t\tres, err = client.ConfigSet(ctx, \"search-default-dialect\", \"2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\n\t\t\tdefDialect, err = client.FTConfigGet(ctx, \"DEFAULT_DIALECT\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(defDialect).To(BeEquivalentTo(map[string]interface{}{\"DEFAULT_DIALECT\": \"2\"}))\n\n\t\t\t// set to 1\n\t\t\tres, err = client.ConfigSet(ctx, \"search-default-dialect\", \"1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\n\t\t\tdefDialect, err = client.FTConfigGet(ctx, \"DEFAULT_DIALECT\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(defDialect).To(BeEquivalentTo(map[string]interface{}{\"DEFAULT_DIALECT\": \"1\"}))\n\n\t\t\tresGet, err = client.ConfigGet(ctx, \"search-default-dialect\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(resGet).To(BeEquivalentTo(map[string]string{\"search-default-dialect\": \"1\"}))\n\n\t\t\t// set to default\n\t\t\tres, err = client.ConfigSet(ctx, \"search-default-dialect\", defaultState[\"search-default-dialect\"]).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\t\t})\n\n\t\tIt(\"should ConfigSet fail for ReadOnly\", func() {\n\t\t\tSkipBeforeRedisVersion(8, \"Config doesn't include modules before Redis 8\")\n\t\t\t_, err := client.ConfigSet(ctx, \"search-max-doctablesize\", \"100000\").Result()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should ConfigSet Modules\", func() {\n\t\t\tSkipBeforeRedisVersion(8, \"Config doesn't include modules before Redis 8\")\n\t\t\tdefaults := map[string]string{}\n\t\t\texpected := map[string]string{\n\t\t\t\t\"search-timeout\":      \"100\",\n\t\t\t\t\"ts-retention-policy\": \"2\",\n\t\t\t\t\"bf-error-rate\":       \"0.13\",\n\t\t\t\t\"cf-initial-size\":     \"64\",\n\t\t\t}\n\n\t\t\t// read the defaults to set them back later\n\t\t\tfor setting := range expected {\n\t\t\t\tval, err := client.ConfigGet(ctx, setting).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tdefaults[setting] = val[setting]\n\t\t\t}\n\n\t\t\t// check if new values can be set\n\t\t\tfor setting, value := range expected {\n\t\t\t\tval, err := client.ConfigSet(ctx, setting, value).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).NotTo(BeEmpty())\n\t\t\t\tExpect(val).To(Equal(\"OK\"))\n\t\t\t}\n\n\t\t\tfor setting, value := range expected {\n\t\t\t\tval, err := client.ConfigGet(ctx, setting).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).NotTo(BeEmpty())\n\t\t\t\tExpect(val[setting]).To(Equal(value))\n\t\t\t}\n\n\t\t\t// set back to the defaults\n\t\t\tfor setting, value := range defaults {\n\t\t\t\tval, err := client.ConfigSet(ctx, setting, value).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).NotTo(BeEmpty())\n\t\t\t\tExpect(val).To(Equal(\"OK\"))\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should Fail ConfigSet Modules\", func() {\n\t\t\tSkipBeforeRedisVersion(8, \"Config doesn't include modules before Redis 8\")\n\t\t\texpected := map[string]string{\n\t\t\t\t\"search-timeout\":      \"-100\",\n\t\t\t\t\"ts-retention-policy\": \"-10\",\n\t\t\t\t\"bf-error-rate\":       \"1.5\",\n\t\t\t\t\"cf-initial-size\":     \"-10\",\n\t\t\t}\n\n\t\t\tfor setting, value := range expected {\n\t\t\t\tval, err := client.ConfigSet(ctx, setting, value).Result()\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\tExpect(err).To(MatchError(ContainSubstring(setting)))\n\t\t\t\tExpect(val).To(BeEmpty())\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should ConfigRewrite\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tconfigRewrite := client.ConfigRewrite(ctx)\n\t\t\tExpect(configRewrite.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(configRewrite.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should DBSize\", func() {\n\t\t\tsize, err := client.DBSize(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should Info\", func() {\n\t\t\tinfo := client.Info(ctx)\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).NotTo(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should InfoMap\", Label(\"redis.info\"), func() {\n\t\t\tinfo := client.InfoMap(ctx)\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).NotTo(BeNil())\n\n\t\t\tinfo = client.InfoMap(ctx, \"dummy\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).To(BeNil())\n\n\t\t\tinfo = client.InfoMap(ctx, \"server\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).To(HaveLen(1))\n\t\t})\n\n\t\tIt(\"should Info Modules\", Label(\"redis.info\"), func() {\n\t\t\tSkipBeforeRedisVersion(8, \"modules are included in info for Redis Version >= 8\")\n\t\t\tinfo := client.Info(ctx)\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).NotTo(BeNil())\n\n\t\t\tinfo = client.Info(ctx, \"search\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"search\"))\n\n\t\t\tinfo = client.Info(ctx, \"modules\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"search\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"ReJSON\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"timeseries\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"bf\"))\n\n\t\t\tinfo = client.Info(ctx, \"everything\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"search\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"ReJSON\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"timeseries\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(\"bf\"))\n\t\t})\n\n\t\tIt(\"should InfoMap Modules\", Label(\"redis.info\"), func() {\n\t\t\tSkipBeforeRedisVersion(8, \"modules are included in info for Redis Version >= 8\")\n\t\t\tinfo := client.InfoMap(ctx)\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).NotTo(BeNil())\n\n\t\t\tinfo = client.InfoMap(ctx, \"search\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(len(info.Val())).To(BeNumerically(\">=\", 2))\n\t\t\tExpect(info.Val()[\"search_version\"]).ToNot(BeNil())\n\n\t\t\tinfo = client.InfoMap(ctx, \"modules\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tval := info.Val()\n\t\t\tmodules, ok := val[\"Modules\"]\n\t\t\tExpect(ok).To(BeTrue())\n\t\t\tExpect(len(val)).To(BeNumerically(\">=\", 2))\n\t\t\tExpect(val[\"search_version\"]).ToNot(BeNil())\n\t\t\tExpect(modules[\"search\"]).ToNot(BeNil())\n\t\t\tExpect(modules[\"ReJSON\"]).ToNot(BeNil())\n\t\t\tExpect(modules[\"timeseries\"]).ToNot(BeNil())\n\t\t\tExpect(modules[\"bf\"]).ToNot(BeNil())\n\n\t\t\tinfo = client.InfoMap(ctx, \"everything\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(len(info.Val())).To(BeNumerically(\">=\", 10))\n\t\t})\n\n\t\tIt(\"should Info cpu\", func() {\n\t\t\tinfo := client.Info(ctx, \"cpu\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).NotTo(Equal(\"\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(`used_cpu_sys`))\n\t\t})\n\n\t\tIt(\"should Info cpu and memory\", func() {\n\t\t\tinfo := client.Info(ctx, \"cpu\", \"memory\")\n\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(info.Val()).NotTo(Equal(\"\"))\n\t\t\tExpect(info.Val()).To(ContainSubstring(`used_cpu_sys`))\n\t\t\tExpect(info.Val()).To(ContainSubstring(`memory`))\n\t\t})\n\n\t\tIt(\"should LastSave\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tlastSave := client.LastSave(ctx)\n\t\t\tExpect(lastSave.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lastSave.Val()).NotTo(Equal(0))\n\t\t})\n\n\t\tIt(\"should Save\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\t// workaround for \"ERR Background save already in progress\"\n\t\t\tEventually(func() string {\n\t\t\t\treturn client.Save(ctx).Val()\n\t\t\t}, \"10s\").Should(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should SlaveOf\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tslaveOf := client.SlaveOf(ctx, \"localhost\", \"8888\")\n\t\t\tExpect(slaveOf.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(slaveOf.Val()).To(Equal(\"OK\"))\n\n\t\t\tslaveOf = client.SlaveOf(ctx, \"NO\", \"ONE\")\n\t\t\tExpect(slaveOf.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(slaveOf.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should ReplicaOf\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\treplicaOf := client.ReplicaOf(ctx, \"localhost\", \"8888\")\n\t\t\tExpect(replicaOf.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(replicaOf.Val()).To(Equal(\"OK\"))\n\n\t\t\treplicaOf = client.ReplicaOf(ctx, \"NO\", \"ONE\")\n\t\t\tExpect(replicaOf.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(replicaOf.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should Time\", func() {\n\t\t\ttm, err := client.Time(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(tm).To(BeTemporally(\"~\", time.Now(), 3*time.Second))\n\t\t})\n\n\t\tIt(\"should Command\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tcmd := cmds[\"mget\"]\n\t\t\tExpect(cmd.Name).To(Equal(\"mget\"))\n\t\t\tExpect(cmd.Arity).To(Equal(int8(-2)))\n\t\t\tExpect(cmd.Flags).To(ContainElement(\"readonly\"))\n\t\t\tExpect(cmd.FirstKeyPos).To(Equal(int8(1)))\n\t\t\tExpect(cmd.LastKeyPos).To(Equal(int8(-1)))\n\t\t\tExpect(cmd.StepCount).To(Equal(int8(1)))\n\n\t\t\tcmd = cmds[\"ping\"]\n\t\t\tExpect(cmd.Name).To(Equal(\"ping\"))\n\t\t\tExpect(cmd.Arity).To(Equal(int8(-1)))\n\t\t\tExpect(cmd.Flags).To(ContainElement(\"fast\"))\n\t\t\tExpect(cmd.FirstKeyPos).To(Equal(int8(0)))\n\t\t\tExpect(cmd.LastKeyPos).To(Equal(int8(0)))\n\t\t\tExpect(cmd.StepCount).To(Equal(int8(0)))\n\t\t})\n\n\t\tIt(\"should Command Tips\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipAfterRedisVersion(7.9, \"Redis 8 changed the COMMAND reply format\")\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tcmd := cmds[\"touch\"]\n\t\t\tExpect(cmd.Name).To(Equal(\"touch\"))\n\t\t\tExpect(cmd.CommandPolicy.Request).To(Equal(routing.ReqMultiShard))\n\t\t\tExpect(cmd.CommandPolicy.Response).To(Equal(routing.RespAggSum))\n\n\t\t\tcmd = cmds[\"flushall\"]\n\t\t\tExpect(cmd.Name).To(Equal(\"flushall\"))\n\t\t\tExpect(cmd.CommandPolicy.Request).To(Equal(routing.ReqAllShards))\n\t\t\tExpect(cmd.CommandPolicy.Response).To(Equal(routing.RespAllSucceeded))\n\t\t})\n\n\t\tIt(\"should return all command names\", func() {\n\t\t\tcmdList := client.CommandList(ctx, nil)\n\t\t\tExpect(cmdList.Err()).NotTo(HaveOccurred())\n\t\t\tcmdNames := cmdList.Val()\n\n\t\t\tExpect(cmdNames).NotTo(BeEmpty())\n\n\t\t\t// Assert that some expected commands are present in the list\n\t\t\tExpect(cmdNames).To(ContainElement(\"get\"))\n\t\t\tExpect(cmdNames).To(ContainElement(\"set\"))\n\t\t\tExpect(cmdNames).To(ContainElement(\"hset\"))\n\t\t})\n\n\t\tIt(\"should filter commands by module\", func() {\n\t\t\tfilter := &redis.FilterBy{\n\t\t\t\tModule: \"JSON\",\n\t\t\t}\n\t\t\tcmdList := client.CommandList(ctx, filter)\n\t\t\tExpect(cmdList.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(cmdList.Val()).To(HaveLen(0))\n\t\t})\n\n\t\tIt(\"should filter commands by ACL category\", func() {\n\t\t\tfilter := &redis.FilterBy{\n\t\t\t\tACLCat: \"admin\",\n\t\t\t}\n\n\t\t\tcmdList := client.CommandList(ctx, filter)\n\t\t\tExpect(cmdList.Err()).NotTo(HaveOccurred())\n\t\t\tcmdNames := cmdList.Val()\n\n\t\t\t// Assert that the returned list only contains commands from the admin ACL category\n\t\t\tExpect(len(cmdNames)).To(BeNumerically(\">\", 10))\n\t\t})\n\n\t\tIt(\"should filter commands by pattern\", func() {\n\t\t\tfilter := &redis.FilterBy{\n\t\t\t\tPattern: \"*GET*\",\n\t\t\t}\n\t\t\tcmdList := client.CommandList(ctx, filter)\n\t\t\tExpect(cmdList.Err()).NotTo(HaveOccurred())\n\t\t\tcmdNames := cmdList.Val()\n\n\t\t\t// Assert that the returned list only contains commands that match the given pattern\n\t\t\tExpect(cmdNames).To(ContainElement(\"get\"))\n\t\t\tExpect(cmdNames).To(ContainElement(\"getbit\"))\n\t\t\tExpect(cmdNames).To(ContainElement(\"getrange\"))\n\t\t\tExpect(cmdNames).NotTo(ContainElement(\"set\"))\n\t\t})\n\t})\n\n\tDescribe(\"debugging\", Label(\"NonRedisEnterprise\"), func() {\n\t\tIt(\"should DebugObject\", func() {\n\t\t\terr := client.DebugObject(ctx, \"foo\").Err()\n\t\t\tExpect(err).To(MatchError(\"ERR no such key\"))\n\n\t\t\terr = client.Set(ctx, \"foo\", \"bar\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\ts, err := client.DebugObject(ctx, \"foo\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(s).To(ContainSubstring(\"serializedlength:4\"))\n\t\t})\n\n\t\tIt(\"should MemoryUsage\", func() {\n\t\t\terr := client.MemoryUsage(ctx, \"foo\").Err()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\n\t\t\terr = client.Set(ctx, \"foo\", \"bar\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tn, err := client.MemoryUsage(ctx, \"foo\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).NotTo(BeZero())\n\n\t\t\tn, err = client.MemoryUsage(ctx, \"foo\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).NotTo(BeZero())\n\n\t\t\t_, err = client.MemoryUsage(ctx, \"foo\", 0, 1).Result()\n\t\t\tExpect(err).Should(MatchError(\"MemoryUsage expects single sample count\"))\n\t\t})\n\t})\n\n\tDescribe(\"keys\", func() {\n\t\tIt(\"should Del\", func() {\n\t\t\terr := client.Set(ctx, \"key1\", \"Hello\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.Set(ctx, \"key2\", \"World\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tn, err := client.Del(ctx, \"key1\", \"key2\", \"key3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should Unlink\", func() {\n\t\t\terr := client.Set(ctx, \"key1\", \"Hello\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.Set(ctx, \"key2\", \"World\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tn, err := client.Unlink(ctx, \"key1\", \"key2\", \"key3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should Dump\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tdump := client.Dump(ctx, \"key\")\n\t\t\tExpect(dump.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(dump.Val()).NotTo(BeEmpty())\n\t\t})\n\n\t\tIt(\"should Exists\", func() {\n\t\t\tset := client.Set(ctx, \"key1\", \"Hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tn, err := client.Exists(ctx, \"key1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(1)))\n\n\t\t\tn, err = client.Exists(ctx, \"key2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(0)))\n\n\t\t\tn, err = client.Exists(ctx, \"key1\", \"key2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(1)))\n\n\t\t\tn, err = client.Exists(ctx, \"key1\", \"key1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should Expire\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"Hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\texpire := client.Expire(ctx, \"key\", 10*time.Second)\n\t\t\tExpect(expire.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expire.Val()).To(Equal(true))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(Equal(10 * time.Second))\n\n\t\t\tset = client.Set(ctx, \"key\", \"Hello World\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tttl = client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(Equal(time.Duration(-1)))\n\n\t\t\tttl = client.TTL(ctx, \"nonexistent_key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(Equal(time.Duration(-2)))\n\t\t})\n\n\t\tIt(\"should ExpireAt\", func() {\n\t\t\tsetCmd := client.Set(ctx, \"key\", \"Hello\", 0)\n\t\t\tExpect(setCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(setCmd.Val()).To(Equal(\"OK\"))\n\n\t\t\tn, err := client.Exists(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(1)))\n\n\t\t\t// Check correct expiration time is set in the future\n\t\t\texpireAt := time.Now().Add(time.Minute)\n\t\t\texpireAtCmd := client.ExpireAt(ctx, \"key\", expireAt)\n\t\t\tExpect(expireAtCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expireAtCmd.Val()).To(Equal(true))\n\n\t\t\ttimeCmd := client.ExpireTime(ctx, \"key\")\n\t\t\tExpect(timeCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(timeCmd.Val().Seconds()).To(BeNumerically(\"==\", expireAt.Unix()))\n\n\t\t\t// Check correct expiration in the past\n\t\t\texpireAtCmd = client.ExpireAt(ctx, \"key\", time.Now().Add(-time.Hour))\n\t\t\tExpect(expireAtCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expireAtCmd.Val()).To(Equal(true))\n\n\t\t\tn, err = client.Exists(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should Keys\", func() {\n\t\t\tmset := client.MSet(ctx, \"one\", \"1\", \"two\", \"2\", \"three\", \"3\", \"four\", \"4\")\n\t\t\tExpect(mset.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mset.Val()).To(Equal(\"OK\"))\n\n\t\t\tkeys := client.Keys(ctx, \"*o*\")\n\t\t\tExpect(keys.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(keys.Val()).To(ConsistOf([]string{\"four\", \"one\", \"two\"}))\n\n\t\t\tkeys = client.Keys(ctx, \"t??\")\n\t\t\tExpect(keys.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(keys.Val()).To(Equal([]string{\"two\"}))\n\n\t\t\tkeys = client.Keys(ctx, \"*\")\n\t\t\tExpect(keys.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(keys.Val()).To(ConsistOf([]string{\"four\", \"one\", \"three\", \"two\"}))\n\t\t})\n\n\t\tIt(\"should Migrate\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tmigrate := client.Migrate(ctx, \"localhost\", redisSecondaryPort, \"key\", 0, 0)\n\t\t\tExpect(migrate.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(migrate.Val()).To(Equal(\"NOKEY\"))\n\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tmigrate = client.Migrate(ctx, \"localhost\", redisSecondaryPort, \"key\", 0, 0)\n\t\t\tExpect(migrate.Err()).To(MatchError(\"IOERR error or timeout writing to target instance\"))\n\t\t\tExpect(migrate.Val()).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should Move\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tmove := client.Move(ctx, \"key\", 2)\n\t\t\tExpect(move.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(move.Val()).To(Equal(false))\n\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tmove = client.Move(ctx, \"key\", 2)\n\t\t\tExpect(move.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(move.Val()).To(Equal(true))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\t\tExpect(get.Val()).To(Equal(\"\"))\n\n\t\t\tpipe := client.Pipeline()\n\t\t\tpipe.Select(ctx, 2)\n\t\t\tget = pipe.Get(ctx, \"key\")\n\t\t\tpipe.FlushDB(ctx)\n\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should Object\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tstart := time.Now()\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\trefCount := client.ObjectRefCount(ctx, \"key\")\n\t\t\tExpect(refCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(refCount.Val()).To(Equal(int64(1)))\n\n\t\t\tclient.ConfigSet(ctx, \"maxmemory-policy\", \"volatile-lfu\")\n\t\t\tfreq := client.ObjectFreq(ctx, \"key\")\n\t\t\tExpect(freq.Err()).NotTo(HaveOccurred())\n\t\t\tclient.ConfigSet(ctx, \"maxmemory-policy\", \"noeviction\") // default\n\n\t\t\terr := client.ObjectEncoding(ctx, \"key\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tidleTime := client.ObjectIdleTime(ctx, \"key\")\n\t\t\tExpect(idleTime.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Redis returned milliseconds/1000, which may cause ObjectIdleTime to be at a critical value,\n\t\t\t// should be +1s to deal with the critical value problem.\n\t\t\t// if too much time (>1s) is used during command execution, it may also cause the test to fail.\n\t\t\t// so the ObjectIdleTime result should be <=now-start+1s\n\t\t\t// link: https://github.com/redis/redis/blob/5b48d900498c85bbf4772c1d466c214439888115/src/object.c#L1265-L1272\n\t\t\tExpect(idleTime.Val()).To(BeNumerically(\"<=\", time.Since(start)+time.Second))\n\t\t})\n\n\t\tIt(\"should Persist\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"Hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\texpire := client.Expire(ctx, \"key\", 10*time.Second)\n\t\t\tExpect(expire.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expire.Val()).To(Equal(true))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(Equal(10 * time.Second))\n\n\t\t\tpersist := client.Persist(ctx, \"key\")\n\t\t\tExpect(persist.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(persist.Val()).To(Equal(true))\n\n\t\t\tttl = client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val() < 0).To(Equal(true))\n\t\t})\n\n\t\tIt(\"should PExpire\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"Hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\texpiration := 900 * time.Millisecond\n\t\t\tpexpire := client.PExpire(ctx, \"key\", expiration)\n\t\t\tExpect(pexpire.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pexpire.Val()).To(Equal(true))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(Equal(time.Second))\n\n\t\t\tpttl := client.PTTL(ctx, \"key\")\n\t\t\tExpect(pttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pttl.Val()).To(BeNumerically(\"~\", expiration, 100*time.Millisecond))\n\t\t})\n\n\t\tIt(\"should PExpireAt\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"Hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\texpiration := 900 * time.Millisecond\n\t\t\tpexpireat := client.PExpireAt(ctx, \"key\", time.Now().Add(expiration))\n\t\t\tExpect(pexpireat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pexpireat.Val()).To(Equal(true))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(Equal(time.Second))\n\n\t\t\tpttl := client.PTTL(ctx, \"key\")\n\t\t\tExpect(pttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pttl.Val()).To(BeNumerically(\"~\", expiration, 100*time.Millisecond))\n\t\t})\n\n\t\tIt(\"should PExpireTime\", func() {\n\t\t\t// The command returns -1 if the key exists but has no associated expiration time.\n\t\t\t// The command returns -2 if the key does not exist.\n\t\t\tpExpireTime := client.PExpireTime(ctx, \"key\")\n\t\t\tExpect(pExpireTime.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pExpireTime.Val() < 0).To(Equal(true))\n\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\ttimestamp := time.Now().Add(time.Minute)\n\t\t\texpireAt := client.PExpireAt(ctx, \"key\", timestamp)\n\t\t\tExpect(expireAt.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expireAt.Val()).To(Equal(true))\n\n\t\t\tpExpireTime = client.PExpireTime(ctx, \"key\")\n\t\t\tExpect(pExpireTime.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pExpireTime.Val().Milliseconds()).To(BeNumerically(\"==\", timestamp.UnixMilli()))\n\t\t})\n\n\t\tIt(\"should PTTL\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"Hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\texpiration := time.Second\n\t\t\texpire := client.Expire(ctx, \"key\", expiration)\n\t\t\tExpect(expire.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tpttl := client.PTTL(ctx, \"key\")\n\t\t\tExpect(pttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pttl.Val()).To(BeNumerically(\"~\", expiration, 100*time.Millisecond))\n\t\t})\n\n\t\tIt(\"should RandomKey\", func() {\n\t\t\trandomKey := client.RandomKey(ctx)\n\t\t\tExpect(randomKey.Err()).To(Equal(redis.Nil))\n\t\t\tExpect(randomKey.Val()).To(Equal(\"\"))\n\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\trandomKey = client.RandomKey(ctx)\n\t\t\tExpect(randomKey.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(randomKey.Val()).To(Equal(\"key\"))\n\t\t})\n\n\t\tIt(\"should Rename\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tstatus := client.Rename(ctx, \"key\", \"key1\")\n\t\t\tExpect(status.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(status.Val()).To(Equal(\"OK\"))\n\n\t\t\tget := client.Get(ctx, \"key1\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should RenameNX\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\trenameNX := client.RenameNX(ctx, \"key\", \"key1\")\n\t\t\tExpect(renameNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(renameNX.Val()).To(Equal(true))\n\n\t\t\tget := client.Get(ctx, \"key1\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should Restore\", func() {\n\t\t\terr := client.Set(ctx, \"key\", \"hello\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tdump := client.Dump(ctx, \"key\")\n\t\t\tExpect(dump.Err()).NotTo(HaveOccurred())\n\n\t\t\terr = client.Del(ctx, \"key\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\trestore, err := client.Restore(ctx, \"key\", 0, dump.Val()).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(restore).To(Equal(\"OK\"))\n\n\t\t\ttype_, err := client.Type(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(type_).To(Equal(\"string\"))\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should RestoreReplace\", func() {\n\t\t\terr := client.Set(ctx, \"key\", \"hello\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tdump := client.Dump(ctx, \"key\")\n\t\t\tExpect(dump.Err()).NotTo(HaveOccurred())\n\n\t\t\trestore, err := client.RestoreReplace(ctx, \"key\", 0, dump.Val()).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(restore).To(Equal(\"OK\"))\n\n\t\t\ttype_, err := client.Type(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(type_).To(Equal(\"string\"))\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should Sort RO\", func() {\n\t\t\tsize, err := client.LPush(ctx, \"list\", \"1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(1)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(2)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(3)))\n\n\t\t\tels, err := client.SortRO(ctx, \"list\", &redis.Sort{\n\t\t\t\tOffset: 0,\n\t\t\t\tCount:  2,\n\t\t\t\tOrder:  \"ASC\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(els).To(Equal([]string{\"1\", \"2\"}))\n\t\t})\n\n\t\tIt(\"should Sort\", func() {\n\t\t\tsize, err := client.LPush(ctx, \"list\", \"1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(1)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(2)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(3)))\n\n\t\t\tels, err := client.Sort(ctx, \"list\", &redis.Sort{\n\t\t\t\tOffset: 0,\n\t\t\t\tCount:  2,\n\t\t\t\tOrder:  \"ASC\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(els).To(Equal([]string{\"1\", \"2\"}))\n\t\t})\n\n\t\tIt(\"should Sort and Get\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsize, err := client.LPush(ctx, \"list\", \"1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(1)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(2)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(3)))\n\n\t\t\terr = client.Set(ctx, \"object_2\", \"value2\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t{\n\t\t\t\tels, err := client.Sort(ctx, \"list\", &redis.Sort{\n\t\t\t\t\tGet: []string{\"object_*\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(els).To(Equal([]string{\"\", \"value2\", \"\"}))\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tels, err := client.SortInterfaces(ctx, \"list\", &redis.Sort{\n\t\t\t\t\tGet: []string{\"object_*\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(els).To(Equal([]interface{}{nil, \"value2\", nil}))\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should Sort and Store\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsize, err := client.LPush(ctx, \"list\", \"1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(1)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(2)))\n\n\t\t\tsize, err = client.LPush(ctx, \"list\", \"2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(3)))\n\n\t\t\tn, err := client.SortStore(ctx, \"list\", \"list2\", &redis.Sort{\n\t\t\t\tOffset: 0,\n\t\t\t\tCount:  2,\n\t\t\t\tOrder:  \"ASC\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\n\t\t\tels, err := client.LRange(ctx, \"list2\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(els).To(Equal([]string{\"1\", \"2\"}))\n\t\t})\n\n\t\tIt(\"should Touch\", func() {\n\t\t\tset1 := client.Set(ctx, \"touch1\", \"hello\", 0)\n\t\t\tExpect(set1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set1.Val()).To(Equal(\"OK\"))\n\n\t\t\tset2 := client.Set(ctx, \"touch2\", \"hello\", 0)\n\t\t\tExpect(set2.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set2.Val()).To(Equal(\"OK\"))\n\n\t\t\ttouch := client.Touch(ctx, \"touch1\", \"touch2\", \"touch3\")\n\t\t\tExpect(touch.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(touch.Val()).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should ExpireTime\", func() {\n\t\t\t// The command returns -1 if the key exists but has no associated expiration time.\n\t\t\t// The command returns -2 if the key does not exist.\n\t\t\texpireTimeCmd := client.ExpireTime(ctx, \"key\")\n\t\t\tExpect(expireTimeCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expireTimeCmd.Val() < 0).To(Equal(true))\n\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\texpireAt := time.Now().Add(time.Minute)\n\t\t\texpireAtCmd := client.ExpireAt(ctx, \"key\", expireAt)\n\t\t\tExpect(expireAtCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expireAtCmd.Val()).To(Equal(true))\n\n\t\t\texpireTimeCmd = client.ExpireTime(ctx, \"key\")\n\t\t\tExpect(expireTimeCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expireTimeCmd.Val().Seconds()).To(BeNumerically(\"==\", expireAt.Unix()))\n\t\t})\n\n\t\tIt(\"should TTL\", func() {\n\t\t\t// The command returns -1 if the key exists but has no associated expire\n\t\t\t// The command returns -2 if the key does not exist.\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val() < 0).To(Equal(true))\n\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\texpire := client.Expire(ctx, \"key\", 60*time.Second)\n\t\t\tExpect(expire.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(expire.Val()).To(Equal(true))\n\n\t\t\tttl = client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(Equal(60 * time.Second))\n\t\t})\n\n\t\tIt(\"should Type\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\ttype_ := client.Type(ctx, \"key\")\n\t\t\tExpect(type_.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(type_.Val()).To(Equal(\"string\"))\n\t\t})\n\t})\n\n\tDescribe(\"scanning\", func() {\n\t\tIt(\"should Scan\", func() {\n\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\tset := client.Set(ctx, fmt.Sprintf(\"key%d\", i), \"hello\", 0)\n\t\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tkeys, cursor, err := client.Scan(ctx, 0, \"\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).NotTo(BeEmpty())\n\t\t\tExpect(cursor).NotTo(BeZero())\n\t\t})\n\n\t\tIt(\"should ScanType\", func() {\n\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\tset := client.Set(ctx, fmt.Sprintf(\"key%d\", i), \"hello\", 0)\n\t\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tkeys, cursor, err := client.ScanType(ctx, 0, \"\", 0, \"string\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).NotTo(BeEmpty())\n\t\t\tExpect(cursor).NotTo(BeZero())\n\t\t})\n\n\t\tIt(\"should SScan\", func() {\n\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\tsadd := client.SAdd(ctx, \"myset\", fmt.Sprintf(\"member%d\", i))\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tkeys, cursor, err := client.SScan(ctx, \"myset\", 0, \"\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).NotTo(BeEmpty())\n\t\t\tExpect(cursor).NotTo(BeZero())\n\t\t})\n\n\t\tIt(\"should HScan\", func() {\n\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tkeys, cursor, err := client.HScan(ctx, \"myhash\", 0, \"\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t// If we don't get at least two items back, it's really strange.\n\t\t\tExpect(cursor).To(BeNumerically(\">=\", 2))\n\t\t\tExpect(len(keys)).To(BeNumerically(\">=\", 2))\n\t\t\tExpect(keys[0]).To(HavePrefix(\"key\"))\n\t\t\tExpect(keys[1]).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should HScan without values\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tkeys, cursor, err := client.HScanNoValues(ctx, \"myhash\", 0, \"\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t// If we don't get at least two items back, it's really strange.\n\t\t\tExpect(cursor).To(BeNumerically(\">=\", 2))\n\t\t\tExpect(len(keys)).To(BeNumerically(\">=\", 2))\n\t\t\tExpect(keys[0]).To(HavePrefix(\"key\"))\n\t\t\tExpect(keys[1]).To(HavePrefix(\"key\"))\n\t\t\tExpect(keys).NotTo(BeEmpty())\n\t\t\tExpect(cursor).NotTo(BeZero())\n\t\t})\n\n\t\tIt(\"should ZScan\", func() {\n\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\terr := client.ZAdd(ctx, \"myset\", redis.Z{\n\t\t\t\t\tScore:  float64(i),\n\t\t\t\t\tMember: fmt.Sprintf(\"member%d\", i),\n\t\t\t\t}).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tkeys, cursor, err := client.ZScan(ctx, \"myset\", 0, \"\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).NotTo(BeEmpty())\n\t\t\tExpect(cursor).NotTo(BeZero())\n\t\t})\n\t})\n\n\tDescribe(\"strings\", func() {\n\t\tIt(\"should Append\", func() {\n\t\t\tn, err := client.Exists(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(0)))\n\n\t\t\tappendRes := client.Append(ctx, \"key\", \"Hello\")\n\t\t\tExpect(appendRes.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(appendRes.Val()).To(Equal(int64(5)))\n\n\t\t\tappendRes = client.Append(ctx, \"key\", \" World\")\n\t\t\tExpect(appendRes.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(appendRes.Val()).To(Equal(int64(11)))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"Hello World\"))\n\t\t})\n\n\t\tIt(\"should BitCount\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"foobar\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitCount := client.BitCount(ctx, \"key\", nil)\n\t\t\tExpect(bitCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitCount.Val()).To(Equal(int64(26)))\n\n\t\t\tbitCount = client.BitCount(ctx, \"key\", &redis.BitCount{\n\t\t\t\tStart: 0,\n\t\t\t\tEnd:   0,\n\t\t\t})\n\t\t\tExpect(bitCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitCount.Val()).To(Equal(int64(4)))\n\n\t\t\tbitCount = client.BitCount(ctx, \"key\", &redis.BitCount{\n\t\t\t\tStart: 1,\n\t\t\t\tEnd:   1,\n\t\t\t})\n\t\t\tExpect(bitCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitCount.Val()).To(Equal(int64(6)))\n\t\t})\n\n\t\tIt(\"should BitOpAnd\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tset := client.Set(ctx, \"key1\", \"1\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tset = client.Set(ctx, \"key2\", \"0\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpAnd := client.BitOpAnd(ctx, \"dest\", \"key1\", \"key2\")\n\t\t\tExpect(bitOpAnd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpAnd.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"0\"))\n\t\t})\n\n\t\tIt(\"should BitOpOr\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tset := client.Set(ctx, \"key1\", \"1\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tset = client.Set(ctx, \"key2\", \"0\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpOr := client.BitOpOr(ctx, \"dest\", \"key1\", \"key2\")\n\t\t\tExpect(bitOpOr.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpOr.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"1\"))\n\t\t})\n\n\t\tIt(\"should BitOpXor\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tset := client.Set(ctx, \"key1\", \"\\xff\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tset = client.Set(ctx, \"key2\", \"\\x0f\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpXor := client.BitOpXor(ctx, \"dest\", \"key1\", \"key2\")\n\t\t\tExpect(bitOpXor.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpXor.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"\\xf0\"))\n\t\t})\n\n\t\tIt(\"should BitOpDiff\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"BITOP DIFF is available since Redis 8.2\")\n\t\t\tset := client.Set(ctx, \"key1\", \"\\xff\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tset = client.Set(ctx, \"key2\", \"\\x0f\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpDiff := client.BitOpDiff(ctx, \"dest\", \"key1\", \"key2\")\n\t\t\tExpect(bitOpDiff.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpDiff.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"\\xf0\"))\n\t\t})\n\n\t\tIt(\"should BitOpDiff1\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"BITOP DIFF is available since Redis 8.2\")\n\t\t\tset := client.Set(ctx, \"key1\", \"\\xff\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tset = client.Set(ctx, \"key2\", \"\\x0f\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpDiff1 := client.BitOpDiff1(ctx, \"dest\", \"key1\", \"key2\")\n\t\t\tExpect(bitOpDiff1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpDiff1.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"\\x00\"))\n\t\t})\n\n\t\tIt(\"should BitOpAndOr\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"BITOP ANDOR is available since Redis 8.2\")\n\t\t\tset := client.Set(ctx, \"key1\", \"\\xff\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tset = client.Set(ctx, \"key2\", \"\\x0f\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpAndOr := client.BitOpAndOr(ctx, \"dest\", \"key1\", \"key2\")\n\t\t\tExpect(bitOpAndOr.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpAndOr.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"\\x0f\"))\n\t\t})\n\n\t\tIt(\"should BitOpOne\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"BITOP ONE is available since Redis 8.2\")\n\t\t\tset := client.Set(ctx, \"key1\", \"\\xff\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tset = client.Set(ctx, \"key2\", \"\\x0f\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpOne := client.BitOpOne(ctx, \"dest\", \"key1\", \"key2\")\n\t\t\tExpect(bitOpOne.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpOne.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"\\xf0\"))\n\t\t})\n\n\t\tIt(\"should BitOpNot\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tset := client.Set(ctx, \"key1\", \"\\x00\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tbitOpNot := client.BitOpNot(ctx, \"dest\", \"key1\")\n\t\t\tExpect(bitOpNot.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bitOpNot.Val()).To(Equal(int64(1)))\n\n\t\t\tget := client.Get(ctx, \"dest\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"\\xff\"))\n\t\t})\n\n\t\tIt(\"should BitPos\", func() {\n\t\t\terr := client.Set(ctx, \"mykey\", \"\\xff\\xf0\\x00\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tpos, err := client.BitPos(ctx, \"mykey\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(12)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(0)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 0, 2).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(16)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 1, 2).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(-1)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(16)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 1, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(-1)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 0, 2, 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(-1)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 0, 0, -3).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(-1)))\n\n\t\t\tpos, err = client.BitPos(ctx, \"mykey\", 0, 0, 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(-1)))\n\n\t\t\t_, err = client.BitPos(ctx, \"mykey\", 0, 0, 0, 0, 0).Result()\n\t\t\tExpect(err).Should(MatchError(\"too many arguments\"))\n\t\t})\n\n\t\tIt(\"should BitPosSpan\", func() {\n\t\t\terr := client.Set(ctx, \"mykey\", \"\\x00\\xff\\x00\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tpos, err := client.BitPosSpan(ctx, \"mykey\", 0, 1, 3, \"byte\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(16)))\n\n\t\t\tpos, err = client.BitPosSpan(ctx, \"mykey\", 0, 1, 3, \"bit\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should BitField\", func() {\n\t\t\tnn, err := client.BitField(ctx, \"mykey\", \"INCRBY\", \"i5\", 100, 1, \"GET\", \"u4\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(nn).To(Equal([]int64{1, 0}))\n\n\t\t\tnn, err = client.BitField(ctx, \"mykey\", \"set\", \"i1\", 1, 1, \"GET\", \"u4\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(nn).To(Equal([]int64{0, 4}))\n\t\t})\n\n\t\tIt(\"should BitFieldRO\", func() {\n\t\t\tnn, err := client.BitField(ctx, \"mykey\", \"SET\", \"u8\", 8, 255).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(nn).To(Equal([]int64{0}))\n\n\t\t\tnn, err = client.BitFieldRO(ctx, \"mykey\", \"u8\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(nn).To(Equal([]int64{0}))\n\n\t\t\tnn, err = client.BitFieldRO(ctx, \"mykey\", \"u8\", 0, \"u4\", 8, \"u4\", 12, \"u4\", 13).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(nn).To(Equal([]int64{0, 15, 15, 14}))\n\n\t\t\t_, err = client.BitFieldRO(ctx, \"mykey\", \"u8\", 0, \"u4\", 8, \"u4\", 12, \"u4\").Result()\n\t\t\tExpect(err).Should(MatchError(\"BitFieldRO: invalid number of arguments, must be even\"))\n\t\t})\n\n\t\tIt(\"should Decr\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"10\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tdecr := client.Decr(ctx, \"key\")\n\t\t\tExpect(decr.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(decr.Val()).To(Equal(int64(9)))\n\n\t\t\tset = client.Set(ctx, \"key\", \"234293482390480948029348230948\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tdecr = client.Decr(ctx, \"key\")\n\t\t\tExpect(decr.Err()).To(MatchError(\"ERR value is not an integer or out of range\"))\n\t\t\tExpect(decr.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should DecrBy\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"10\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tdecrBy := client.DecrBy(ctx, \"key\", 5)\n\t\t\tExpect(decrBy.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(decrBy.Val()).To(Equal(int64(5)))\n\t\t})\n\n\t\tIt(\"should Get\", func() {\n\t\t\tget := client.Get(ctx, \"_\")\n\t\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\t\tExpect(get.Val()).To(Equal(\"\"))\n\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tget = client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should GetBit\", func() {\n\t\t\tsetBit := client.SetBit(ctx, \"key\", 7, 1)\n\t\t\tExpect(setBit.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(setBit.Val()).To(Equal(int64(0)))\n\n\t\t\tgetBit := client.GetBit(ctx, \"key\", 0)\n\t\t\tExpect(getBit.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getBit.Val()).To(Equal(int64(0)))\n\n\t\t\tgetBit = client.GetBit(ctx, \"key\", 7)\n\t\t\tExpect(getBit.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getBit.Val()).To(Equal(int64(1)))\n\n\t\t\tgetBit = client.GetBit(ctx, \"key\", 100)\n\t\t\tExpect(getBit.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getBit.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should GetRange\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"This is a string\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tgetRange := client.GetRange(ctx, \"key\", 0, 3)\n\t\t\tExpect(getRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getRange.Val()).To(Equal(\"This\"))\n\n\t\t\tgetRange = client.GetRange(ctx, \"key\", -3, -1)\n\t\t\tExpect(getRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getRange.Val()).To(Equal(\"ing\"))\n\n\t\t\tgetRange = client.GetRange(ctx, \"key\", 0, -1)\n\t\t\tExpect(getRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getRange.Val()).To(Equal(\"This is a string\"))\n\n\t\t\tgetRange = client.GetRange(ctx, \"key\", 10, 100)\n\t\t\tExpect(getRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getRange.Val()).To(Equal(\"string\"))\n\t\t})\n\n\t\tIt(\"should GetSet\", func() {\n\t\t\tincr := client.Incr(ctx, \"key\")\n\t\t\tExpect(incr.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incr.Val()).To(Equal(int64(1)))\n\n\t\t\tgetSet := client.GetSet(ctx, \"key\", \"0\")\n\t\t\tExpect(getSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getSet.Val()).To(Equal(\"1\"))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"0\"))\n\t\t})\n\n\t\tIt(\"should GetEX\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"value\", 100*time.Second)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(BeNumerically(\"~\", 100*time.Second, 3*time.Second))\n\n\t\t\tgetEX := client.GetEx(ctx, \"key\", 200*time.Second)\n\t\t\tExpect(getEX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getEX.Val()).To(Equal(\"value\"))\n\n\t\t\tttl = client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).To(BeNumerically(\"~\", 200*time.Second, 3*time.Second))\n\t\t})\n\n\t\tIt(\"should GetDel\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"value\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tgetDel := client.GetDel(ctx, \"key\")\n\t\t\tExpect(getDel.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getDel.Val()).To(Equal(\"value\"))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should DelExArgs when value matches\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"lock\", \"token-123\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Delete only if value matches\n\t\t\tdeleted := client.DelExArgs(ctx, \"lock\", redis.DelExArgs{\n\t\t\t\tMode:       \"IFEQ\",\n\t\t\t\tMatchValue: \"token-123\",\n\t\t\t})\n\t\t\tExpect(deleted.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(deleted.Val()).To(Equal(int64(1)))\n\n\t\t\t// Verify key was deleted\n\t\t\tget := client.Get(ctx, \"lock\")\n\t\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should DelExArgs fail when value does not match\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"lock\", \"token-123\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Try to delete with wrong value\n\t\t\tdeleted := client.DelExArgs(ctx, \"lock\", redis.DelExArgs{\n\t\t\t\tMode:       \"IFEQ\",\n\t\t\t\tMatchValue: \"wrong-token\",\n\t\t\t})\n\t\t\tExpect(deleted.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(deleted.Val()).To(Equal(int64(0)))\n\n\t\t\t// Verify key was NOT deleted\n\t\t\tval, err := client.Get(ctx, \"lock\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"token-123\"))\n\t\t})\n\n\t\tIt(\"should DelExArgs on non-existent key\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Try to delete non-existent key\n\t\t\tdeleted := client.DelExArgs(ctx, \"nonexistent\", redis.DelExArgs{\n\t\t\t\tMode:       \"IFEQ\",\n\t\t\t\tMatchValue: \"any-value\",\n\t\t\t})\n\t\t\tExpect(deleted.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(deleted.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should DelExArgs with IFEQ\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"temp-key\", \"temp-value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Delete with IFEQ\n\t\t\targs := redis.DelExArgs{\n\t\t\t\tMode:       \"IFEQ\",\n\t\t\t\tMatchValue: \"temp-value\",\n\t\t\t}\n\t\t\tdeleted := client.DelExArgs(ctx, \"temp-key\", args)\n\t\t\tExpect(deleted.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(deleted.Val()).To(Equal(int64(1)))\n\n\t\t\t// Verify key was deleted\n\t\t\tget := client.Get(ctx, \"temp-key\")\n\t\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should DelExArgs with IFNE\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"temporary\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Delete only if value is NOT \"permanent\"\n\t\t\targs := redis.DelExArgs{\n\t\t\t\tMode:       \"IFNE\",\n\t\t\t\tMatchValue: \"permanent\",\n\t\t\t}\n\t\t\tdeleted := client.DelExArgs(ctx, \"key\", args)\n\t\t\tExpect(deleted.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(deleted.Val()).To(Equal(int64(1)))\n\n\t\t\t// Verify key was deleted\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should DelExArgs with IFNE fail when value matches\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"permanent\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Try to delete but value matches (should fail)\n\t\t\targs := redis.DelExArgs{\n\t\t\t\tMode:       \"IFNE\",\n\t\t\t\tMatchValue: \"permanent\",\n\t\t\t}\n\t\t\tdeleted := client.DelExArgs(ctx, \"key\", args)\n\t\t\tExpect(deleted.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(deleted.Val()).To(Equal(int64(0)))\n\n\t\t\t// Verify key was NOT deleted\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"permanent\"))\n\t\t})\n\n\t\tIt(\"should Digest\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set a value\n\t\t\terr := client.Set(ctx, \"my-key\", \"my-value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest (returns uint64)\n\t\t\tdigest := client.Digest(ctx, \"my-key\")\n\t\t\tExpect(digest.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(digest.Val()).NotTo(BeZero())\n\n\t\t\t// Digest should be consistent\n\t\t\tdigest2 := client.Digest(ctx, \"my-key\")\n\t\t\tExpect(digest2.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(digest2.Val()).To(Equal(digest.Val()))\n\t\t})\n\n\t\tIt(\"should Digest on non-existent key\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Get digest of non-existent key\n\t\t\tdigest := client.Digest(ctx, \"nonexistent\")\n\t\t\tExpect(digest.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should use Digest with SetArgs IFDEQ\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"value1\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest\n\t\t\tdigest := client.Digest(ctx, \"key\")\n\t\t\tExpect(digest.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Update using digest\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode:        \"IFDEQ\",\n\t\t\t\tMatchDigest: digest.Val(),\n\t\t\t}\n\t\t\tresult := client.SetArgs(ctx, \"key\", \"value2\", args)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"value2\"))\n\t\t})\n\n\t\tIt(\"should use Digest with DelExArgs IFDEQ\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest\n\t\t\tdigest := client.Digest(ctx, \"key\")\n\t\t\tExpect(digest.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Delete using digest\n\t\t\targs := redis.DelExArgs{\n\t\t\t\tMode:        \"IFDEQ\",\n\t\t\t\tMatchDigest: digest.Val(),\n\t\t\t}\n\t\t\tdeleted := client.DelExArgs(ctx, \"key\", args)\n\t\t\tExpect(deleted.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(deleted.Val()).To(Equal(int64(1)))\n\n\t\t\t// Verify key was deleted\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should Incr\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"10\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tincr := client.Incr(ctx, \"key\")\n\t\t\tExpect(incr.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incr.Val()).To(Equal(int64(11)))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"11\"))\n\t\t})\n\n\t\tIt(\"should IncrBy\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"10\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tincrBy := client.IncrBy(ctx, \"key\", 5)\n\t\t\tExpect(incrBy.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrBy.Val()).To(Equal(int64(15)))\n\t\t})\n\n\t\tIt(\"should IncrByFloat\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"10.50\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tincrByFloat := client.IncrByFloat(ctx, \"key\", 0.1)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(Equal(10.6))\n\n\t\t\tset = client.Set(ctx, \"key\", \"5.0e3\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"key\", 2.0e2)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(Equal(float64(5200)))\n\t\t})\n\n\t\tIt(\"should IncrByFloatOverflow\", func() {\n\t\t\tincrByFloat := client.IncrByFloat(ctx, \"key\", 996945661)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(Equal(float64(996945661)))\n\t\t})\n\n\t\tIt(\"should MSetMGet\", func() {\n\t\t\tmSet := client.MSet(ctx, \"key1\", \"hello1\", \"key2\", \"hello2\")\n\t\t\tExpect(mSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSet.Val()).To(Equal(\"OK\"))\n\n\t\t\tmGet := client.MGet(ctx, \"key1\", \"key2\", \"_\")\n\t\t\tExpect(mGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mGet.Val()).To(Equal([]interface{}{\"hello1\", \"hello2\", nil}))\n\n\t\t\t// MSet struct\n\t\t\ttype set struct {\n\t\t\t\tSet1 string                 `redis:\"set1\"`\n\t\t\t\tSet2 int16                  `redis:\"set2\"`\n\t\t\t\tSet3 time.Duration          `redis:\"set3\"`\n\t\t\t\tSet4 interface{}            `redis:\"set4\"`\n\t\t\t\tSet5 map[string]interface{} `redis:\"-\"`\n\t\t\t}\n\t\t\tmSet = client.MSet(ctx, &set{\n\t\t\t\tSet1: \"val1\",\n\t\t\t\tSet2: 1024,\n\t\t\t\tSet3: 2 * time.Millisecond,\n\t\t\t\tSet4: nil,\n\t\t\t\tSet5: map[string]interface{}{\"k1\": 1},\n\t\t\t})\n\t\t\tExpect(mSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSet.Val()).To(Equal(\"OK\"))\n\n\t\t\tmGet = client.MGet(ctx, \"set1\", \"set2\", \"set3\", \"set4\")\n\t\t\tExpect(mGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mGet.Val()).To(Equal([]interface{}{\n\t\t\t\t\"val1\",\n\t\t\t\t\"1024\",\n\t\t\t\tstrconv.Itoa(int(2 * time.Millisecond.Nanoseconds())),\n\t\t\t\t\"\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should scan Mget\", func() {\n\t\t\tnow := time.Now()\n\n\t\t\terr := client.MSet(ctx, \"key1\", \"hello1\", \"key2\", 123, \"time\", now.Format(time.RFC3339Nano)).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tres := client.MGet(ctx, \"key1\", \"key2\", \"_\", \"time\")\n\t\t\tExpect(res.Err()).NotTo(HaveOccurred())\n\n\t\t\ttype data struct {\n\t\t\t\tKey1 string    `redis:\"key1\"`\n\t\t\t\tKey2 int       `redis:\"key2\"`\n\t\t\t\tTime TimeValue `redis:\"time\"`\n\t\t\t}\n\t\t\tvar d data\n\t\t\tExpect(res.Scan(&d)).NotTo(HaveOccurred())\n\t\t\tExpect(d.Time.UnixNano()).To(Equal(now.UnixNano()))\n\t\t\td.Time.Time = time.Time{}\n\t\t\tExpect(d).To(Equal(data{\n\t\t\t\tKey1: \"hello1\",\n\t\t\t\tKey2: 123,\n\t\t\t\tTime: TimeValue{Time: time.Time{}},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should MSetNX\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tmSetNX := client.MSetNX(ctx, \"key1\", \"hello1\", \"key2\", \"hello2\")\n\t\t\tExpect(mSetNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetNX.Val()).To(Equal(true))\n\n\t\t\tmSetNX = client.MSetNX(ctx, \"key2\", \"hello1\", \"key3\", \"hello2\")\n\t\t\tExpect(mSetNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetNX.Val()).To(Equal(false))\n\n\t\t\t// set struct\n\t\t\t// MSet struct\n\t\t\ttype set struct {\n\t\t\t\tSet1 string                 `redis:\"set1\"`\n\t\t\t\tSet2 int16                  `redis:\"set2\"`\n\t\t\t\tSet3 time.Duration          `redis:\"set3\"`\n\t\t\t\tSet4 interface{}            `redis:\"set4\"`\n\t\t\t\tSet5 map[string]interface{} `redis:\"-\"`\n\t\t\t}\n\t\t\tmSetNX = client.MSetNX(ctx, &set{\n\t\t\t\tSet1: \"val1\",\n\t\t\t\tSet2: 1024,\n\t\t\t\tSet3: 2 * time.Millisecond,\n\t\t\t\tSet4: nil,\n\t\t\t\tSet5: map[string]interface{}{\"k1\": 1},\n\t\t\t})\n\t\t\tExpect(mSetNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetNX.Val()).To(Equal(true))\n\t\t})\n\n\t\tIt(\"should MSetEX\", func() {\n\t\t\tSkipBeforeRedisVersion(8.3, \"MSetEX is available since redis 8.4\")\n\t\t\targs := redis.MSetEXArgs{\n\t\t\t\tExpiration: &redis.ExpirationOption{\n\t\t\t\t\tMode:  redis.EX,\n\t\t\t\t\tValue: 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tmSetEX := client.MSetEX(ctx, args, \"key1\", \"hello1\", \"key2\", \"hello2\")\n\t\t\tExpect(mSetEX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetEX.Val()).To(Equal(int64(1)))\n\n\t\t\t// Verify keys were set\n\t\t\tval1 := client.Get(ctx, \"key1\")\n\t\t\tExpect(val1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val1.Val()).To(Equal(\"hello1\"))\n\n\t\t\tval2 := client.Get(ctx, \"key2\")\n\t\t\tExpect(val2.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val2.Val()).To(Equal(\"hello2\"))\n\n\t\t\t// Verify TTL was set\n\t\t\tttl1 := client.TTL(ctx, \"key1\")\n\t\t\tExpect(ttl1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl1.Val()).To(BeNumerically(\">\", 0))\n\t\t\tExpect(ttl1.Val()).To(BeNumerically(\"<=\", 1*time.Second))\n\n\t\t\tttl2 := client.TTL(ctx, \"key2\")\n\t\t\tExpect(ttl2.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl2.Val()).To(BeNumerically(\">\", 0))\n\t\t\tExpect(ttl2.Val()).To(BeNumerically(\"<=\", 1*time.Second))\n\t\t})\n\n\t\tIt(\"should MSetEX with NX mode\", func() {\n\t\t\tSkipBeforeRedisVersion(8.3, \"MSetEX is available since redis 8.4\")\n\n\t\t\tclient.Set(ctx, \"key1\", \"existing\", 0)\n\n\t\t\t// Try to set with NX mode - should fail because key1 exists\n\t\t\targs := redis.MSetEXArgs{\n\t\t\t\tCondition: redis.NX,\n\t\t\t\tExpiration: &redis.ExpirationOption{\n\t\t\t\t\tMode:  redis.EX,\n\t\t\t\t\tValue: 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tmSetEX := client.MSetEX(ctx, args, \"key1\", \"new1\", \"key2\", \"new2\")\n\t\t\tExpect(mSetEX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetEX.Val()).To(Equal(int64(0)))\n\n\t\t\tval1 := client.Get(ctx, \"key1\")\n\t\t\tExpect(val1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val1.Val()).To(Equal(\"existing\"))\n\n\t\t\tval2 := client.Get(ctx, \"key2\")\n\t\t\tExpect(val2.Err()).To(Equal(redis.Nil))\n\n\t\t\tclient.Del(ctx, \"key1\")\n\n\t\t\t// Now try with NX mode when keys don't exist - should succeed\n\t\t\tmSetEX = client.MSetEX(ctx, args, \"key1\", \"new1\", \"key2\", \"new2\")\n\t\t\tExpect(mSetEX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetEX.Val()).To(Equal(int64(1)))\n\n\t\t\tval1 = client.Get(ctx, \"key1\")\n\t\t\tExpect(val1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val1.Val()).To(Equal(\"new1\"))\n\n\t\t\tval2 = client.Get(ctx, \"key2\")\n\t\t\tExpect(val2.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val2.Val()).To(Equal(\"new2\"))\n\t\t})\n\n\t\tIt(\"should MSetEX with XX mode\", func() {\n\t\t\tSkipBeforeRedisVersion(8.3, \"MSetEX is available since redis 8.4\")\n\n\t\t\targs := redis.MSetEXArgs{\n\t\t\t\tCondition: redis.XX,\n\t\t\t\tExpiration: &redis.ExpirationOption{\n\t\t\t\t\tMode:  redis.EX,\n\t\t\t\t\tValue: 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tmSetEX := client.MSetEX(ctx, args, \"key1\", \"new1\", \"key2\", \"new2\")\n\t\t\tExpect(mSetEX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetEX.Val()).To(Equal(int64(0)))\n\n\t\t\tclient.Set(ctx, \"key1\", \"existing1\", 0)\n\t\t\tclient.Set(ctx, \"key2\", \"existing2\", 0)\n\n\t\t\tmSetEX = client.MSetEX(ctx, args, \"key1\", \"new1\", \"key2\", \"new2\")\n\t\t\tExpect(mSetEX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetEX.Val()).To(Equal(int64(1)))\n\n\t\t\tval1 := client.Get(ctx, \"key1\")\n\t\t\tExpect(val1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val1.Val()).To(Equal(\"new1\"))\n\n\t\t\tval2 := client.Get(ctx, \"key2\")\n\t\t\tExpect(val2.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val2.Val()).To(Equal(\"new2\"))\n\n\t\t\tttl1 := client.TTL(ctx, \"key1\")\n\t\t\tExpect(ttl1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl1.Val()).To(BeNumerically(\">\", 0))\n\t\t})\n\n\t\tIt(\"should MSetEX with map\", func() {\n\t\t\tSkipBeforeRedisVersion(8.3, \"MSetEX is available since redis 8.4\")\n\t\t\targs := redis.MSetEXArgs{\n\t\t\t\tExpiration: &redis.ExpirationOption{\n\t\t\t\t\tMode:  redis.EX,\n\t\t\t\t\tValue: 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tmSetEX := client.MSetEX(ctx, args, map[string]interface{}{\n\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\"key2\": \"value2\",\n\t\t\t})\n\t\t\tExpect(mSetEX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(mSetEX.Val()).To(Equal(int64(1)))\n\n\t\t\tval1 := client.Get(ctx, \"key1\")\n\t\t\tExpect(val1.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val1.Val()).To(Equal(\"value1\"))\n\n\t\t\tval2 := client.Get(ctx, \"key2\")\n\t\t\tExpect(val2.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(val2.Val()).To(Equal(\"value2\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with TTL\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tTTL: 500 * time.Millisecond,\n\t\t\t}\n\t\t\terr := client.SetArgs(ctx, \"key\", \"hello\", args).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Get(ctx, \"key\").Err()\n\t\t\t}, \"2s\", \"100ms\").Should(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with expiration date\", func() {\n\t\t\texpireAt := time.Now().AddDate(1, 1, 1)\n\t\t\targs := redis.SetArgs{\n\t\t\t\tExpireAt: expireAt,\n\t\t\t}\n\t\t\terr := client.SetArgs(ctx, \"key\", \"hello\", args).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\n\t\t\t// check the key has an expiration date\n\t\t\t// (so a TTL value different of -1)\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val()).ToNot(Equal(-1))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with negative expiration date\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tExpireAt: time.Now().AddDate(-3, 1, 1),\n\t\t\t}\n\t\t\t// redis accepts a timestamp less than the current date\n\t\t\t// but returns nil when trying to get the key\n\t\t\terr := client.SetArgs(ctx, \"key\", \"hello\", args).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with keepttl\", func() {\n\t\t\t// Set with ttl\n\t\t\targsWithTTL := redis.SetArgs{\n\t\t\t\tTTL: 5 * time.Second,\n\t\t\t}\n\t\t\tset := client.SetArgs(ctx, \"key\", \"hello\", argsWithTTL)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Result()).To(Equal(\"OK\"))\n\n\t\t\t// Set with keepttl\n\t\t\targsWithKeepTTL := redis.SetArgs{\n\t\t\t\tKeepTTL: true,\n\t\t\t}\n\t\t\tset = client.SetArgs(ctx, \"key\", \"hello\", argsWithKeepTTL)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Result()).To(Equal(\"OK\"))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\t// set keepttl will Retain the ttl associated with the key\n\t\t\tExpect(ttl.Val().Nanoseconds()).NotTo(Equal(-1))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with NX mode and key exists\", func() {\n\t\t\terr := client.Set(ctx, \"key\", \"hello\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode: \"nx\",\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"hello\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with NX mode and key does not exist\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode: \"nx\",\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"hello\", args).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with NX mode and GET option\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode: \"nx\",\n\t\t\t\tGet:  true,\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"hello\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with expiration, NX mode, and key does not exist\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tTTL:  500 * time.Millisecond,\n\t\t\t\tMode: \"nx\",\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"hello\", args).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"OK\"))\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Get(ctx, \"key\").Err()\n\t\t\t}, \"1s\", \"100ms\").Should(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with expiration, NX mode, and key exists\", func() {\n\t\t\te := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(e.Err()).NotTo(HaveOccurred())\n\n\t\t\targs := redis.SetArgs{\n\t\t\t\tTTL:  500 * time.Millisecond,\n\t\t\t\tMode: \"nx\",\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with expiration, NX mode, and GET option\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tTTL:  500 * time.Millisecond,\n\t\t\t\tMode: \"nx\",\n\t\t\t\tGet:  true,\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"hello\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with XX mode and key does not exist\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode: \"xx\",\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with XX mode and key exists\", func() {\n\t\t\te := client.Set(ctx, \"key\", \"hello\", 0).Err()\n\t\t\tExpect(e).NotTo(HaveOccurred())\n\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode: \"xx\",\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with XX mode and GET option, and key exists\", func() {\n\t\t\te := client.Set(ctx, \"key\", \"hello\", 0).Err()\n\t\t\tExpect(e).NotTo(HaveOccurred())\n\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode: \"xx\",\n\t\t\t\tGet:  true,\n\t\t\t}\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with XX mode and GET option, and key does not exist\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode: \"xx\",\n\t\t\t\tGet:  true,\n\t\t\t}\n\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with expiration, XX mode, GET option, and key does not exist\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tTTL:  500 * time.Millisecond,\n\t\t\t\tMode: \"xx\",\n\t\t\t\tGet:  true,\n\t\t\t}\n\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with expiration, XX mode, GET option, and key exists\", func() {\n\t\t\te := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(e.Err()).NotTo(HaveOccurred())\n\n\t\t\targs := redis.SetArgs{\n\t\t\t\tTTL:  500 * time.Millisecond,\n\t\t\t\tMode: \"xx\",\n\t\t\t\tGet:  true,\n\t\t\t}\n\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Get(ctx, \"key\").Err()\n\t\t\t}, \"1s\", \"100ms\").Should(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with Get and key does not exist yet\", func() {\n\t\t\targs := redis.SetArgs{\n\t\t\t\tGet: true,\n\t\t\t}\n\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"hello\", args).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should SetWithArgs with Get and key exists\", func() {\n\t\t\te := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(e.Err()).NotTo(HaveOccurred())\n\n\t\t\targs := redis.SetArgs{\n\t\t\t\tGet: true,\n\t\t\t}\n\n\t\t\tval, err := client.SetArgs(ctx, \"key\", \"world\", args).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should Pipelined SetArgs with Get and key exists\", func() {\n\t\t\te := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(e.Err()).NotTo(HaveOccurred())\n\n\t\t\targs := redis.SetArgs{\n\t\t\t\tGet: true,\n\t\t\t}\n\n\t\t\tpipe := client.Pipeline()\n\t\t\tsetArgs := pipe.SetArgs(ctx, \"key\", \"world\", args)\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(setArgs.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(setArgs.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should Set with expiration\", func() {\n\t\t\terr := client.Set(ctx, \"key\", \"hello\", 100*time.Millisecond).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Get(ctx, \"key\").Err()\n\t\t\t}, \"1s\", \"100ms\").Should(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should Set with keepttl\", func() {\n\t\t\t// set with ttl\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 5*time.Second)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\t// set with keepttl\n\t\t\tset = client.Set(ctx, \"key\", \"hello1\", redis.KeepTTL)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\t// set keepttl will Retain the ttl associated with the key\n\t\t\tExpect(ttl.Val().Nanoseconds()).NotTo(Equal(-1))\n\t\t})\n\n\t\tIt(\"should SetGet\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should SetEX\", func() {\n\t\t\terr := client.SetEx(ctx, \"key\", \"hello\", 1*time.Second).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Get(ctx, \"foo\").Err()\n\t\t\t}, \"2s\", \"100ms\").Should(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should SetNX\", func() {\n\t\t\tsetNX := client.SetNX(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(setNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(setNX.Val()).To(Equal(true))\n\n\t\t\tsetNX = client.SetNX(ctx, \"key\", \"hello2\", 0)\n\t\t\tExpect(setNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(setNX.Val()).To(Equal(false))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should SetNX with expiration\", func() {\n\t\t\tisSet, err := client.SetNX(ctx, \"key\", \"hello\", time.Second).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(true))\n\n\t\t\tisSet, err = client.SetNX(ctx, \"key\", \"hello2\", time.Second).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(false))\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should SetNX with keepttl\", func() {\n\t\t\tisSet, err := client.SetNX(ctx, \"key\", \"hello1\", redis.KeepTTL).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(true))\n\n\t\t\tttl := client.TTL(ctx, \"key\")\n\t\t\tExpect(ttl.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ttl.Val().Nanoseconds()).To(Equal(int64(-1)))\n\t\t})\n\n\t\tIt(\"should SetXX\", func() {\n\t\t\tisSet, err := client.SetXX(ctx, \"key\", \"hello2\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(false))\n\n\t\t\terr = client.Set(ctx, \"key\", \"hello\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tisSet, err = client.SetXX(ctx, \"key\", \"hello2\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(true))\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello2\"))\n\t\t})\n\n\t\tIt(\"should SetXX with expiration\", func() {\n\t\t\tisSet, err := client.SetXX(ctx, \"key\", \"hello2\", time.Second).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(false))\n\n\t\t\terr = client.Set(ctx, \"key\", \"hello\", time.Second).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tisSet, err = client.SetXX(ctx, \"key\", \"hello2\", time.Second).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(true))\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello2\"))\n\t\t})\n\n\t\tIt(\"should SetXX with keepttl\", func() {\n\t\t\tisSet, err := client.SetXX(ctx, \"key\", \"hello2\", time.Second).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(false))\n\n\t\t\terr = client.Set(ctx, \"key\", \"hello\", time.Second).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tisSet, err = client.SetXX(ctx, \"key\", \"hello2\", 5*time.Second).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(true))\n\n\t\t\tisSet, err = client.SetXX(ctx, \"key\", \"hello3\", redis.KeepTTL).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(isSet).To(Equal(true))\n\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"hello3\"))\n\n\t\t\t// set keepttl will Retain the ttl associated with the key\n\t\t\tttl, err := client.TTL(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(ttl).NotTo(Equal(-1))\n\t\t})\n\n\t\tIt(\"should SetIFEQ when value matches\", func() {\n\t\t\tif RedisVersion < 8.4 {\n\t\t\t\tSkip(\"CAS/CAD commands require Redis >= 8.4\")\n\t\t\t}\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"old-value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update only if current value is \"old-value\"\n\t\t\tresult := client.SetIFEQ(ctx, \"key\", \"new-value\", \"old-value\", 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"new-value\"))\n\t\t})\n\n\t\tIt(\"should SetIFEQ fail when value does not match\", func() {\n\t\t\tif RedisVersion < 8.4 {\n\t\t\t\tSkip(\"CAS/CAD commands require Redis >= 8.4\")\n\t\t\t}\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"current-value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Try to update with wrong match value\n\t\t\tresult := client.SetIFEQ(ctx, \"key\", \"new-value\", \"wrong-value\", 0)\n\t\t\tExpect(result.Err()).To(Equal(redis.Nil))\n\n\t\t\t// Verify value was NOT updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"current-value\"))\n\t\t})\n\n\t\tIt(\"should SetIFEQ with expiration\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"token-123\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update with expiration\n\t\t\tresult := client.SetIFEQ(ctx, \"key\", \"token-456\", \"token-123\", 500*time.Millisecond)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"token-456\"))\n\n\t\t\t// Wait for expiration\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Get(ctx, \"key\").Err()\n\t\t\t}, \"1s\", \"100ms\").Should(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should SetIFNE when value does not match\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"pending\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update only if current value is NOT \"completed\"\n\t\t\tresult := client.SetIFNE(ctx, \"key\", \"processing\", \"completed\", 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"processing\"))\n\t\t})\n\n\t\tIt(\"should SetIFNE fail when value matches\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"completed\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Try to update but value matches (should fail)\n\t\t\tresult := client.SetIFNE(ctx, \"key\", \"processing\", \"completed\", 0)\n\t\t\tExpect(result.Err()).To(Equal(redis.Nil))\n\n\t\t\t// Verify value was NOT updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"completed\"))\n\t\t})\n\n\t\tIt(\"should SetArgs with IFEQ\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"counter\", \"100\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update with IFEQ\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode:       \"IFEQ\",\n\t\t\t\tMatchValue: \"100\",\n\t\t\t\tTTL:        1 * time.Hour,\n\t\t\t}\n\t\t\tresult := client.SetArgs(ctx, \"counter\", \"200\", args)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"counter\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"200\"))\n\t\t})\n\n\t\tIt(\"should SetArgs with IFEQ and GET\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"old\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update with IFEQ and GET old value\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode:       \"IFEQ\",\n\t\t\t\tMatchValue: \"old\",\n\t\t\t\tGet:        true,\n\t\t\t}\n\t\t\tresult := client.SetArgs(ctx, \"key\", \"new\", args)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"old\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"new\"))\n\t\t})\n\n\t\tIt(\"should SetArgs with IFNE\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"status\", \"pending\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update with IFNE\n\t\t\targs := redis.SetArgs{\n\t\t\t\tMode:       \"IFNE\",\n\t\t\t\tMatchValue: \"completed\",\n\t\t\t\tTTL:        30 * time.Minute,\n\t\t\t}\n\t\t\tresult := client.SetArgs(ctx, \"status\", \"processing\", args)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"status\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"processing\"))\n\t\t})\n\n\t\tIt(\"should SetIFEQGet return previous value\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"old-value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update and get previous value\n\t\t\tresult := client.SetIFEQGet(ctx, \"key\", \"new-value\", \"old-value\", 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"old-value\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"new-value\"))\n\t\t})\n\n\t\tIt(\"should SetIFNEGet return previous value\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"pending\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Update and get previous value\n\t\t\tresult := client.SetIFNEGet(ctx, \"key\", \"processing\", \"completed\", 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"pending\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"processing\"))\n\t\t})\n\n\t\tIt(\"should SetIFDEQ when digest matches\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"value1\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest\n\t\t\tdigest := client.Digest(ctx, \"key\")\n\t\t\tExpect(digest.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Update using digest\n\t\t\tresult := client.SetIFDEQ(ctx, \"key\", \"value2\", digest.Val(), 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"value2\"))\n\t\t})\n\n\t\tIt(\"should SetIFDEQ fail when digest does not match\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"value1\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest of a different value to use as wrong digest\n\t\t\terr = client.Set(ctx, \"temp-key\", \"different-value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\twrongDigest := client.Digest(ctx, \"temp-key\")\n\t\t\tExpect(wrongDigest.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Try to update with wrong digest\n\t\t\tresult := client.SetIFDEQ(ctx, \"key\", \"value2\", wrongDigest.Val(), 0)\n\t\t\tExpect(result.Err()).To(Equal(redis.Nil))\n\n\t\t\t// Verify value was NOT updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"value1\"))\n\t\t})\n\n\t\tIt(\"should SetIFDEQGet return previous value\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"value1\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest\n\t\t\tdigest := client.Digest(ctx, \"key\")\n\t\t\tExpect(digest.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Update using digest and get previous value\n\t\t\tresult := client.SetIFDEQGet(ctx, \"key\", \"value2\", digest.Val(), 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"value1\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"value2\"))\n\t\t})\n\n\t\tIt(\"should SetIFDNE when digest does not match\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"value1\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest of a different value\n\t\t\terr = client.Set(ctx, \"temp-key\", \"different-value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tdifferentDigest := client.Digest(ctx, \"temp-key\")\n\t\t\tExpect(differentDigest.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Update with different digest (should succeed because digest doesn't match)\n\t\t\tresult := client.SetIFDNE(ctx, \"key\", \"value2\", differentDigest.Val(), 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\t// Verify value was updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"value2\"))\n\t\t})\n\n\t\tIt(\"should SetIFDNE fail when digest matches\", func() {\n\t\t\tSkipBeforeRedisVersion(8.4, \"CAS/CAD commands require Redis >= 8.4\")\n\n\t\t\t// Set initial value\n\t\t\terr := client.Set(ctx, \"key\", \"value1\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Get digest\n\t\t\tdigest := client.Digest(ctx, \"key\")\n\t\t\tExpect(digest.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Try to update but digest matches (should fail)\n\t\t\tresult := client.SetIFDNE(ctx, \"key\", \"value2\", digest.Val(), 0)\n\t\t\tExpect(result.Err()).To(Equal(redis.Nil))\n\n\t\t\t// Verify value was NOT updated\n\t\t\tval, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"value1\"))\n\t\t})\n\n\t\tIt(\"should SetRange\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"Hello World\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\trange_ := client.SetRange(ctx, \"key\", 6, \"Redis\")\n\t\t\tExpect(range_.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(range_.Val()).To(Equal(int64(11)))\n\n\t\t\tget := client.Get(ctx, \"key\")\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(get.Val()).To(Equal(\"Hello Redis\"))\n\t\t})\n\n\t\tIt(\"should StrLen\", func() {\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tstrLen := client.StrLen(ctx, \"key\")\n\t\t\tExpect(strLen.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(strLen.Val()).To(Equal(int64(5)))\n\n\t\t\tstrLen = client.StrLen(ctx, \"_\")\n\t\t\tExpect(strLen.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(strLen.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should Copy\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tset := client.Set(ctx, \"key\", \"hello\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\t\tcopy := client.Copy(ctx, \"key\", \"newKey\", redisOptions().DB, false)\n\t\t\tExpect(copy.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(copy.Val()).To(Equal(int64(1)))\n\n\t\t\t// Value is available by both keys now\n\t\t\tgetOld := client.Get(ctx, \"key\")\n\t\t\tExpect(getOld.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getOld.Val()).To(Equal(\"hello\"))\n\t\t\tgetNew := client.Get(ctx, \"newKey\")\n\t\t\tExpect(getNew.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getNew.Val()).To(Equal(\"hello\"))\n\n\t\t\t// Overwriting an existing key should not succeed\n\t\t\toverwrite := client.Copy(ctx, \"newKey\", \"key\", redisOptions().DB, false)\n\t\t\tExpect(overwrite.Val()).To(Equal(int64(0)))\n\n\t\t\t// Overwrite is allowed when replace=rue\n\t\t\treplace := client.Copy(ctx, \"newKey\", \"key\", redisOptions().DB, true)\n\t\t\tExpect(replace.Val()).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should fail module loadex\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tdryRun := client.ModuleLoadex(ctx, &redis.ModuleLoadexConfig{\n\t\t\t\tPath: \"/path/to/non-existent-library.so\",\n\t\t\t\tConf: map[string]interface{}{\n\t\t\t\t\t\"param1\": \"value1\",\n\t\t\t\t},\n\t\t\t\tArgs: []interface{}{\n\t\t\t\t\t\"arg1\",\n\t\t\t\t},\n\t\t\t})\n\t\t\tExpect(dryRun.Err()).To(HaveOccurred())\n\t\t\tExpect(dryRun.Err().Error()).To(Equal(\"ERR Error loading the extension. Please check the server logs.\"))\n\t\t})\n\n\t\tIt(\"converts the module loadex configuration to a slice of arguments correctly\", func() {\n\t\t\tconf := &redis.ModuleLoadexConfig{\n\t\t\t\tPath: \"/path/to/your/module.so\",\n\t\t\t\tConf: map[string]interface{}{\n\t\t\t\t\t\"param1\": \"value1\",\n\t\t\t\t},\n\t\t\t\tArgs: []interface{}{\n\t\t\t\t\t\"arg1\",\n\t\t\t\t\t\"arg2\",\n\t\t\t\t\t3,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\targs := conf.ToArgs()\n\n\t\t\t// Test if the arguments are in the correct order\n\t\t\texpectedArgs := []interface{}{\n\t\t\t\t\"MODULE\",\n\t\t\t\t\"LOADEX\",\n\t\t\t\t\"/path/to/your/module.so\",\n\t\t\t\t\"CONFIG\",\n\t\t\t\t\"param1\",\n\t\t\t\t\"value1\",\n\t\t\t\t\"ARGS\",\n\t\t\t\t\"arg1\",\n\t\t\t\t\"ARGS\",\n\t\t\t\t\"arg2\",\n\t\t\t\t\"ARGS\",\n\t\t\t\t3,\n\t\t\t}\n\n\t\t\tExpect(args).To(Equal(expectedArgs))\n\t\t})\n\n\t\tIt(\"should IncrByFloat with edge cases\", func() {\n\t\t\t// Test with negative increment\n\t\t\tset := client.Set(ctx, \"key\", \"10.5\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\n\t\t\tincrByFloat := client.IncrByFloat(ctx, \"key\", -2.3)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(BeNumerically(\"~\", 8.2, 0.0001))\n\n\t\t\t// Test with zero increment (should return current value)\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"key\", 0.0)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(BeNumerically(\"~\", 8.2, 0.0001))\n\n\t\t\t// Test with very small increment (precision test)\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"key\", 0.0001)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(BeNumerically(\"~\", 8.2001, 0.00001))\n\n\t\t\t// Test with non-existent key (should start from 0)\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"nonexistent\", 5.5)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(Equal(5.5))\n\n\t\t\t// Test with integer value stored as string\n\t\t\tset = client.Set(ctx, \"intkey\", \"42\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"intkey\", 0.5)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(Equal(42.5))\n\n\t\t\t// Test with scientific notation\n\t\t\tset = client.Set(ctx, \"scikey\", \"1.5e2\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"scikey\", 5.0)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(Equal(155.0))\n\n\t\t\t// Test with negative scientific notation\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"scikey\", -1.5e1)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(Equal(140.0))\n\n\t\t\t// Test error case: non-numeric value\n\t\t\tset = client.Set(ctx, \"stringkey\", \"notanumber\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"stringkey\", 1.0)\n\t\t\tExpect(incrByFloat.Err()).To(HaveOccurred())\n\t\t\tExpect(incrByFloat.Err().Error()).To(ContainSubstring(\"value is not a valid float\"))\n\n\t\t\t// Test with very large numbers\n\t\t\tset = client.Set(ctx, \"largekey\", \"1.7976931348623157e+308\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\n\t\t\t// This should work as it's within float64 range\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"largekey\", -1.0e+308)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(BeNumerically(\"~\", 7.976931348623157e+307, 1e+300))\n\n\t\t\t// Test with very small numbers (near zero)\n\t\t\tset = client.Set(ctx, \"smallkey\", \"1e-10\", 0)\n\t\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\n\t\t\tincrByFloat = client.IncrByFloat(ctx, \"smallkey\", 1e-10)\n\t\t\tExpect(incrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(incrByFloat.Val()).To(BeNumerically(\"~\", 2e-10, 1e-15))\n\t\t})\n\t})\n\n\tDescribe(\"hashes\", func() {\n\t\tIt(\"should HDel\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"key\", \"hello\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\n\t\t\thDel := client.HDel(ctx, \"hash\", \"key\")\n\t\t\tExpect(hDel.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hDel.Val()).To(Equal(int64(1)))\n\n\t\t\thDel = client.HDel(ctx, \"hash\", \"key\")\n\t\t\tExpect(hDel.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hDel.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should HExists\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"key\", \"hello\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\n\t\t\thExists := client.HExists(ctx, \"hash\", \"key\")\n\t\t\tExpect(hExists.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hExists.Val()).To(Equal(true))\n\n\t\t\thExists = client.HExists(ctx, \"hash\", \"key1\")\n\t\t\tExpect(hExists.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hExists.Val()).To(Equal(false))\n\t\t})\n\n\t\tIt(\"should HGet\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"key\", \"hello\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\n\t\t\thGet := client.HGet(ctx, \"hash\", \"key\")\n\t\t\tExpect(hGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hGet.Val()).To(Equal(\"hello\"))\n\n\t\t\thGet = client.HGet(ctx, \"hash\", \"key1\")\n\t\t\tExpect(hGet.Err()).To(Equal(redis.Nil))\n\t\t\tExpect(hGet.Val()).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should HGetAll\", func() {\n\t\t\terr := client.HSet(ctx, \"hash\", \"key1\", \"hello1\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.HSet(ctx, \"hash\", \"key2\", \"hello2\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tm, err := client.HGetAll(ctx, \"hash\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(m).To(Equal(map[string]string{\"key1\": \"hello1\", \"key2\": \"hello2\"}))\n\t\t})\n\n\t\tIt(\"should scan\", func() {\n\t\t\tnow := time.Now()\n\n\t\t\terr := client.HMSet(ctx, \"hash\", \"key1\", \"hello1\", \"key2\", 123, \"time\", now.Format(time.RFC3339Nano)).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tres := client.HGetAll(ctx, \"hash\")\n\t\t\tExpect(res.Err()).NotTo(HaveOccurred())\n\n\t\t\ttype data struct {\n\t\t\t\tKey1 string    `redis:\"key1\"`\n\t\t\t\tKey2 int       `redis:\"key2\"`\n\t\t\t\tTime TimeValue `redis:\"time\"`\n\t\t\t}\n\t\t\tvar d data\n\t\t\tExpect(res.Scan(&d)).NotTo(HaveOccurred())\n\t\t\tExpect(d.Time.UnixNano()).To(Equal(now.UnixNano()))\n\t\t\td.Time.Time = time.Time{}\n\t\t\tExpect(d).To(Equal(data{\n\t\t\t\tKey1: \"hello1\",\n\t\t\t\tKey2: 123,\n\t\t\t\tTime: TimeValue{Time: time.Time{}},\n\t\t\t}))\n\n\t\t\ttype data2 struct {\n\t\t\t\tKey1 string    `redis:\"key1\"`\n\t\t\t\tKey2 int       `redis:\"key2\"`\n\t\t\t\tTime time.Time `redis:\"time\"`\n\t\t\t}\n\t\t\terr = client.HSet(ctx, \"hash\", &data2{\n\t\t\t\tKey1: \"hello2\",\n\t\t\t\tKey2: 200,\n\t\t\t\tTime: now,\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvar d2 data2\n\t\t\terr = client.HMGet(ctx, \"hash\", \"key1\", \"key2\", \"time\").Scan(&d2)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(d2.Key1).To(Equal(\"hello2\"))\n\t\t\tExpect(d2.Key2).To(Equal(200))\n\t\t\tExpect(d2.Time.Unix()).To(Equal(now.Unix()))\n\t\t})\n\n\t\tIt(\"should HIncrBy\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"key\", \"5\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\n\t\t\thIncrBy := client.HIncrBy(ctx, \"hash\", \"key\", 1)\n\t\t\tExpect(hIncrBy.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hIncrBy.Val()).To(Equal(int64(6)))\n\n\t\t\thIncrBy = client.HIncrBy(ctx, \"hash\", \"key\", -1)\n\t\t\tExpect(hIncrBy.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hIncrBy.Val()).To(Equal(int64(5)))\n\n\t\t\thIncrBy = client.HIncrBy(ctx, \"hash\", \"key\", -10)\n\t\t\tExpect(hIncrBy.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hIncrBy.Val()).To(Equal(int64(-5)))\n\t\t})\n\n\t\tIt(\"should HIncrByFloat\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"field\", \"10.50\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSet.Val()).To(Equal(int64(1)))\n\n\t\t\thIncrByFloat := client.HIncrByFloat(ctx, \"hash\", \"field\", 0.1)\n\t\t\tExpect(hIncrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hIncrByFloat.Val()).To(Equal(10.6))\n\n\t\t\thSet = client.HSet(ctx, \"hash\", \"field\", \"5.0e3\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSet.Val()).To(Equal(int64(0)))\n\n\t\t\thIncrByFloat = client.HIncrByFloat(ctx, \"hash\", \"field\", 2.0e2)\n\t\t\tExpect(hIncrByFloat.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hIncrByFloat.Val()).To(Equal(float64(5200)))\n\t\t})\n\n\t\tIt(\"should HKeys\", func() {\n\t\t\thkeys := client.HKeys(ctx, \"hash\")\n\t\t\tExpect(hkeys.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hkeys.Val()).To(Equal([]string{}))\n\n\t\t\thset := client.HSet(ctx, \"hash\", \"key1\", \"hello1\")\n\t\t\tExpect(hset.Err()).NotTo(HaveOccurred())\n\t\t\thset = client.HSet(ctx, \"hash\", \"key2\", \"hello2\")\n\t\t\tExpect(hset.Err()).NotTo(HaveOccurred())\n\n\t\t\thkeys = client.HKeys(ctx, \"hash\")\n\t\t\tExpect(hkeys.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hkeys.Val()).To(Equal([]string{\"key1\", \"key2\"}))\n\t\t})\n\n\t\tIt(\"should HLen\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"key1\", \"hello1\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\thSet = client.HSet(ctx, \"hash\", \"key2\", \"hello2\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\n\t\t\thLen := client.HLen(ctx, \"hash\")\n\t\t\tExpect(hLen.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hLen.Val()).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should HMGet\", func() {\n\t\t\terr := client.HSet(ctx, \"hash\", \"key1\", \"hello1\", \"key2\", \"hello2\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.HMGet(ctx, \"hash\", \"key1\", \"key2\", \"_\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]interface{}{\"hello1\", \"hello2\", nil}))\n\t\t})\n\n\t\tIt(\"should HSet\", func() {\n\t\t\tok, err := client.HSet(ctx, \"hash\", map[string]interface{}{\n\t\t\t\t\"key1\": \"hello1\",\n\t\t\t\t\"key2\": \"hello2\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(ok).To(Equal(int64(2)))\n\n\t\t\tv, err := client.HGet(ctx, \"hash\", \"key1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(\"hello1\"))\n\n\t\t\tv, err = client.HGet(ctx, \"hash\", \"key2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(\"hello2\"))\n\n\t\t\tkeys, err := client.HKeys(ctx, \"hash\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(keys).To(ConsistOf([]string{\"key1\", \"key2\"}))\n\t\t})\n\n\t\tIt(\"should HSet\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"key\", \"hello\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSet.Val()).To(Equal(int64(1)))\n\n\t\t\thGet := client.HGet(ctx, \"hash\", \"key\")\n\t\t\tExpect(hGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hGet.Val()).To(Equal(\"hello\"))\n\n\t\t\t// set struct\n\t\t\t// MSet struct\n\t\t\ttype set struct {\n\t\t\t\tSet1 string                 `redis:\"set1\"`\n\t\t\t\tSet2 int16                  `redis:\"set2\"`\n\t\t\t\tSet3 time.Duration          `redis:\"set3\"`\n\t\t\t\tSet4 interface{}            `redis:\"set4\"`\n\t\t\t\tSet5 map[string]interface{} `redis:\"-\"`\n\t\t\t\tSet6 string                 `redis:\"set6,omitempty\"`\n\t\t\t}\n\n\t\t\thSet = client.HSet(ctx, \"hash\", &set{\n\t\t\t\tSet1: \"val1\",\n\t\t\t\tSet2: 1024,\n\t\t\t\tSet3: 2 * time.Millisecond,\n\t\t\t\tSet4: nil,\n\t\t\t\tSet5: map[string]interface{}{\"k1\": 1},\n\t\t\t})\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSet.Val()).To(Equal(int64(4)))\n\n\t\t\thMGet := client.HMGet(ctx, \"hash\", \"set1\", \"set2\", \"set3\", \"set4\", \"set5\", \"set6\")\n\t\t\tExpect(hMGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hMGet.Val()).To(Equal([]interface{}{\n\t\t\t\t\"val1\",\n\t\t\t\t\"1024\",\n\t\t\t\tstrconv.Itoa(int(2 * time.Millisecond.Nanoseconds())),\n\t\t\t\t\"\",\n\t\t\t\tnil,\n\t\t\t\tnil,\n\t\t\t}))\n\n\t\t\thSet = client.HSet(ctx, \"hash2\", &set{\n\t\t\t\tSet1: \"val2\",\n\t\t\t\tSet6: \"val\",\n\t\t\t})\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSet.Val()).To(Equal(int64(5)))\n\n\t\t\thMGet = client.HMGet(ctx, \"hash2\", \"set1\", \"set6\")\n\t\t\tExpect(hMGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hMGet.Val()).To(Equal([]interface{}{\n\t\t\t\t\"val2\",\n\t\t\t\t\"val\",\n\t\t\t}))\n\n\t\t\ttype setOmitEmpty struct {\n\t\t\t\tSet1 string        `redis:\"set1\"`\n\t\t\t\tSet2 int           `redis:\"set2,omitempty\"`\n\t\t\t\tSet3 time.Duration `redis:\"set3,omitempty\"`\n\t\t\t\tSet4 string        `redis:\"set4,omitempty\"`\n\t\t\t\tSet5 time.Time     `redis:\"set5,omitempty\"`\n\t\t\t\tSet6 *numberStruct `redis:\"set6,omitempty\"`\n\t\t\t\tSet7 numberStruct  `redis:\"set7,omitempty\"`\n\t\t\t}\n\n\t\t\thSet = client.HSet(ctx, \"hash3\", &setOmitEmpty{\n\t\t\t\tSet1: \"val\",\n\t\t\t})\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\t// both set1 and set7 are set\n\t\t\t// custom struct is not omitted\n\t\t\tExpect(hSet.Val()).To(Equal(int64(2)))\n\n\t\t\thGetAll := client.HGetAll(ctx, \"hash3\")\n\t\t\tExpect(hGetAll.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hGetAll.Val()).To(Equal(map[string]string{\n\t\t\t\t\"set1\": \"val\",\n\t\t\t\t\"set7\": `{\"Number\":0}`,\n\t\t\t}))\n\t\t\tvar hash3 setOmitEmpty\n\t\t\tExpect(hGetAll.Scan(&hash3)).NotTo(HaveOccurred())\n\t\t\tExpect(hash3.Set1).To(Equal(\"val\"))\n\t\t\tExpect(hash3.Set2).To(Equal(0))\n\t\t\tExpect(hash3.Set3).To(Equal(time.Duration(0)))\n\t\t\tExpect(hash3.Set4).To(Equal(\"\"))\n\t\t\tExpect(hash3.Set5).To(Equal(time.Time{}))\n\t\t\tExpect(hash3.Set6).To(BeNil())\n\t\t\tExpect(hash3.Set7).To(Equal(numberStruct{}))\n\n\t\t\tnow := time.Now()\n\t\t\thSet = client.HSet(ctx, \"hash4\", setOmitEmpty{\n\t\t\t\tSet1: \"val\",\n\t\t\t\tSet5: now,\n\t\t\t\tSet6: &numberStruct{\n\t\t\t\t\tNumber: 5,\n\t\t\t\t},\n\t\t\t\tSet7: numberStruct{\n\t\t\t\t\tNumber: 3,\n\t\t\t\t},\n\t\t\t})\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSet.Val()).To(Equal(int64(4)))\n\n\t\t\thGetAll = client.HGetAll(ctx, \"hash4\")\n\t\t\tExpect(hGetAll.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hGetAll.Val()).To(Equal(map[string]string{\n\t\t\t\t\"set1\": \"val\",\n\t\t\t\t\"set5\": now.Format(time.RFC3339Nano),\n\t\t\t\t\"set6\": `{\"Number\":5}`,\n\t\t\t\t\"set7\": `{\"Number\":3}`,\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should HSetNX\", func() {\n\t\t\thSetNX := client.HSetNX(ctx, \"hash\", \"key\", \"hello\")\n\t\t\tExpect(hSetNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSetNX.Val()).To(Equal(true))\n\n\t\t\thSetNX = client.HSetNX(ctx, \"hash\", \"key\", \"hello\")\n\t\t\tExpect(hSetNX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hSetNX.Val()).To(Equal(false))\n\n\t\t\thGet := client.HGet(ctx, \"hash\", \"key\")\n\t\t\tExpect(hGet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hGet.Val()).To(Equal(\"hello\"))\n\t\t})\n\n\t\tIt(\"should HVals\", func() {\n\t\t\terr := client.HSet(ctx, \"hash\", \"key1\", \"hello1\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.HSet(ctx, \"hash\", \"key2\", \"hello2\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.HVals(ctx, \"hash\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal([]string{\"hello1\", \"hello2\"}))\n\n\t\t\tvar slice []string\n\t\t\terr = client.HVals(ctx, \"hash\").ScanSlice(&slice)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(slice).To(Equal([]string{\"hello1\", \"hello2\"}))\n\t\t})\n\n\t\tIt(\"should HRandField\", func() {\n\t\t\terr := client.HSet(ctx, \"hash\", \"key1\", \"hello1\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.HSet(ctx, \"hash\", \"key2\", \"hello2\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv := client.HRandField(ctx, \"hash\", 1)\n\t\t\tExpect(v.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(v.Val()).To(Or(Equal([]string{\"key1\"}), Equal([]string{\"key2\"})))\n\n\t\t\tv = client.HRandField(ctx, \"hash\", 0)\n\t\t\tExpect(v.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(v.Val()).To(HaveLen(0))\n\n\t\t\tkv, err := client.HRandFieldWithValues(ctx, \"hash\", 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(kv).To(Or(\n\t\t\t\tEqual([]redis.KeyValue{{Key: \"key1\", Value: \"hello1\"}}),\n\t\t\t\tEqual([]redis.KeyValue{{Key: \"key2\", Value: \"hello2\"}}),\n\t\t\t))\n\t\t})\n\n\t\tIt(\"should HStrLen\", func() {\n\t\t\thSet := client.HSet(ctx, \"hash\", \"key\", \"hello\")\n\t\t\tExpect(hSet.Err()).NotTo(HaveOccurred())\n\n\t\t\thStrLen := client.HStrLen(ctx, \"hash\", \"key\")\n\t\t\tExpect(hStrLen.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hStrLen.Val()).To(Equal(int64(len(\"hello\"))))\n\n\t\t\tnonHStrLen := client.HStrLen(ctx, \"hash\", \"keyNon\")\n\t\t\tExpect(hStrLen.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(nonHStrLen.Val()).To(Equal(int64(0)))\n\n\t\t\thDel := client.HDel(ctx, \"hash\", \"key\")\n\t\t\tExpect(hDel.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(hDel.Val()).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should HExpire\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tres, err := client.HExpire(ctx, \"no_such_key\", 10*time.Second, \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(res).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err = client.HExpire(ctx, \"myhash\", 10*time.Second, \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, 1, -2}))\n\t\t})\n\n\t\tIt(\"should HPExpire\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tres, err := client.HPExpire(ctx, \"no_such_key\", 10*time.Second, \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(res).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err = client.HPExpire(ctx, \"myhash\", 10*time.Second, \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, 1, -2}))\n\t\t})\n\n\t\tIt(\"should HExpireAt\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tresEmpty, err := client.HExpireAt(ctx, \"no_such_key\", time.Now().Add(10*time.Second), \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err := client.HExpireAt(ctx, \"myhash\", time.Now().Add(10*time.Second), \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, 1, -2}))\n\t\t})\n\n\t\tIt(\"should HPExpireAt\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tresEmpty, err := client.HPExpireAt(ctx, \"no_such_key\", time.Now().Add(10*time.Second), \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err := client.HPExpireAt(ctx, \"myhash\", time.Now().Add(10*time.Second), \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, 1, -2}))\n\t\t})\n\n\t\tIt(\"should HPersist\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tresEmpty, err := client.HPersist(ctx, \"no_such_key\", \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err := client.HPersist(ctx, \"myhash\", \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{-1, -1, -2}))\n\n\t\t\tres, err = client.HExpire(ctx, \"myhash\", 10*time.Second, \"key1\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, -2}))\n\n\t\t\tres, err = client.HPersist(ctx, \"myhash\", \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, -1, -2}))\n\t\t})\n\n\t\tIt(\"should HExpireTime\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tresEmpty, err := client.HExpireTime(ctx, \"no_such_key\", \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err := client.HExpire(ctx, \"myhash\", 10*time.Second, \"key1\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, -2}))\n\n\t\t\tres, err = client.HExpireTime(ctx, \"myhash\", \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res[0]).To(BeNumerically(\"~\", time.Now().Add(10*time.Second).Unix(), 1))\n\t\t})\n\n\t\tIt(\"should HPExpireTime\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tresEmpty, err := client.HPExpireTime(ctx, \"no_such_key\", \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\texpireAt := time.Now().Add(10 * time.Second)\n\t\t\tres, err := client.HPExpireAt(ctx, \"myhash\", expireAt, \"key1\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, -2}))\n\n\t\t\tres, err = client.HPExpireTime(ctx, \"myhash\", \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(BeEquivalentTo([]int64{expireAt.UnixMilli(), -1, -2}))\n\t\t})\n\n\t\tIt(\"should HTTL\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tresEmpty, err := client.HTTL(ctx, \"no_such_key\", \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err := client.HExpire(ctx, \"myhash\", 10*time.Second, \"key1\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, -2}))\n\n\t\t\tres, err = client.HTTL(ctx, \"myhash\", \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{10, -1, -2}))\n\t\t})\n\n\t\tIt(\"should HPTTL\", Label(\"hash-expiration\", \"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tresEmpty, err := client.HPTTL(ctx, \"no_such_key\", \"field1\", \"field2\", \"field3\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2}))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsadd := client.HSet(ctx, \"myhash\", fmt.Sprintf(\"key%d\", i), \"hello\")\n\t\t\t\tExpect(sadd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tres, err := client.HExpire(ctx, \"myhash\", 10*time.Second, \"key1\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]int64{1, -2}))\n\n\t\t\tres, err = client.HPTTL(ctx, \"myhash\", \"key1\", \"key2\", \"key200\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t// overhead of the push notification check is about 1-2ms for 100 commands\n\t\t\tExpect(res[0]).To(BeNumerically(\"~\", 10*time.Second.Milliseconds(), 2))\n\t\t})\n\n\t\tIt(\"should HGETDEL\", Label(\"hash\", \"HGETDEL\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\n\t\t\terr := client.HSet(ctx, \"myhash\", \"f1\", \"val1\", \"f2\", \"val2\", \"f3\", \"val3\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Execute HGETDEL on fields f1 and f2.\n\t\t\tres, err := client.HGetDel(ctx, \"myhash\", \"f1\", \"f2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t// Expect the returned values for f1 and f2.\n\t\t\tExpect(res).To(Equal([]string{\"val1\", \"val2\"}))\n\n\t\t\t// Verify that f1 and f2 have been deleted, while f3 remains.\n\t\t\tremaining, err := client.HMGet(ctx, \"myhash\", \"f1\", \"f2\", \"f3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(remaining[0]).To(BeNil())\n\t\t\tExpect(remaining[1]).To(BeNil())\n\t\t\tExpect(remaining[2]).To(Equal(\"val3\"))\n\t\t})\n\n\t\tIt(\"should return nil responses for HGETDEL on non-existent key\", Label(\"hash\", \"HGETDEL\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\t\t// HGETDEL on a key that does not exist.\n\t\t\tres, err := client.HGetDel(ctx, \"nonexistent\", \"f1\", \"f2\").Result()\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(res).To(Equal([]string{\"\", \"\"}))\n\t\t})\n\n\t\t// -----------------------------\n\t\t// HGETEX with various TTL options\n\t\t// -----------------------------\n\t\tIt(\"should HGETEX with EX option\", Label(\"hash\", \"HGETEX\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\n\t\t\terr := client.HSet(ctx, \"myhash\", \"f1\", \"val1\", \"f2\", \"val2\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Call HGETEX with EX option and 60 seconds TTL.\n\t\t\topt := redis.HGetEXOptions{\n\t\t\t\tExpirationType: redis.HGetEXExpirationEX,\n\t\t\t\tExpirationVal:  60,\n\t\t\t}\n\t\t\tres, err := client.HGetEXWithArgs(ctx, \"myhash\", &opt, \"f1\", \"f2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]string{\"val1\", \"val2\"}))\n\t\t})\n\n\t\tIt(\"should HGETEX with PERSIST option\", Label(\"hash\", \"HGETEX\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\n\t\t\terr := client.HSet(ctx, \"myhash\", \"f1\", \"val1\", \"f2\", \"val2\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Call HGETEX with PERSIST (no TTL value needed).\n\t\t\topt := redis.HGetEXOptions{ExpirationType: redis.HGetEXExpirationPERSIST}\n\t\t\tres, err := client.HGetEXWithArgs(ctx, \"myhash\", &opt, \"f1\", \"f2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]string{\"val1\", \"val2\"}))\n\t\t})\n\n\t\tIt(\"should HGETEX with EXAT option\", Label(\"hash\", \"HGETEX\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\n\t\t\terr := client.HSet(ctx, \"myhash\", \"f1\", \"val1\", \"f2\", \"val2\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Set expiration at a specific Unix timestamp (60 seconds from now).\n\t\t\texpireAt := time.Now().Add(60 * time.Second).Unix()\n\t\t\topt := redis.HGetEXOptions{\n\t\t\t\tExpirationType: redis.HGetEXExpirationEXAT,\n\t\t\t\tExpirationVal:  expireAt,\n\t\t\t}\n\t\t\tres, err := client.HGetEXWithArgs(ctx, \"myhash\", &opt, \"f1\", \"f2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]string{\"val1\", \"val2\"}))\n\t\t})\n\n\t\t// -----------------------------\n\t\t// HSETEX with FNX/FXX options\n\t\t// -----------------------------\n\t\tIt(\"should HSETEX with FNX condition\", Label(\"hash\", \"HSETEX\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\n\t\t\topt := redis.HSetEXOptions{\n\t\t\t\tCondition:      redis.HSetEXFNX,\n\t\t\t\tExpirationType: redis.HSetEXExpirationEX,\n\t\t\t\tExpirationVal:  60,\n\t\t\t}\n\t\t\tres, err := client.HSetEXWithArgs(ctx, \"myhash\", &opt, \"f1\", \"val1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal(int64(1)))\n\n\t\t\topt = redis.HSetEXOptions{\n\t\t\t\tCondition:      redis.HSetEXFNX,\n\t\t\t\tExpirationType: redis.HSetEXExpirationEX,\n\t\t\t\tExpirationVal:  60,\n\t\t\t}\n\t\t\tres, err = client.HSetEXWithArgs(ctx, \"myhash\", &opt, \"f1\", \"val2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should HSETEX with FXX condition\", Label(\"hash\", \"HSETEX\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\n\t\t\terr := client.HSet(ctx, \"myhash\", \"f2\", \"val1\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\topt := redis.HSetEXOptions{\n\t\t\t\tCondition:      redis.HSetEXFXX,\n\t\t\t\tExpirationType: redis.HSetEXExpirationEX,\n\t\t\t\tExpirationVal:  60,\n\t\t\t}\n\t\t\tres, err := client.HSetEXWithArgs(ctx, \"myhash\", &opt, \"f2\", \"val2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal(int64(1)))\n\t\t\topt = redis.HSetEXOptions{\n\t\t\t\tCondition:      redis.HSetEXFXX,\n\t\t\t\tExpirationType: redis.HSetEXExpirationEX,\n\t\t\t\tExpirationVal:  60,\n\t\t\t}\n\t\t\tres, err = client.HSetEXWithArgs(ctx, \"myhash\", &opt, \"f3\", \"val3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should HSETEX with multiple field operations\", Label(\"hash\", \"HSETEX\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\n\t\t\topt := redis.HSetEXOptions{\n\t\t\t\tExpirationType: redis.HSetEXExpirationEX,\n\t\t\t\tExpirationVal:  60,\n\t\t\t}\n\t\t\tres, err := client.HSetEXWithArgs(ctx, \"myhash\", &opt, \"f1\", \"val1\", \"f2\", \"val2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal(int64(1)))\n\n\t\t\tvalues, err := client.HMGet(ctx, \"myhash\", \"f1\", \"f2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(values).To(Equal([]interface{}{\"val1\", \"val2\"}))\n\t\t})\n\t})\n\n\tDescribe(\"hyperloglog\", func() {\n\t\tIt(\"should PFMerge\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tpfAdd := client.PFAdd(ctx, \"hll1\", \"1\", \"2\", \"3\", \"4\", \"5\")\n\t\t\tExpect(pfAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tpfCount := client.PFCount(ctx, \"hll1\")\n\t\t\tExpect(pfCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pfCount.Val()).To(Equal(int64(5)))\n\n\t\t\tpfAdd = client.PFAdd(ctx, \"hll2\", \"a\", \"b\", \"c\", \"d\", \"e\")\n\t\t\tExpect(pfAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tpfMerge := client.PFMerge(ctx, \"hllMerged\", \"hll1\", \"hll2\")\n\t\t\tExpect(pfMerge.Err()).NotTo(HaveOccurred())\n\n\t\t\tpfCount = client.PFCount(ctx, \"hllMerged\")\n\t\t\tExpect(pfCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pfCount.Val()).To(Equal(int64(10)))\n\n\t\t\tpfCount = client.PFCount(ctx, \"hll1\", \"hll2\")\n\t\t\tExpect(pfCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pfCount.Val()).To(Equal(int64(10)))\n\t\t})\n\t})\n\n\tDescribe(\"lists\", func() {\n\t\tIt(\"should BLPop\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\trPush := client.RPush(ctx, \"list1\", \"a\", \"b\", \"c\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tbLPop := client.BLPop(ctx, 0, \"list1\", \"list2\")\n\t\t\tExpect(bLPop.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bLPop.Val()).To(Equal([]string{\"list1\", \"a\"}))\n\t\t})\n\n\t\tIt(\"should BLPopBlocks\", func() {\n\t\t\tstarted := make(chan bool)\n\t\t\tdone := make(chan bool)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tstarted <- true\n\t\t\t\tbLPop := client.BLPop(ctx, 0, \"list\")\n\t\t\t\tExpect(bLPop.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(bLPop.Val()).To(Equal([]string{\"list\", \"a\"}))\n\t\t\t\tdone <- true\n\t\t\t}()\n\t\t\t<-started\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tFail(\"BLPop is not blocked\")\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\t// ok\n\t\t\t}\n\n\t\t\trPush := client.RPush(ctx, \"list\", \"a\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// ok\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tFail(\"BLPop is still blocked\")\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should BLPop timeout\", func() {\n\t\t\tval, err := client.BLPop(ctx, time.Second, \"list1\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(BeNil())\n\n\t\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats.Hits).To(Equal(uint32(2)))\n\t\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t\t})\n\n\t\tIt(\"should BRPop\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\trPush := client.RPush(ctx, \"list1\", \"a\", \"b\", \"c\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tbRPop := client.BRPop(ctx, 0, \"list1\", \"list2\")\n\t\t\tExpect(bRPop.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(bRPop.Val()).To(Equal([]string{\"list1\", \"c\"}))\n\t\t})\n\n\t\tIt(\"should BRPop blocks\", func() {\n\t\t\tstarted := make(chan bool)\n\t\t\tdone := make(chan bool)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tstarted <- true\n\t\t\t\tbrpop := client.BRPop(ctx, 0, \"list\")\n\t\t\t\tExpect(brpop.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(brpop.Val()).To(Equal([]string{\"list\", \"a\"}))\n\t\t\t\tdone <- true\n\t\t\t}()\n\t\t\t<-started\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tFail(\"BRPop is not blocked\")\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\t// ok\n\t\t\t}\n\n\t\t\trPush := client.RPush(ctx, \"list\", \"a\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// ok\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tFail(\"BRPop is still blocked\")\n\t\t\t\t// ok\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should BRPopLPush\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\t_, err := client.BRPopLPush(ctx, \"list1\", \"list2\", time.Second).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\n\t\t\terr = client.RPush(ctx, \"list1\", \"a\", \"b\", \"c\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.BRPopLPush(ctx, \"list1\", \"list2\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(\"c\"))\n\t\t})\n\n\t\tIt(\"should LCS\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.MSet(ctx, \"key1\", \"ohmytext\", \"key2\", \"mynewtext\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tlcs, err := client.LCS(ctx, &redis.LCSQuery{\n\t\t\t\tKey1: \"key1\",\n\t\t\t\tKey2: \"key2\",\n\t\t\t}).Result()\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(lcs.MatchString).To(Equal(\"mytext\"))\n\n\t\t\tlcs, err = client.LCS(ctx, &redis.LCSQuery{\n\t\t\t\tKey1: \"nonexistent_key1\",\n\t\t\t\tKey2: \"key2\",\n\t\t\t}).Result()\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(lcs.MatchString).To(Equal(\"\"))\n\n\t\t\tlcs, err = client.LCS(ctx, &redis.LCSQuery{\n\t\t\t\tKey1: \"key1\",\n\t\t\t\tKey2: \"key2\",\n\t\t\t\tLen:  true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(lcs.MatchString).To(Equal(\"\"))\n\t\t\tExpect(lcs.Len).To(Equal(int64(6)))\n\n\t\t\tlcs, err = client.LCS(ctx, &redis.LCSQuery{\n\t\t\t\tKey1: \"key1\",\n\t\t\t\tKey2: \"key2\",\n\t\t\t\tIdx:  true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(lcs.MatchString).To(Equal(\"\"))\n\t\t\tExpect(lcs.Len).To(Equal(int64(6)))\n\t\t\tExpect(lcs.Matches).To(Equal([]redis.LCSMatchedPosition{\n\t\t\t\t{\n\t\t\t\t\tKey1:     redis.LCSPosition{Start: 4, End: 7},\n\t\t\t\t\tKey2:     redis.LCSPosition{Start: 5, End: 8},\n\t\t\t\t\tMatchLen: 0,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKey1:     redis.LCSPosition{Start: 2, End: 3},\n\t\t\t\t\tKey2:     redis.LCSPosition{Start: 0, End: 1},\n\t\t\t\t\tMatchLen: 0,\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\tlcs, err = client.LCS(ctx, &redis.LCSQuery{\n\t\t\t\tKey1:         \"key1\",\n\t\t\t\tKey2:         \"key2\",\n\t\t\t\tIdx:          true,\n\t\t\t\tMinMatchLen:  3,\n\t\t\t\tWithMatchLen: true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(lcs.MatchString).To(Equal(\"\"))\n\t\t\tExpect(lcs.Len).To(Equal(int64(6)))\n\t\t\tExpect(lcs.Matches).To(Equal([]redis.LCSMatchedPosition{\n\t\t\t\t{\n\t\t\t\t\tKey1:     redis.LCSPosition{Start: 4, End: 7},\n\t\t\t\t\tKey2:     redis.LCSPosition{Start: 5, End: 8},\n\t\t\t\t\tMatchLen: 4,\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\t_, err = client.Set(ctx, \"keywithstringvalue\", \"golang\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t_, err = client.LPush(ctx, \"keywithnonstringvalue\", \"somevalue\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t_, err = client.LCS(ctx, &redis.LCSQuery{\n\t\t\t\tKey1: \"keywithstringvalue\",\n\t\t\t\tKey2: \"keywithnonstringvalue\",\n\t\t\t}).Result()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(Equal(\"ERR The specified keys must contain string values\"))\n\t\t})\n\n\t\tIt(\"should LIndex\", func() {\n\t\t\tlPush := client.LPush(ctx, \"list\", \"World\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\t\t\tlPush = client.LPush(ctx, \"list\", \"Hello\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlIndex := client.LIndex(ctx, \"list\", 0)\n\t\t\tExpect(lIndex.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lIndex.Val()).To(Equal(\"Hello\"))\n\n\t\t\tlIndex = client.LIndex(ctx, \"list\", -1)\n\t\t\tExpect(lIndex.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lIndex.Val()).To(Equal(\"World\"))\n\n\t\t\tlIndex = client.LIndex(ctx, \"list\", 3)\n\t\t\tExpect(lIndex.Err()).To(Equal(redis.Nil))\n\t\t\tExpect(lIndex.Val()).To(Equal(\"\"))\n\t\t})\n\n\t\tIt(\"should LInsert\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"Hello\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"World\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlInsert := client.LInsert(ctx, \"list\", \"BEFORE\", \"World\", \"There\")\n\t\t\tExpect(lInsert.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lInsert.Val()).To(Equal(int64(3)))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"Hello\", \"There\", \"World\"}))\n\t\t})\n\n\t\tIt(\"should LMPop\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.LPush(ctx, \"list1\", \"one\", \"two\", \"three\", \"four\", \"five\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.LPush(ctx, \"list2\", \"a\", \"b\", \"c\", \"d\", \"e\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tkey, val, err := client.LMPop(ctx, \"left\", 3, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list1\"))\n\t\t\tExpect(val).To(Equal([]string{\"five\", \"four\", \"three\"}))\n\n\t\t\tkey, val, err = client.LMPop(ctx, \"right\", 3, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list1\"))\n\t\t\tExpect(val).To(Equal([]string{\"one\", \"two\"}))\n\n\t\t\tkey, val, err = client.LMPop(ctx, \"left\", 1, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list2\"))\n\t\t\tExpect(val).To(Equal([]string{\"e\"}))\n\n\t\t\tkey, val, err = client.LMPop(ctx, \"right\", 10, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list2\"))\n\t\t\tExpect(val).To(Equal([]string{\"a\", \"b\", \"c\", \"d\"}))\n\n\t\t\terr = client.LMPop(ctx, \"left\", 10, \"list1\", \"list2\").Err()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\n\t\t\terr = client.Set(ctx, \"list3\", 1024, 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.LMPop(ctx, \"left\", 10, \"list1\", \"list2\", \"list3\").Err()\n\t\t\tExpect(err.Error()).To(Equal(\"WRONGTYPE Operation against a key holding the wrong kind of value\"))\n\n\t\t\terr = client.LMPop(ctx, \"right\", 0, \"list1\", \"list2\").Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should BLMPop\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.LPush(ctx, \"list1\", \"one\", \"two\", \"three\", \"four\", \"five\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.LPush(ctx, \"list2\", \"a\", \"b\", \"c\", \"d\", \"e\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tkey, val, err := client.BLMPop(ctx, 0, \"left\", 3, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list1\"))\n\t\t\tExpect(val).To(Equal([]string{\"five\", \"four\", \"three\"}))\n\n\t\t\tkey, val, err = client.BLMPop(ctx, 0, \"right\", 3, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list1\"))\n\t\t\tExpect(val).To(Equal([]string{\"one\", \"two\"}))\n\n\t\t\tkey, val, err = client.BLMPop(ctx, 0, \"left\", 1, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list2\"))\n\t\t\tExpect(val).To(Equal([]string{\"e\"}))\n\n\t\t\tkey, val, err = client.BLMPop(ctx, 0, \"right\", 10, \"list1\", \"list2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"list2\"))\n\t\t\tExpect(val).To(Equal([]string{\"a\", \"b\", \"c\", \"d\"}))\n\t\t})\n\n\t\tIt(\"should BLMPopBlocks\", func() {\n\t\t\tstarted := make(chan bool)\n\t\t\tdone := make(chan bool)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tstarted <- true\n\t\t\t\tkey, val, err := client.BLMPop(ctx, 0, \"left\", 1, \"list_list\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(key).To(Equal(\"list_list\"))\n\t\t\t\tExpect(val).To(Equal([]string{\"a\"}))\n\t\t\t\tdone <- true\n\t\t\t}()\n\t\t\t<-started\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tFail(\"BLMPop is not blocked\")\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\t// ok\n\t\t\t}\n\n\t\t\t_, err := client.LPush(ctx, \"list_list\", \"a\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// ok\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tFail(\"BLMPop is still blocked\")\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should BLMPop timeout\", func() {\n\t\t\t_, val, err := client.BLMPop(ctx, time.Second, \"left\", 1, \"list1\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(BeNil())\n\n\t\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats.Hits).To(Equal(uint32(2)))\n\t\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t\t})\n\n\t\tIt(\"should LLen\", func() {\n\t\t\tlPush := client.LPush(ctx, \"list\", \"World\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\t\t\tlPush = client.LPush(ctx, \"list\", \"Hello\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlLen := client.LLen(ctx, \"list\")\n\t\t\tExpect(lLen.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lLen.Val()).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should LPop\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"two\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"three\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlPop := client.LPop(ctx, \"list\")\n\t\t\tExpect(lPop.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPop.Val()).To(Equal(\"one\"))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"two\", \"three\"}))\n\t\t})\n\n\t\tIt(\"should LPopCount\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"two\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"three\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"four\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlPopCount := client.LPopCount(ctx, \"list\", 2)\n\t\t\tExpect(lPopCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPopCount.Val()).To(Equal([]string{\"one\", \"two\"}))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"three\", \"four\"}))\n\t\t})\n\n\t\tIt(\"should LPos\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"a\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"b\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"c\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"b\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlPos := client.LPos(ctx, \"list\", \"b\", redis.LPosArgs{})\n\t\t\tExpect(lPos.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPos.Val()).To(Equal(int64(1)))\n\n\t\t\tlPos = client.LPos(ctx, \"list\", \"b\", redis.LPosArgs{Rank: 2})\n\t\t\tExpect(lPos.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPos.Val()).To(Equal(int64(3)))\n\n\t\t\tlPos = client.LPos(ctx, \"list\", \"b\", redis.LPosArgs{Rank: -2})\n\t\t\tExpect(lPos.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPos.Val()).To(Equal(int64(1)))\n\n\t\t\tlPos = client.LPos(ctx, \"list\", \"b\", redis.LPosArgs{Rank: 2, MaxLen: 1})\n\t\t\tExpect(lPos.Err()).To(Equal(redis.Nil))\n\n\t\t\tlPos = client.LPos(ctx, \"list\", \"z\", redis.LPosArgs{})\n\t\t\tExpect(lPos.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should LPosCount\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"a\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"b\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"c\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"b\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlPos := client.LPosCount(ctx, \"list\", \"b\", 2, redis.LPosArgs{})\n\t\t\tExpect(lPos.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPos.Val()).To(Equal([]int64{1, 3}))\n\n\t\t\tlPos = client.LPosCount(ctx, \"list\", \"b\", 2, redis.LPosArgs{Rank: 2})\n\t\t\tExpect(lPos.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPos.Val()).To(Equal([]int64{3}))\n\n\t\t\tlPos = client.LPosCount(ctx, \"list\", \"b\", 1, redis.LPosArgs{Rank: 1, MaxLen: 1})\n\t\t\tExpect(lPos.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPos.Val()).To(Equal([]int64{}))\n\n\t\t\tlPos = client.LPosCount(ctx, \"list\", \"b\", 1, redis.LPosArgs{Rank: 1, MaxLen: 0})\n\t\t\tExpect(lPos.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPos.Val()).To(Equal([]int64{1}))\n\t\t})\n\n\t\tIt(\"should LPush\", func() {\n\t\t\tlPush := client.LPush(ctx, \"list\", \"World\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\t\t\tlPush = client.LPush(ctx, \"list\", \"Hello\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"Hello\", \"World\"}))\n\t\t})\n\n\t\tIt(\"should LPushX\", func() {\n\t\t\tlPush := client.LPush(ctx, \"list\", \"World\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlPushX := client.LPushX(ctx, \"list\", \"Hello\")\n\t\t\tExpect(lPushX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPushX.Val()).To(Equal(int64(2)))\n\n\t\t\tlPush = client.LPush(ctx, \"list1\", \"three\")\n\t\t\tExpect(lPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPush.Val()).To(Equal(int64(1)))\n\n\t\t\tlPushX = client.LPushX(ctx, \"list1\", \"two\", \"one\")\n\t\t\tExpect(lPushX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPushX.Val()).To(Equal(int64(3)))\n\n\t\t\tlPushX = client.LPushX(ctx, \"list2\", \"Hello\")\n\t\t\tExpect(lPushX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lPushX.Val()).To(Equal(int64(0)))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"Hello\", \"World\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list1\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\", \"two\", \"three\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list2\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{}))\n\t\t})\n\n\t\tIt(\"should LRange\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"two\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"three\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, 0)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list\", -3, 2)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\", \"two\", \"three\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list\", -100, 100)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\", \"two\", \"three\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list\", 5, 10)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{}))\n\t\t})\n\n\t\tIt(\"should LRem\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"hello\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"hello\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"key\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"hello\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlRem := client.LRem(ctx, \"list\", -2, \"hello\")\n\t\t\tExpect(lRem.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRem.Val()).To(Equal(int64(2)))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"hello\", \"key\"}))\n\t\t})\n\n\t\tIt(\"should LSet\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"two\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"three\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlSet := client.LSet(ctx, \"list\", 0, \"four\")\n\t\t\tExpect(lSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lSet.Val()).To(Equal(\"OK\"))\n\n\t\t\tlSet = client.LSet(ctx, \"list\", -2, \"five\")\n\t\t\tExpect(lSet.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lSet.Val()).To(Equal(\"OK\"))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"four\", \"five\", \"three\"}))\n\t\t})\n\n\t\tIt(\"should LTrim\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"two\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"three\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlTrim := client.LTrim(ctx, \"list\", 1, -1)\n\t\t\tExpect(lTrim.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lTrim.Val()).To(Equal(\"OK\"))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"two\", \"three\"}))\n\t\t})\n\n\t\tIt(\"should RPop\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"two\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"three\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\trPop := client.RPop(ctx, \"list\")\n\t\t\tExpect(rPop.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPop.Val()).To(Equal(\"three\"))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\", \"two\"}))\n\t\t})\n\n\t\tIt(\"should RPopCount\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\", \"two\", \"three\", \"four\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(4)))\n\n\t\t\trPopCount := client.RPopCount(ctx, \"list\", 2)\n\t\t\tExpect(rPopCount.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPopCount.Val()).To(Equal([]string{\"four\", \"three\"}))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\", \"two\"}))\n\t\t})\n\n\t\tIt(\"should RPopLPush\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"two\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\trPush = client.RPush(ctx, \"list\", \"three\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\n\t\t\trPopLPush := client.RPopLPush(ctx, \"list\", \"list2\")\n\t\t\tExpect(rPopLPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPopLPush.Val()).To(Equal(\"three\"))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\", \"two\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list2\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"three\"}))\n\t\t})\n\n\t\tIt(\"should RPush\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"Hello\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(1)))\n\n\t\t\trPush = client.RPush(ctx, \"list\", \"World\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(2)))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"Hello\", \"World\"}))\n\t\t})\n\n\t\tIt(\"should RPushX\", func() {\n\t\t\trPush := client.RPush(ctx, \"list\", \"Hello\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(1)))\n\n\t\t\trPushX := client.RPushX(ctx, \"list\", \"World\")\n\t\t\tExpect(rPushX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPushX.Val()).To(Equal(int64(2)))\n\n\t\t\trPush = client.RPush(ctx, \"list1\", \"one\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(1)))\n\n\t\t\trPushX = client.RPushX(ctx, \"list1\", \"two\", \"three\")\n\t\t\tExpect(rPushX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPushX.Val()).To(Equal(int64(3)))\n\n\t\t\trPushX = client.RPushX(ctx, \"list2\", \"World\")\n\t\t\tExpect(rPushX.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPushX.Val()).To(Equal(int64(0)))\n\n\t\t\tlRange := client.LRange(ctx, \"list\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"Hello\", \"World\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list1\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"one\", \"two\", \"three\"}))\n\n\t\t\tlRange = client.LRange(ctx, \"list2\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{}))\n\t\t})\n\n\t\tIt(\"should LMove\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\trPush := client.RPush(ctx, \"lmove1\", \"ichi\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(1)))\n\n\t\t\trPush = client.RPush(ctx, \"lmove1\", \"ni\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(2)))\n\n\t\t\trPush = client.RPush(ctx, \"lmove1\", \"san\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(3)))\n\n\t\t\tlMove := client.LMove(ctx, \"lmove1\", \"lmove2\", \"RIGHT\", \"LEFT\")\n\t\t\tExpect(lMove.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lMove.Val()).To(Equal(\"san\"))\n\n\t\t\tlRange := client.LRange(ctx, \"lmove2\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"san\"}))\n\t\t})\n\n\t\tIt(\"should BLMove\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\trPush := client.RPush(ctx, \"blmove1\", \"ichi\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(1)))\n\n\t\t\trPush = client.RPush(ctx, \"blmove1\", \"ni\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(2)))\n\n\t\t\trPush = client.RPush(ctx, \"blmove1\", \"san\")\n\t\t\tExpect(rPush.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(rPush.Val()).To(Equal(int64(3)))\n\n\t\t\tblMove := client.BLMove(ctx, \"blmove1\", \"blmove2\", \"RIGHT\", \"LEFT\", time.Second)\n\t\t\tExpect(blMove.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(blMove.Val()).To(Equal(\"san\"))\n\n\t\t\tlRange := client.LRange(ctx, \"blmove2\", 0, -1)\n\t\t\tExpect(lRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(lRange.Val()).To(Equal([]string{\"san\"}))\n\t\t})\n\t})\n\n\tDescribe(\"sets\", func() {\n\t\tIt(\"should SAdd\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"Hello\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sAdd.Val()).To(Equal(int64(1)))\n\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"World\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sAdd.Val()).To(Equal(int64(1)))\n\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"World\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sAdd.Val()).To(Equal(int64(0)))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(ConsistOf([]string{\"Hello\", \"World\"}))\n\t\t})\n\n\t\tIt(\"should SAdd strings\", func() {\n\t\t\tset := []string{\"Hello\", \"World\", \"World\"}\n\t\t\tsAdd := client.SAdd(ctx, \"set\", set)\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sAdd.Val()).To(Equal(int64(2)))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(ConsistOf([]string{\"Hello\", \"World\"}))\n\t\t})\n\n\t\tIt(\"should SCard\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"Hello\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sAdd.Val()).To(Equal(int64(1)))\n\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"World\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sAdd.Val()).To(Equal(int64(1)))\n\n\t\t\tsCard := client.SCard(ctx, \"set\")\n\t\t\tExpect(sCard.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sCard.Val()).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should SDiff\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"a\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"d\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"e\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsDiff := client.SDiff(ctx, \"set1\", \"set2\")\n\t\t\tExpect(sDiff.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sDiff.Val()).To(ConsistOf([]string{\"a\", \"b\"}))\n\t\t})\n\n\t\tIt(\"should SDiffStore\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"a\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"d\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"e\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsDiffStore := client.SDiffStore(ctx, \"set\", \"set1\", \"set2\")\n\t\t\tExpect(sDiffStore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sDiffStore.Val()).To(Equal(int64(2)))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(ConsistOf([]string{\"a\", \"b\"}))\n\t\t})\n\n\t\tIt(\"should SInter\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"a\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"d\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"e\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsInter := client.SInter(ctx, \"set1\", \"set2\")\n\t\t\tExpect(sInter.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInter.Val()).To(Equal([]string{\"c\"}))\n\t\t})\n\n\t\tIt(\"should SInterCard\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"a\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"d\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"e\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\t// limit 0 means no limit,see https://redis.io/commands/sintercard/ for more details\n\t\t\tsInterCard := client.SInterCard(ctx, 0, \"set1\", \"set2\")\n\t\t\tExpect(sInterCard.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInterCard.Val()).To(Equal(int64(2)))\n\n\t\t\tsInterCard = client.SInterCard(ctx, 1, \"set1\", \"set2\")\n\t\t\tExpect(sInterCard.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInterCard.Val()).To(Equal(int64(1)))\n\n\t\t\tsInterCard = client.SInterCard(ctx, 3, \"set1\", \"set2\")\n\t\t\tExpect(sInterCard.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInterCard.Val()).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should SInterStore\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"a\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"d\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"e\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsInterStore := client.SInterStore(ctx, \"set\", \"set1\", \"set2\")\n\t\t\tExpect(sInterStore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInterStore.Val()).To(Equal(int64(1)))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(Equal([]string{\"c\"}))\n\t\t})\n\n\t\tIt(\"should IsMember\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"one\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsIsMember := client.SIsMember(ctx, \"set\", \"one\")\n\t\t\tExpect(sIsMember.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sIsMember.Val()).To(Equal(true))\n\n\t\t\tsIsMember = client.SIsMember(ctx, \"set\", \"two\")\n\t\t\tExpect(sIsMember.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sIsMember.Val()).To(Equal(false))\n\t\t})\n\n\t\tIt(\"should SMIsMember\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"one\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsMIsMember := client.SMIsMember(ctx, \"set\", \"one\", \"two\")\n\t\t\tExpect(sMIsMember.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMIsMember.Val()).To(Equal([]bool{true, false}))\n\t\t})\n\n\t\tIt(\"should SMembers\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"Hello\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"World\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(ConsistOf([]string{\"Hello\", \"World\"}))\n\t\t})\n\n\t\tIt(\"should SMembersMap\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"Hello\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"World\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsMembersMap := client.SMembersMap(ctx, \"set\")\n\t\t\tExpect(sMembersMap.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembersMap.Val()).To(Equal(map[string]struct{}{\"Hello\": {}, \"World\": {}}))\n\t\t})\n\n\t\tIt(\"should SMove\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"one\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"two\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"three\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsMove := client.SMove(ctx, \"set1\", \"set2\", \"two\")\n\t\t\tExpect(sMove.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMove.Val()).To(Equal(true))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set1\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(Equal([]string{\"one\"}))\n\n\t\t\tsMembers = client.SMembers(ctx, \"set2\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(ConsistOf([]string{\"three\", \"two\"}))\n\t\t})\n\n\t\tIt(\"should SPop\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"one\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"two\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"three\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsPop := client.SPop(ctx, \"set\")\n\t\t\tExpect(sPop.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sPop.Val()).NotTo(Equal(\"\"))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(HaveLen(2))\n\t\t})\n\n\t\tIt(\"should SPopN\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"one\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"two\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"three\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"four\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsPopN := client.SPopN(ctx, \"set\", 1)\n\t\t\tExpect(sPopN.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sPopN.Val()).NotTo(Equal([]string{\"\"}))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(HaveLen(3))\n\n\t\t\tsPopN = client.SPopN(ctx, \"set\", 4)\n\t\t\tExpect(sPopN.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sPopN.Val()).To(HaveLen(3))\n\n\t\t\tsMembers = client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(HaveLen(0))\n\t\t})\n\n\t\tIt(\"should SRandMember and SRandMemberN\", func() {\n\t\t\terr := client.SAdd(ctx, \"set\", \"one\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.SAdd(ctx, \"set\", \"two\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.SAdd(ctx, \"set\", \"three\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmembers, err := client.SMembers(ctx, \"set\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(HaveLen(3))\n\n\t\t\tmember, err := client.SRandMember(ctx, \"set\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(member).NotTo(Equal(\"\"))\n\n\t\t\tmembers, err = client.SRandMemberN(ctx, \"set\", 2).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(HaveLen(2))\n\t\t})\n\n\t\tIt(\"should SRem\", func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set\", \"one\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"two\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set\", \"three\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsRem := client.SRem(ctx, \"set\", \"one\")\n\t\t\tExpect(sRem.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sRem.Val()).To(Equal(int64(1)))\n\n\t\t\tsRem = client.SRem(ctx, \"set\", \"four\")\n\t\t\tExpect(sRem.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sRem.Val()).To(Equal(int64(0)))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(ConsistOf([]string{\"three\", \"two\"}))\n\t\t})\n\n\t\tIt(\"should SUnion\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"a\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"d\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"e\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsUnion := client.SUnion(ctx, \"set1\", \"set2\")\n\t\t\tExpect(sUnion.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sUnion.Val()).To(HaveLen(5))\n\t\t})\n\n\t\tIt(\"should SUnionStore\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tsAdd := client.SAdd(ctx, \"set1\", \"a\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"b\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set1\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"c\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"d\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\t\t\tsAdd = client.SAdd(ctx, \"set2\", \"e\")\n\t\t\tExpect(sAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tsUnionStore := client.SUnionStore(ctx, \"set\", \"set1\", \"set2\")\n\t\t\tExpect(sUnionStore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sUnionStore.Val()).To(Equal(int64(5)))\n\n\t\t\tsMembers := client.SMembers(ctx, \"set\")\n\t\t\tExpect(sMembers.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sMembers.Val()).To(HaveLen(5))\n\t\t})\n\t})\n\n\tDescribe(\"sorted sets\", func() {\n\t\tIt(\"should BZPopMax\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmember, err := client.BZPopMax(ctx, 0, \"zset1\", \"zset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(member).To(Equal(&redis.ZWithKey{\n\t\t\t\tZ: redis.Z{\n\t\t\t\t\tScore:  3,\n\t\t\t\t\tMember: \"three\",\n\t\t\t\t},\n\t\t\t\tKey: \"zset1\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should BZPopMax blocks\", func() {\n\t\t\tstarted := make(chan bool)\n\t\t\tdone := make(chan bool)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tstarted <- true\n\t\t\t\tbZPopMax := client.BZPopMax(ctx, 0, \"zset\")\n\t\t\t\tExpect(bZPopMax.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(bZPopMax.Val()).To(Equal(&redis.ZWithKey{\n\t\t\t\t\tZ: redis.Z{\n\t\t\t\t\t\tMember: \"a\",\n\t\t\t\t\t\tScore:  1,\n\t\t\t\t\t},\n\t\t\t\t\tKey: \"zset\",\n\t\t\t\t}))\n\t\t\t\tdone <- true\n\t\t\t}()\n\t\t\t<-started\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tFail(\"BZPopMax is not blocked\")\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\t// ok\n\t\t\t}\n\n\t\t\tzAdd := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tMember: \"a\",\n\t\t\t\tScore:  1,\n\t\t\t})\n\t\t\tExpect(zAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// ok\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tFail(\"BZPopMax is still blocked\")\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should BZPopMax timeout\", func() {\n\t\t\tval, err := client.BZPopMax(ctx, time.Second, \"zset1\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(BeNil())\n\n\t\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats.Hits).To(Equal(uint32(2)))\n\t\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t\t})\n\n\t\tIt(\"should BZPopMin\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmember, err := client.BZPopMin(ctx, 0, \"zset1\", \"zset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(member).To(Equal(&redis.ZWithKey{\n\t\t\t\tZ: redis.Z{\n\t\t\t\t\tScore:  1,\n\t\t\t\t\tMember: \"one\",\n\t\t\t\t},\n\t\t\t\tKey: \"zset1\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should BZPopMin blocks\", func() {\n\t\t\tstarted := make(chan bool)\n\t\t\tdone := make(chan bool)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tstarted <- true\n\t\t\t\tbZPopMin := client.BZPopMin(ctx, 0, \"zset\")\n\t\t\t\tExpect(bZPopMin.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(bZPopMin.Val()).To(Equal(&redis.ZWithKey{\n\t\t\t\t\tZ: redis.Z{\n\t\t\t\t\t\tMember: \"a\",\n\t\t\t\t\t\tScore:  1,\n\t\t\t\t\t},\n\t\t\t\t\tKey: \"zset\",\n\t\t\t\t}))\n\t\t\t\tdone <- true\n\t\t\t}()\n\t\t\t<-started\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tFail(\"BZPopMin is not blocked\")\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\t// ok\n\t\t\t}\n\n\t\t\tzAdd := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tMember: \"a\",\n\t\t\t\tScore:  1,\n\t\t\t})\n\t\t\tExpect(zAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// ok\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tFail(\"BZPopMin is still blocked\")\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should BZPopMin timeout\", func() {\n\t\t\tval, err := client.BZPopMin(ctx, time.Second, \"zset1\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(BeNil())\n\n\t\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats.Hits).To(Equal(uint32(2)))\n\t\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t\t})\n\n\t\tIt(\"should ZAdd\", func() {\n\t\t\tadded, err := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tadded, err = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"uno\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tadded, err = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tadded, err = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"uno\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZAdd bytes\", func() {\n\t\t\tadded, err := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: []byte(\"one\"),\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tadded, err = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: []byte(\"uno\"),\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tadded, err = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: []byte(\"two\"),\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tadded, err = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: []byte(\"two\"),\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"uno\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsGTAndLT\", func() {\n\t\t\t// Test only the GT+LT options.\n\t\t\tadded, err := client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tGT:      true,\n\t\t\t\tMembers: []redis.Z{{Score: 1, Member: \"one\"}},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\n\t\t\tadded, err = client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tGT:      true,\n\t\t\t\tMembers: []redis.Z{{Score: 2, Member: \"one\"}},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\n\t\t\tadded, err = client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tLT:      true,\n\t\t\t\tMembers: []redis.Z{{Score: 1, Member: \"one\"}},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsLT\", func() {\n\t\t\tadded, err := client.ZAddLT(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\n\t\t\tadded, err = client.ZAddLT(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\n\t\t\tadded, err = client.ZAddLT(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsGT\", func() {\n\t\t\tadded, err := client.ZAddGT(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\n\t\t\tadded, err = client.ZAddGT(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 3, Member: \"one\"}}))\n\n\t\t\tadded, err = client.ZAddGT(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 3, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsNX\", func() {\n\t\t\tadded, err := client.ZAddNX(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\n\t\t\tadded, err = client.ZAddNX(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsXX\", func() {\n\t\t\tadded, err := client.ZAddXX(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(BeEmpty())\n\n\t\t\tadded, err = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tadded, err = client.ZAddXX(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsCh\", func() {\n\t\t\tchanged, err := client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tCh: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(changed).To(Equal(int64(1)))\n\n\t\t\tchanged, err = client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tCh: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(changed).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should ZAddArgsNXCh\", func() {\n\t\t\tchanged, err := client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tNX: true,\n\t\t\t\tCh: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(changed).To(Equal(int64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\n\t\t\tchanged, err = client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tNX: true,\n\t\t\t\tCh: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 2, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(changed).To(Equal(int64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsXXCh\", func() {\n\t\t\tchanged, err := client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tXX: true,\n\t\t\t\tCh: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(changed).To(Equal(int64(0)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(BeEmpty())\n\n\t\t\tadded, err := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tchanged, err = client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tXX: true,\n\t\t\t\tCh: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 2, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(changed).To(Equal(int64(1)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsIncr\", func() {\n\t\t\tscore, err := client.ZAddArgsIncr(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(score).To(Equal(float64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\n\t\t\tscore, err = client.ZAddArgsIncr(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(score).To(Equal(float64(2)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsIncrNX\", func() {\n\t\t\tscore, err := client.ZAddArgsIncr(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tNX: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(score).To(Equal(float64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\n\t\t\tscore, err = client.ZAddArgsIncr(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tNX: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(score).To(Equal(float64(0)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZAddArgsIncrXX\", func() {\n\t\t\tscore, err := client.ZAddArgsIncr(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tXX: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(score).To(Equal(float64(0)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(BeEmpty())\n\n\t\t\tadded, err := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(1)))\n\n\t\t\tscore, err = client.ZAddArgsIncr(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tXX: true,\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(score).To(Equal(float64(2)))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"one\"}}))\n\t\t})\n\n\t\tIt(\"should ZCard\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tcard, err := client.ZCard(ctx, \"zset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(card).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should ZCount\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tcount, err := client.ZCount(ctx, \"zset\", \"-inf\", \"+inf\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(count).To(Equal(int64(3)))\n\n\t\t\tcount, err = client.ZCount(ctx, \"zset\", \"(1\", \"3\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(count).To(Equal(int64(2)))\n\n\t\t\tcount, err = client.ZLexCount(ctx, \"zset\", \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(count).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should ZIncrBy\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tn, err := client.ZIncrBy(ctx, \"zset\", 2, \"one\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(float64(3)))\n\n\t\t\tval, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]redis.Z{{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZInterStore\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset3\", redis.Z{Score: 3, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tn, err := client.ZInterStore(ctx, \"out\", &redis.ZStore{\n\t\t\t\tKeys:    []string{\"zset1\", \"zset2\"},\n\t\t\t\tWeights: []float64{2, 3},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"out\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  5,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  10,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZMPop\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tkey, elems, err := client.ZMPop(ctx, \"min\", 1, \"zset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"zset\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\t_, _, err = client.ZMPop(ctx, \"min\", 1, \"nosuchkey\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\n\t\t\terr = client.ZAdd(ctx, \"myzset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"myzset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"myzset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tkey, elems, err = client.ZMPop(ctx, \"min\", 1, \"myzset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"myzset\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\tkey, elems, err = client.ZMPop(ctx, \"max\", 10, \"myzset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"myzset\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\n\t\t\terr = client.ZAdd(ctx, \"myzset2\", redis.Z{Score: 4, Member: \"four\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"myzset2\", redis.Z{Score: 5, Member: \"five\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"myzset2\", redis.Z{Score: 6, Member: \"six\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tkey, elems, err = client.ZMPop(ctx, \"min\", 10, \"myzset\", \"myzset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"myzset2\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  4,\n\t\t\t\tMember: \"four\",\n\t\t\t}, {\n\t\t\t\tScore:  5,\n\t\t\t\tMember: \"five\",\n\t\t\t}, {\n\t\t\t\tScore:  6,\n\t\t\t\tMember: \"six\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should BZMPop\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tkey, elems, err := client.BZMPop(ctx, 0, \"min\", 1, \"zset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"zset\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\t\t\tkey, elems, err = client.BZMPop(ctx, 0, \"max\", 1, \"zset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"zset\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\t\t\tkey, elems, err = client.BZMPop(ctx, 0, \"min\", 10, \"zset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"zset\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\n\t\t\tkey, elems, err = client.BZMPop(ctx, 0, \"max\", 10, \"zset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"zset2\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\terr = client.ZAdd(ctx, \"myzset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tkey, elems, err = client.BZMPop(ctx, 0, \"min\", 10, \"myzset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"myzset\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\terr = client.ZAdd(ctx, \"myzset2\", redis.Z{Score: 4, Member: \"four\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"myzset2\", redis.Z{Score: 5, Member: \"five\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tkey, elems, err = client.BZMPop(ctx, 0, \"min\", 10, \"myzset\", \"myzset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(key).To(Equal(\"myzset2\"))\n\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\tScore:  4,\n\t\t\t\tMember: \"four\",\n\t\t\t}, {\n\t\t\t\tScore:  5,\n\t\t\t\tMember: \"five\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should BZMPopBlocks\", func() {\n\t\t\tstarted := make(chan bool)\n\t\t\tdone := make(chan bool)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tstarted <- true\n\t\t\t\tkey, elems, err := client.BZMPop(ctx, 0, \"min\", 1, \"list_list\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(key).To(Equal(\"list_list\"))\n\t\t\t\tExpect(elems).To(Equal([]redis.Z{{\n\t\t\t\t\tScore:  1,\n\t\t\t\t\tMember: \"one\",\n\t\t\t\t}}))\n\t\t\t\tdone <- true\n\t\t\t}()\n\t\t\t<-started\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tFail(\"BZMPop is not blocked\")\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\t// ok\n\t\t\t}\n\n\t\t\terr := client.ZAdd(ctx, \"list_list\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// ok\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tFail(\"BZMPop is still blocked\")\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should BZMPop timeout\", func() {\n\t\t\t_, val, err := client.BZMPop(ctx, time.Second, \"min\", 1, \"list1\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\tExpect(val).To(BeNil())\n\n\t\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats.Hits).To(Equal(uint32(2)))\n\t\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t\t})\n\n\t\tIt(\"should ZMScore\", func() {\n\t\t\tzmScore := client.ZMScore(ctx, \"zset\", \"one\", \"three\")\n\t\t\tExpect(zmScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zmScore.Val()).To(HaveLen(2))\n\t\t\tExpect(zmScore.Val()[0]).To(Equal(float64(0)))\n\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzmScore = client.ZMScore(ctx, \"zset\", \"one\", \"three\")\n\t\t\tExpect(zmScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zmScore.Val()).To(HaveLen(2))\n\t\t\tExpect(zmScore.Val()[0]).To(Equal(float64(1)))\n\n\t\t\tzmScore = client.ZMScore(ctx, \"zset\", \"four\")\n\t\t\tExpect(zmScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zmScore.Val()).To(HaveLen(1))\n\n\t\t\tzmScore = client.ZMScore(ctx, \"zset\", \"four\", \"one\")\n\t\t\tExpect(zmScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zmScore.Val()).To(HaveLen(2))\n\t\t})\n\n\t\tIt(\"should ZPopMax\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmembers, err := client.ZPopMax(ctx, \"zset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\n\t\t\t// adding back 3\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tmembers, err = client.ZPopMax(ctx, \"zset\", 2).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\n\t\t\t// adding back 2 & 3\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tmembers, err = client.ZPopMax(ctx, \"zset\", 10).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\t_, err = client.ZPopMax(ctx, \"zset\", 10, 11).Result()\n\t\t\tExpect(err).Should(MatchError(\"too many arguments\"))\n\t\t})\n\n\t\tIt(\"should ZPopMin\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmembers, err := client.ZPopMin(ctx, \"zset\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\t// adding back 1\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tmembers, err = client.ZPopMin(ctx, \"zset\", 2).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\n\t\t\t// adding back 1 & 2\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmembers, err = client.ZPopMin(ctx, \"zset\", 10).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(members).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\n\t\t\t_, err = client.ZPopMin(ctx, \"zset\", 10, 11).Result()\n\t\t\tExpect(err).Should(MatchError(\"too many arguments\"))\n\t\t})\n\n\t\tIt(\"should ZRange\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRange := client.ZRange(ctx, \"zset\", 0, -1)\n\t\t\tExpect(zRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRange.Val()).To(Equal([]string{\"one\", \"two\", \"three\"}))\n\n\t\t\tzRange = client.ZRange(ctx, \"zset\", 2, 3)\n\t\t\tExpect(zRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRange.Val()).To(Equal([]string{\"three\"}))\n\n\t\t\tzRange = client.ZRange(ctx, \"zset\", -2, -1)\n\t\t\tExpect(zRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRange.Val()).To(Equal([]string{\"two\", \"three\"}))\n\t\t})\n\n\t\tIt(\"should ZRangeWithScores\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", 2, 3).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 3, Member: \"three\"}}))\n\n\t\t\tvals, err = client.ZRangeWithScores(ctx, \"zset\", -2, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZRangeArgs\", func() {\n\t\t\tadded, err := client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t\t{Score: 2, Member: \"two\"},\n\t\t\t\t\t{Score: 3, Member: \"three\"},\n\t\t\t\t\t{Score: 4, Member: \"four\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(4)))\n\n\t\t\tzRange, err := client.ZRangeArgs(ctx, redis.ZRangeArgs{\n\t\t\t\tKey:     \"zset\",\n\t\t\t\tStart:   1,\n\t\t\t\tStop:    4,\n\t\t\t\tByScore: true,\n\t\t\t\tRev:     true,\n\t\t\t\tOffset:  1,\n\t\t\t\tCount:   2,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(zRange).To(Equal([]string{\"three\", \"two\"}))\n\n\t\t\tzRange, err = client.ZRangeArgs(ctx, redis.ZRangeArgs{\n\t\t\t\tKey:    \"zset\",\n\t\t\t\tStart:  \"-\",\n\t\t\t\tStop:   \"+\",\n\t\t\t\tByLex:  true,\n\t\t\t\tRev:    true,\n\t\t\t\tOffset: 2,\n\t\t\t\tCount:  2,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(zRange).To(Equal([]string{\"two\", \"one\"}))\n\n\t\t\tzRange, err = client.ZRangeArgs(ctx, redis.ZRangeArgs{\n\t\t\t\tKey:     \"zset\",\n\t\t\t\tStart:   \"(1\",\n\t\t\t\tStop:    \"(4\",\n\t\t\t\tByScore: true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(zRange).To(Equal([]string{\"two\", \"three\"}))\n\n\t\t\t// withScores.\n\t\t\tzSlice, err := client.ZRangeArgsWithScores(ctx, redis.ZRangeArgs{\n\t\t\t\tKey:     \"zset\",\n\t\t\t\tStart:   1,\n\t\t\t\tStop:    4,\n\t\t\t\tByScore: true,\n\t\t\t\tRev:     true,\n\t\t\t\tOffset:  1,\n\t\t\t\tCount:   2,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(zSlice).To(Equal([]redis.Z{\n\t\t\t\t{Score: 3, Member: \"three\"},\n\t\t\t\t{Score: 2, Member: \"two\"},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should ZRangeByScore\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRangeByScore := client.ZRangeByScore(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"-inf\",\n\t\t\t\tMax: \"+inf\",\n\t\t\t})\n\t\t\tExpect(zRangeByScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByScore.Val()).To(Equal([]string{\"one\", \"two\", \"three\"}))\n\n\t\t\tzRangeByScore = client.ZRangeByScore(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"1\",\n\t\t\t\tMax: \"2\",\n\t\t\t})\n\t\t\tExpect(zRangeByScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByScore.Val()).To(Equal([]string{\"one\", \"two\"}))\n\n\t\t\tzRangeByScore = client.ZRangeByScore(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"(1\",\n\t\t\t\tMax: \"2\",\n\t\t\t})\n\t\t\tExpect(zRangeByScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByScore.Val()).To(Equal([]string{\"two\"}))\n\n\t\t\tzRangeByScore = client.ZRangeByScore(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"(1\",\n\t\t\t\tMax: \"(2\",\n\t\t\t})\n\t\t\tExpect(zRangeByScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByScore.Val()).To(Equal([]string{}))\n\t\t})\n\n\t\tIt(\"should ZRangeByLex\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  0,\n\t\t\t\tMember: \"a\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  0,\n\t\t\t\tMember: \"b\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{\n\t\t\t\tScore:  0,\n\t\t\t\tMember: \"c\",\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRangeByLex := client.ZRangeByLex(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"-\",\n\t\t\t\tMax: \"+\",\n\t\t\t})\n\t\t\tExpect(zRangeByLex.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByLex.Val()).To(Equal([]string{\"a\", \"b\", \"c\"}))\n\n\t\t\tzRangeByLex = client.ZRangeByLex(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"[a\",\n\t\t\t\tMax: \"[b\",\n\t\t\t})\n\t\t\tExpect(zRangeByLex.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByLex.Val()).To(Equal([]string{\"a\", \"b\"}))\n\n\t\t\tzRangeByLex = client.ZRangeByLex(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"(a\",\n\t\t\t\tMax: \"[b\",\n\t\t\t})\n\t\t\tExpect(zRangeByLex.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByLex.Val()).To(Equal([]string{\"b\"}))\n\n\t\t\tzRangeByLex = client.ZRangeByLex(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"(a\",\n\t\t\t\tMax: \"(b\",\n\t\t\t})\n\t\t\tExpect(zRangeByLex.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRangeByLex.Val()).To(Equal([]string{}))\n\t\t})\n\n\t\tIt(\"should ZRangeByScoreWithScoresMap\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.ZRangeByScoreWithScores(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"-inf\",\n\t\t\t\tMax: \"+inf\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\n\t\t\tvals, err = client.ZRangeByScoreWithScores(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"1\",\n\t\t\t\tMax: \"2\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\n\t\t\tvals, err = client.ZRangeByScoreWithScores(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"(1\",\n\t\t\t\tMax: \"2\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"two\"}}))\n\n\t\t\tvals, err = client.ZRangeByScoreWithScores(ctx, \"zset\", &redis.ZRangeBy{\n\t\t\t\tMin: \"(1\",\n\t\t\t\tMax: \"(2\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{}))\n\t\t})\n\n\t\tIt(\"should ZRangeStore\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tadded, err := client.ZAddArgs(ctx, \"zset\", redis.ZAddArgs{\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t\t{Score: 2, Member: \"two\"},\n\t\t\t\t\t{Score: 3, Member: \"three\"},\n\t\t\t\t\t{Score: 4, Member: \"four\"},\n\t\t\t\t},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(added).To(Equal(int64(4)))\n\n\t\t\trangeStore, err := client.ZRangeStore(ctx, \"new-zset\", redis.ZRangeArgs{\n\t\t\t\tKey:     \"zset\",\n\t\t\t\tStart:   1,\n\t\t\t\tStop:    4,\n\t\t\t\tByScore: true,\n\t\t\t\tRev:     true,\n\t\t\t\tOffset:  1,\n\t\t\t\tCount:   2,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(rangeStore).To(Equal(int64(2)))\n\n\t\t\tzRange, err := client.ZRange(ctx, \"new-zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(zRange).To(Equal([]string{\"two\", \"three\"}))\n\t\t})\n\n\t\tIt(\"should ZRank\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRank := client.ZRank(ctx, \"zset\", \"three\")\n\t\t\tExpect(zRank.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRank.Val()).To(Equal(int64(2)))\n\n\t\t\tzRank = client.ZRank(ctx, \"zset\", \"four\")\n\t\t\tExpect(zRank.Err()).To(Equal(redis.Nil))\n\t\t\tExpect(zRank.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should ZRankWithScore\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRankWithScore := client.ZRankWithScore(ctx, \"zset\", \"one\")\n\t\t\tExpect(zRankWithScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRankWithScore.Result()).To(Equal(redis.RankScore{Rank: 0, Score: 1}))\n\n\t\t\tzRankWithScore = client.ZRankWithScore(ctx, \"zset\", \"two\")\n\t\t\tExpect(zRankWithScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRankWithScore.Result()).To(Equal(redis.RankScore{Rank: 1, Score: 2}))\n\n\t\t\tzRankWithScore = client.ZRankWithScore(ctx, \"zset\", \"three\")\n\t\t\tExpect(zRankWithScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRankWithScore.Result()).To(Equal(redis.RankScore{Rank: 2, Score: 3}))\n\n\t\t\tzRankWithScore = client.ZRankWithScore(ctx, \"zset\", \"four\")\n\t\t\tExpect(zRankWithScore.Err()).To(HaveOccurred())\n\t\t\tExpect(zRankWithScore.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should ZRem\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRem := client.ZRem(ctx, \"zset\", \"two\")\n\t\t\tExpect(zRem.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRem.Val()).To(Equal(int64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZRemRangeByRank\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRemRangeByRank := client.ZRemRangeByRank(ctx, \"zset\", 0, 1)\n\t\t\tExpect(zRemRangeByRank.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRemRangeByRank.Val()).To(Equal(int64(2)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZRemRangeByScore\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRemRangeByScore := client.ZRemRangeByScore(ctx, \"zset\", \"-inf\", \"(2\")\n\t\t\tExpect(zRemRangeByScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRemRangeByScore.Val()).To(Equal(int64(1)))\n\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZRemRangeByLex\", func() {\n\t\t\tzz := []redis.Z{\n\t\t\t\t{Score: 0, Member: \"aaaa\"},\n\t\t\t\t{Score: 0, Member: \"b\"},\n\t\t\t\t{Score: 0, Member: \"c\"},\n\t\t\t\t{Score: 0, Member: \"d\"},\n\t\t\t\t{Score: 0, Member: \"e\"},\n\t\t\t\t{Score: 0, Member: \"foo\"},\n\t\t\t\t{Score: 0, Member: \"zap\"},\n\t\t\t\t{Score: 0, Member: \"zip\"},\n\t\t\t\t{Score: 0, Member: \"ALPHA\"},\n\t\t\t\t{Score: 0, Member: \"alpha\"},\n\t\t\t}\n\t\t\tfor _, z := range zz {\n\t\t\t\terr := client.ZAdd(ctx, \"zset\", z).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tn, err := client.ZRemRangeByLex(ctx, \"zset\", \"[alpha\", \"[omega\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(6)))\n\n\t\t\tvals, err := client.ZRange(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]string{\"ALPHA\", \"aaaa\", \"zap\", \"zip\"}))\n\t\t})\n\n\t\tIt(\"should ZRevRange\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRevRange := client.ZRevRange(ctx, \"zset\", 0, -1)\n\t\t\tExpect(zRevRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRevRange.Val()).To(Equal([]string{\"three\", \"two\", \"one\"}))\n\n\t\t\tzRevRange = client.ZRevRange(ctx, \"zset\", 2, 3)\n\t\t\tExpect(zRevRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRevRange.Val()).To(Equal([]string{\"one\"}))\n\n\t\t\tzRevRange = client.ZRevRange(ctx, \"zset\", -2, -1)\n\t\t\tExpect(zRevRange.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRevRange.Val()).To(Equal([]string{\"two\", \"one\"}))\n\t\t})\n\n\t\tIt(\"should ZRevRangeWithScoresMap\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.ZRevRangeWithScores(ctx, \"zset\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\tval, err = client.ZRevRangeWithScores(ctx, \"zset\", 2, 3).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]redis.Z{{Score: 1, Member: \"one\"}}))\n\n\t\t\tval, err = client.ZRevRangeWithScores(ctx, \"zset\", -2, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]redis.Z{{\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZRevRangeByScore\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.ZRevRangeByScore(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"+inf\", Min: \"-inf\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]string{\"three\", \"two\", \"one\"}))\n\n\t\t\tvals, err = client.ZRevRangeByScore(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"2\", Min: \"(1\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]string{\"two\"}))\n\n\t\t\tvals, err = client.ZRevRangeByScore(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"(2\", Min: \"(1\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]string{}))\n\t\t})\n\n\t\tIt(\"should ZRevRangeByLex\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 0, Member: \"a\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 0, Member: \"b\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 0, Member: \"c\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.ZRevRangeByLex(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"+\", Min: \"-\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]string{\"c\", \"b\", \"a\"}))\n\n\t\t\tvals, err = client.ZRevRangeByLex(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"[b\", Min: \"(a\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]string{\"b\"}))\n\n\t\t\tvals, err = client.ZRevRangeByLex(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"(b\", Min: \"(a\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]string{}))\n\t\t})\n\n\t\tIt(\"should ZRevRangeByScoreWithScores\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.ZRevRangeByScoreWithScores(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"+inf\", Min: \"-inf\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZRevRangeByScoreWithScoresMap\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.ZRevRangeByScoreWithScores(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"+inf\", Min: \"-inf\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  2,\n\t\t\t\tMember: \"two\",\n\t\t\t}, {\n\t\t\t\tScore:  1,\n\t\t\t\tMember: \"one\",\n\t\t\t}}))\n\n\t\t\tvals, err = client.ZRevRangeByScoreWithScores(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"2\", Min: \"(1\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{Score: 2, Member: \"two\"}}))\n\n\t\t\tvals, err = client.ZRevRangeByScoreWithScores(\n\t\t\t\tctx, \"zset\", &redis.ZRangeBy{Max: \"(2\", Min: \"(1\"}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{}))\n\t\t})\n\n\t\tIt(\"should ZRevRank\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRevRank := client.ZRevRank(ctx, \"zset\", \"one\")\n\t\t\tExpect(zRevRank.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRevRank.Val()).To(Equal(int64(2)))\n\n\t\t\tzRevRank = client.ZRevRank(ctx, \"zset\", \"four\")\n\t\t\tExpect(zRevRank.Err()).To(Equal(redis.Nil))\n\t\t\tExpect(zRevRank.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should ZRevRankWithScore\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tzRevRankWithScore := client.ZRevRankWithScore(ctx, \"zset\", \"one\")\n\t\t\tExpect(zRevRankWithScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRevRankWithScore.Result()).To(Equal(redis.RankScore{Rank: 2, Score: 1}))\n\n\t\t\tzRevRankWithScore = client.ZRevRankWithScore(ctx, \"zset\", \"two\")\n\t\t\tExpect(zRevRankWithScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRevRankWithScore.Result()).To(Equal(redis.RankScore{Rank: 1, Score: 2}))\n\n\t\t\tzRevRankWithScore = client.ZRevRankWithScore(ctx, \"zset\", \"three\")\n\t\t\tExpect(zRevRankWithScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zRevRankWithScore.Result()).To(Equal(redis.RankScore{Rank: 0, Score: 3}))\n\n\t\t\tzRevRankWithScore = client.ZRevRankWithScore(ctx, \"zset\", \"four\")\n\t\t\tExpect(zRevRankWithScore.Err()).To(HaveOccurred())\n\t\t\tExpect(zRevRankWithScore.Err()).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should ZScore\", func() {\n\t\t\tzAdd := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1.001, Member: \"one\"})\n\t\t\tExpect(zAdd.Err()).NotTo(HaveOccurred())\n\n\t\t\tzScore := client.ZScore(ctx, \"zset\", \"one\")\n\t\t\tExpect(zScore.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(zScore.Val()).To(Equal(1.001))\n\t\t})\n\n\t\tIt(\"should ZUnion\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAddArgs(ctx, \"zset1\", redis.ZAddArgs{\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t\t{Score: 2, Member: \"two\"},\n\t\t\t\t},\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ZAddArgs(ctx, \"zset2\", redis.ZAddArgs{\n\t\t\t\tMembers: []redis.Z{\n\t\t\t\t\t{Score: 1, Member: \"one\"},\n\t\t\t\t\t{Score: 2, Member: \"two\"},\n\t\t\t\t\t{Score: 3, Member: \"three\"},\n\t\t\t\t},\n\t\t\t}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tunion, err := client.ZUnion(ctx, redis.ZStore{\n\t\t\t\tKeys:      []string{\"zset1\", \"zset2\"},\n\t\t\t\tWeights:   []float64{2, 3},\n\t\t\t\tAggregate: \"sum\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(union).To(Equal([]string{\"one\", \"three\", \"two\"}))\n\n\t\t\tunionScores, err := client.ZUnionWithScores(ctx, redis.ZStore{\n\t\t\t\tKeys:      []string{\"zset1\", \"zset2\"},\n\t\t\t\tWeights:   []float64{2, 3},\n\t\t\t\tAggregate: \"sum\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(unionScores).To(Equal([]redis.Z{\n\t\t\t\t{Score: 5, Member: \"one\"},\n\t\t\t\t{Score: 9, Member: \"three\"},\n\t\t\t\t{Score: 10, Member: \"two\"},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should ZUnionStore\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tn, err := client.ZUnionStore(ctx, \"out\", &redis.ZStore{\n\t\t\t\tKeys:    []string{\"zset1\", \"zset2\"},\n\t\t\t\tWeights: []float64{2, 3},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(3)))\n\n\t\t\tval, err := client.ZRangeWithScores(ctx, \"out\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]redis.Z{{\n\t\t\t\tScore:  5,\n\t\t\t\tMember: \"one\",\n\t\t\t}, {\n\t\t\t\tScore:  9,\n\t\t\t\tMember: \"three\",\n\t\t\t}, {\n\t\t\t\tScore:  10,\n\t\t\t\tMember: \"two\",\n\t\t\t}}))\n\t\t})\n\n\t\tIt(\"should ZRandMember\", func() {\n\t\t\terr := client.ZAdd(ctx, \"zset\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv := client.ZRandMember(ctx, \"zset\", 1)\n\t\t\tExpect(v.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(v.Val()).To(Or(Equal([]string{\"one\"}), Equal([]string{\"two\"})))\n\n\t\t\tv = client.ZRandMember(ctx, \"zset\", 0)\n\t\t\tExpect(v.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(v.Val()).To(HaveLen(0))\n\n\t\t\tkv, err := client.ZRandMemberWithScores(ctx, \"zset\", 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(kv).To(Or(\n\t\t\t\tEqual([]redis.Z{{Member: \"one\", Score: 1}}),\n\t\t\t\tEqual([]redis.Z{{Member: \"two\", Score: 2}}),\n\t\t\t))\n\t\t})\n\n\t\tIt(\"should ZDiff\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.ZDiff(ctx, \"zset1\", \"zset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal([]string{\"two\", \"three\"}))\n\t\t})\n\n\t\tIt(\"should ZDiffWithScores\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.ZDiffWithScores(ctx, \"zset1\", \"zset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal([]redis.Z{\n\t\t\t\t{\n\t\t\t\t\tMember: \"two\",\n\t\t\t\t\tScore:  2,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMember: \"three\",\n\t\t\t\t\tScore:  3,\n\t\t\t\t},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should ZInter\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.ZInter(ctx, &redis.ZStore{\n\t\t\t\tKeys: []string{\"zset1\", \"zset2\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal([]string{\"one\", \"two\"}))\n\t\t})\n\n\t\tIt(\"should ZInterCard\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// limit 0 means no limit\n\t\t\tsInterCard := client.ZInterCard(ctx, 0, \"zset1\", \"zset2\")\n\t\t\tExpect(sInterCard.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInterCard.Val()).To(Equal(int64(2)))\n\n\t\t\tsInterCard = client.ZInterCard(ctx, 1, \"zset1\", \"zset2\")\n\t\t\tExpect(sInterCard.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInterCard.Val()).To(Equal(int64(1)))\n\n\t\t\tsInterCard = client.ZInterCard(ctx, 3, \"zset1\", \"zset2\")\n\t\t\tExpect(sInterCard.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(sInterCard.Val()).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should ZInterWithScores\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.ZInterWithScores(ctx, &redis.ZStore{\n\t\t\t\tKeys:      []string{\"zset1\", \"zset2\"},\n\t\t\t\tWeights:   []float64{2, 3},\n\t\t\t\tAggregate: \"Max\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal([]redis.Z{\n\t\t\t\t{\n\t\t\t\t\tMember: \"one\",\n\t\t\t\t\tScore:  3,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMember: \"two\",\n\t\t\t\t\tScore:  6,\n\t\t\t\t},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should ZDiffStore\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.ZAdd(ctx, \"zset1\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset1\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 1, Member: \"one\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 2, Member: \"two\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.ZAdd(ctx, \"zset2\", redis.Z{Score: 3, Member: \"three\"}).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tv, err := client.ZDiffStore(ctx, \"out1\", \"zset1\", \"zset2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(int64(0)))\n\t\t\tv, err = client.ZDiffStore(ctx, \"out1\", \"zset2\", \"zset1\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(int64(1)))\n\t\t\tvals, err := client.ZRangeWithScores(ctx, \"out1\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.Z{{\n\t\t\t\tScore:  3,\n\t\t\t\tMember: \"three\",\n\t\t\t}}))\n\t\t})\n\t})\n\n\tDescribe(\"streams\", func() {\n\t\tBeforeEach(func() {\n\t\t\tid, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream: \"stream\",\n\t\t\t\tID:     \"1-0\",\n\t\t\t\tValues: map[string]interface{}{\"uno\": \"un\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id).To(Equal(\"1-0\"))\n\n\t\t\t// Values supports []interface{}.\n\t\t\tid, err = client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream: \"stream\",\n\t\t\t\tID:     \"2-0\",\n\t\t\t\tValues: []interface{}{\"dos\", \"deux\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id).To(Equal(\"2-0\"))\n\n\t\t\t// Value supports []string.\n\t\t\tid, err = client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream: \"stream\",\n\t\t\t\tID:     \"3-0\",\n\t\t\t\tValues: []string{\"tres\", \"troix\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id).To(Equal(\"3-0\"))\n\t\t})\n\n\t\tIt(\"should XTrimMaxLen\", func() {\n\t\t\tn, err := client.XTrimMaxLen(ctx, \"stream\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should XTrimMaxLenApprox\", func() {\n\t\t\tn, err := client.XTrimMaxLenApprox(ctx, \"stream\", 0, 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should XTrimMinID\", func() {\n\t\t\tn, err := client.XTrimMinID(ctx, \"stream\", \"4-0\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should XTrimMinIDApprox\", func() {\n\t\t\tn, err := client.XTrimMinIDApprox(ctx, \"stream\", \"4-0\", 0).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should XTrimMaxLenMode\", func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"doesn't work with older redis stack images\")\n\t\t\tn, err := client.XTrimMaxLenMode(ctx, \"stream\", 0, \"KEEPREF\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(BeNumerically(\">=\", 0))\n\t\t})\n\n\t\tIt(\"should XTrimMaxLenApproxMode\", func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"doesn't work with older redis stack images\")\n\t\t\tn, err := client.XTrimMaxLenApproxMode(ctx, \"stream\", 0, 0, \"KEEPREF\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(BeNumerically(\">=\", 0))\n\t\t})\n\n\t\tIt(\"should XTrimMinIDMode\", func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"doesn't work with older redis stack images\")\n\t\t\tn, err := client.XTrimMinIDMode(ctx, \"stream\", \"4-0\", \"KEEPREF\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(BeNumerically(\">=\", 0))\n\t\t})\n\n\t\tIt(\"should XTrimMinIDApproxMode\", func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"doesn't work with older redis stack images\")\n\t\t\tn, err := client.XTrimMinIDApproxMode(ctx, \"stream\", \"4-0\", 0, \"KEEPREF\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(BeNumerically(\">=\", 0))\n\t\t})\n\n\t\tIt(\"should XAdd\", func() {\n\t\t\tid, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream: \"stream\",\n\t\t\t\tValues: map[string]interface{}{\"quatro\": \"quatre\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.XRange(ctx, \"stream\", \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t{ID: id, Values: map[string]interface{}{\"quatro\": \"quatre\"}},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XAdd with MaxLen\", func() {\n\t\t\tid, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream: \"stream\",\n\t\t\t\tMaxLen: 1,\n\t\t\t\tValues: map[string]interface{}{\"quatro\": \"quatre\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvals, err := client.XRange(ctx, \"stream\", \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: id, Values: map[string]interface{}{\"quatro\": \"quatre\"}},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XAdd with MinID\", func() {\n\t\t\tid, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream: \"stream\",\n\t\t\t\tMinID:  \"5-0\",\n\t\t\t\tID:     \"4-0\",\n\t\t\t\tValues: map[string]interface{}{\"quatro\": \"quatre\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id).To(Equal(\"4-0\"))\n\n\t\t\tvals, err := client.XRange(ctx, \"stream\", \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(HaveLen(0))\n\t\t})\n\n\t\tIt(\"should XAdd with IDMP (idempotent production)\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"IDMP requires Redis 8.6+\")\n\t\t\tstreamName := \"idmp-stream\"\n\t\t\tdefer client.Del(ctx, streamName)\n\n\t\t\t// First add with IDMP\n\t\t\tid1, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream:       streamName,\n\t\t\t\tProducerID:   \"producer1\",\n\t\t\t\tIdempotentID: \"msg1\",\n\t\t\t\tValues:       map[string]interface{}{\"field\": \"value1\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id1).NotTo(BeEmpty())\n\n\t\t\t// Add the same message again - should return the same ID (duplicate detection)\n\t\t\tid2, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream:       streamName,\n\t\t\t\tProducerID:   \"producer1\",\n\t\t\t\tIdempotentID: \"msg1\",\n\t\t\t\tValues:       map[string]interface{}{\"field\": \"value2\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id2).To(Equal(id1)) // Should return the same entry ID\n\n\t\t\t// Verify only one message was added\n\t\t\tvals, err := client.XRange(ctx, streamName, \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(HaveLen(1))\n\t\t\tExpect(vals[0].ID).To(Equal(id1))\n\n\t\t\t// Add a different message with different idempotent ID\n\t\t\tid3, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream:       streamName,\n\t\t\t\tProducerID:   \"producer1\",\n\t\t\t\tIdempotentID: \"msg2\",\n\t\t\t\tValues:       map[string]interface{}{\"field\": \"value3\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id3).NotTo(Equal(id1)) // Should be a different entry ID\n\n\t\t\t// Verify two messages now exist\n\t\t\tvals, err = client.XRange(ctx, streamName, \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(HaveLen(2))\n\n\t\t\t// Verify XINFO STREAM shows idempotent stats\n\t\t\tinfo, err := client.XInfoStream(ctx, streamName).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(info.PIDsTracked).To(Equal(int64(1)))\n\t\t\tExpect(info.IIDsTracked).To(BeNumerically(\">\", 0))\n\t\t\tExpect(info.IIDsAdded).To(Equal(int64(2)))\n\t\t\tExpect(info.IIDsDuplicates).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should XAdd with IDMPAUTO (auto-generated idempotent ID)\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"IDMPAUTO requires Redis 8.6+\")\n\t\t\tstreamName := \"idmpauto-stream\"\n\t\t\tdefer client.Del(ctx, streamName)\n\n\t\t\tid1, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream:         streamName,\n\t\t\t\tProducerID:     \"producer1\",\n\t\t\t\tIdempotentAuto: true,\n\t\t\t\tValues:         map[string]interface{}{\"field\": \"value1\", \"order\": \"123\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id1).NotTo(BeEmpty())\n\n\t\t\t// Add the same message again - should return the same ID (duplicate detection)\n\t\t\t// Redis will calculate the same idempotent ID based on content\n\t\t\tid2, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream:         streamName,\n\t\t\t\tProducerID:     \"producer1\",\n\t\t\t\tIdempotentAuto: true,\n\t\t\t\tValues:         map[string]interface{}{\"field\": \"value1\", \"order\": \"123\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id2).To(Equal(id1)) // Should return the same entry ID\n\n\t\t\t// Verify only one message was added\n\t\t\tvals, err := client.XRange(ctx, streamName, \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(HaveLen(1))\n\n\t\t\t// Add a different message - should create a new entry\n\t\t\tid3, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream:         streamName,\n\t\t\t\tProducerID:     \"producer1\",\n\t\t\t\tIdempotentAuto: true,\n\t\t\t\tValues:         map[string]interface{}{\"field\": \"value2\", \"order\": \"456\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(id3).NotTo(Equal(id1))\n\n\t\t\t// Verify two messages now exist\n\t\t\tvals, err = client.XRange(ctx, streamName, \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(HaveLen(2))\n\n\t\t\t// Verify XINFO STREAM shows idempotent stats\n\t\t\tinfo, err := client.XInfoStream(ctx, streamName).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(info.IIDsAdded).To(Equal(int64(2)))\n\t\t\tExpect(info.IIDsDuplicates).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should XCfgSet configure idempotent production settings\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"XCFGSET requires Redis 8.6+\")\n\t\t\tstreamName := \"xcfgset-stream\"\n\t\t\tdefer client.Del(ctx, streamName)\n\n\t\t\t_, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\tStream: streamName,\n\t\t\t\tValues: map[string]interface{}{\"field\": \"value\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Configure IDMP settings\n\t\t\tstatus, err := client.XCfgSet(ctx, &redis.XCfgSetArgs{\n\t\t\t\tStream:   streamName,\n\t\t\t\tDuration: 200, // 200 seconds\n\t\t\t\tMaxSize:  500, // 500 idempotent IDs\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(status).To(Equal(\"OK\"))\n\n\t\t\t// Verify the settings were applied\n\t\t\tinfo, err := client.XInfoStream(ctx, streamName).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(info.IDMPDuration).To(Equal(int64(200)))\n\t\t\tExpect(info.IDMPMaxSize).To(Equal(int64(500)))\n\n\t\t\tstatus, err = client.XCfgSet(ctx, &redis.XCfgSetArgs{\n\t\t\t\tStream:   streamName,\n\t\t\t\tDuration: 300,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(status).To(Equal(\"OK\"))\n\n\t\t\tinfo, err = client.XInfoStream(ctx, streamName).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(info.IDMPDuration).To(Equal(int64(300)))\n\n\t\t\tstatus, err = client.XCfgSet(ctx, &redis.XCfgSetArgs{\n\t\t\t\tStream:  streamName,\n\t\t\t\tMaxSize: 1000,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(status).To(Equal(\"OK\"))\n\n\t\t\tinfo, err = client.XInfoStream(ctx, streamName).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(info.IDMPMaxSize).To(Equal(int64(1000)))\n\t\t})\n\n\t\tIt(\"should XDel\", func() {\n\t\t\tn, err := client.XDel(ctx, \"stream\", \"1-0\", \"2-0\", \"3-0\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should XAckDel\", func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"doesn't work with older redis stack images\")\n\t\t\t// First, create a consumer group\n\t\t\terr := client.XGroupCreate(ctx, \"stream\", \"testgroup\", \"0\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Read messages to create pending entries\n\t\t\t_, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\tGroup:    \"testgroup\",\n\t\t\t\tConsumer: \"testconsumer\",\n\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Test XAckDel with KEEPREF mode\n\t\t\tn, err := client.XAckDel(ctx, \"stream\", \"testgroup\", \"KEEPREF\", \"1-0\", \"2-0\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(HaveLen(2))\n\n\t\t\t// Clean up\n\t\t\tclient.XGroupDestroy(ctx, \"stream\", \"testgroup\")\n\t\t})\n\n\t\tIt(\"should XDelEx\", func() {\n\t\t\tSkipBeforeRedisVersion(8.2, \"doesn't work with older redis stack images\")\n\t\t\t// Test XDelEx with KEEPREF mode\n\t\t\tn, err := client.XDelEx(ctx, \"stream\", \"KEEPREF\", \"1-0\", \"2-0\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(HaveLen(2))\n\t\t})\n\n\t\tIt(\"should XLen\", func() {\n\t\t\tn, err := client.XLen(ctx, \"stream\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(3)))\n\t\t})\n\n\t\tIt(\"should XRange\", func() {\n\t\t\tmsgs, err := client.XRange(ctx, \"stream\", \"-\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t}))\n\n\t\t\tmsgs, err = client.XRange(ctx, \"stream\", \"2\", \"+\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t}))\n\n\t\t\tmsgs, err = client.XRange(ctx, \"stream\", \"-\", \"2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XRangeN\", func() {\n\t\t\tmsgs, err := client.XRangeN(ctx, \"stream\", \"-\", \"+\", 2).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t}))\n\n\t\t\tmsgs, err = client.XRangeN(ctx, \"stream\", \"2\", \"+\", 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t}))\n\n\t\t\tmsgs, err = client.XRangeN(ctx, \"stream\", \"-\", \"2\", 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XRevRange\", func() {\n\t\t\tmsgs, err := client.XRevRange(ctx, \"stream\", \"+\", \"-\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t}))\n\n\t\t\tmsgs, err = client.XRevRange(ctx, \"stream\", \"+\", \"2\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XRevRangeN\", func() {\n\t\t\tmsgs, err := client.XRevRangeN(ctx, \"stream\", \"+\", \"-\", 2).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t}))\n\n\t\t\tmsgs, err = client.XRevRangeN(ctx, \"stream\", \"+\", \"2\", 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msgs).To(Equal([]redis.XMessage{\n\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XRead\", func() {\n\t\t\tres, err := client.XReadStreams(ctx, \"stream\", \"0\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t{\n\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\t_, err = client.XReadStreams(ctx, \"stream\", \"3\").Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should XRead\", func() {\n\t\t\tres, err := client.XRead(ctx, &redis.XReadArgs{\n\t\t\t\tStreams: []string{\"stream\", \"0\"},\n\t\t\t\tCount:   2,\n\t\t\t\tBlock:   100 * time.Millisecond,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t{\n\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\t_, err = client.XRead(ctx, &redis.XReadArgs{\n\t\t\t\tStreams: []string{\"stream\", \"3\"},\n\t\t\t\tCount:   1,\n\t\t\t\tBlock:   100 * time.Millisecond,\n\t\t\t}).Result()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"should XRead LastEntry\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tres, err := client.XRead(ctx, &redis.XReadArgs{\n\t\t\t\tStreams: []string{\"stream\"},\n\t\t\t\tCount:   2, // we expect 1 message\n\t\t\t\tID:      \"+\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t{\n\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XRead LastEntry from two streams\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tres, err := client.XRead(ctx, &redis.XReadArgs{\n\t\t\t\tStreams: []string{\"stream\", \"stream\"},\n\t\t\t\tID:      \"+\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t{\n\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should XRead LastEntry blocks\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tstart := time.Now()\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\tid, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\t\tStream: \"empty\",\n\t\t\t\t\tID:     \"4-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"quatro\": \"quatre\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(id).To(Equal(\"4-0\"))\n\t\t\t}()\n\n\t\t\tres, err := client.XRead(ctx, &redis.XReadArgs{\n\t\t\t\tStreams: []string{\"empty\"},\n\t\t\t\tBlock:   500 * time.Millisecond,\n\t\t\t\tID:      \"+\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t// Ensure that the XRead call with LastEntry option blocked for at least 100ms.\n\t\t\tExpect(time.Since(start)).To(BeNumerically(\">=\", 100*time.Millisecond))\n\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t{\n\t\t\t\t\tStream: \"empty\",\n\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t{ID: \"4-0\", Values: map[string]interface{}{\"quatro\": \"quatre\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t})\n\n\t\tDescribe(\"group\", func() {\n\t\t\tBeforeEach(func() {\n\t\t\t\terr := client.XGroupCreate(ctx, \"stream\", \"group\", \"0\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t\t{\n\t\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t})\n\n\t\t\tAfterEach(func() {\n\t\t\t\tn, err := client.XGroupDestroy(ctx, \"stream\", \"group\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(1)))\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup skip empty\", func() {\n\t\t\t\tn, err := client.XDel(ctx, \"stream\", \"2-0\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(1)))\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer\",\n\t\t\t\t\tStreams:  []string{\"stream\", \"0\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t\t{\n\t\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t\t\t\t{ID: \"2-0\", Values: nil},\n\t\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t})\n\n\t\t\tIt(\"should XGroupCreateMkStream\", func() {\n\t\t\t\terr := client.XGroupCreateMkStream(ctx, \"stream2\", \"group\", \"0\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\terr = client.XGroupCreateMkStream(ctx, \"stream2\", \"group\", \"0\").Err()\n\t\t\t\tExpect(err).To(Equal(proto.RedisError(\"BUSYGROUP Consumer Group name already exists\")))\n\n\t\t\t\tn, err := client.XGroupDestroy(ctx, \"stream2\", \"group\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(1)))\n\n\t\t\t\tn, err = client.Del(ctx, \"stream2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(1)))\n\t\t\t})\n\n\t\t\tIt(\"should XPending\", func() {\n\t\t\t\tinfo, err := client.XPending(ctx, \"stream\", \"group\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(info).To(Equal(&redis.XPending{\n\t\t\t\t\tCount:     3,\n\t\t\t\t\tLower:     \"1-0\",\n\t\t\t\t\tHigher:    \"3-0\",\n\t\t\t\t\tConsumers: map[string]int64{\"consumer\": 3},\n\t\t\t\t}))\n\t\t\t\targs := &redis.XPendingExtArgs{\n\t\t\t\t\tStream:   \"stream\",\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tStart:    \"-\",\n\t\t\t\t\tEnd:      \"+\",\n\t\t\t\t\tCount:    10,\n\t\t\t\t\tConsumer: \"consumer\",\n\t\t\t\t}\n\t\t\t\tinfoExt, err := client.XPendingExt(ctx, args).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tfor i := range infoExt {\n\t\t\t\t\tinfoExt[i].Idle = 0\n\t\t\t\t}\n\t\t\t\tExpect(infoExt).To(Equal([]redis.XPendingExt{\n\t\t\t\t\t{ID: \"1-0\", Consumer: \"consumer\", Idle: 0, RetryCount: 1},\n\t\t\t\t\t{ID: \"2-0\", Consumer: \"consumer\", Idle: 0, RetryCount: 1},\n\t\t\t\t\t{ID: \"3-0\", Consumer: \"consumer\", Idle: 0, RetryCount: 1},\n\t\t\t\t}))\n\n\t\t\t\targs.Idle = 72 * time.Hour\n\t\t\t\tinfoExt, err = client.XPendingExt(ctx, args).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(infoExt).To(HaveLen(0))\n\t\t\t})\n\n\t\t\tIt(\"should XGroup Create Delete Consumer\", func() {\n\t\t\t\tn, err := client.XGroupCreateConsumer(ctx, \"stream\", \"group\", \"c1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(1)))\n\n\t\t\t\tn, err = client.XGroupDelConsumer(ctx, \"stream\", \"group\", \"consumer\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(3)))\n\t\t\t})\n\n\t\t\tIt(\"should XAutoClaim\", func() {\n\t\t\t\txca := &redis.XAutoClaimArgs{\n\t\t\t\t\tStream:   \"stream\",\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer\",\n\t\t\t\t\tStart:    \"-\",\n\t\t\t\t\tCount:    2,\n\t\t\t\t}\n\t\t\t\tmsgs, start, err := client.XAutoClaim(ctx, xca).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(start).To(Equal(\"3-0\"))\n\t\t\t\tExpect(msgs).To(Equal([]redis.XMessage{{\n\t\t\t\t\tID:     \"1-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"uno\": \"un\"},\n\t\t\t\t}, {\n\t\t\t\t\tID:     \"2-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"dos\": \"deux\"},\n\t\t\t\t}}))\n\n\t\t\t\txca.Start = start\n\t\t\t\tmsgs, start, err = client.XAutoClaim(ctx, xca).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(start).To(Equal(\"0-0\"))\n\t\t\t\tExpect(msgs).To(Equal([]redis.XMessage{{\n\t\t\t\t\tID:     \"3-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"tres\": \"troix\"},\n\t\t\t\t}}))\n\n\t\t\t\tids, start, err := client.XAutoClaimJustID(ctx, xca).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(start).To(Equal(\"0-0\"))\n\t\t\t\tExpect(ids).To(Equal([]string{\"3-0\"}))\n\t\t\t})\n\n\t\t\tIt(\"should XClaim\", func() {\n\t\t\t\tmsgs, err := client.XClaim(ctx, &redis.XClaimArgs{\n\t\t\t\t\tStream:   \"stream\",\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer\",\n\t\t\t\t\tMessages: []string{\"1-0\", \"2-0\", \"3-0\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(msgs).To(Equal([]redis.XMessage{{\n\t\t\t\t\tID:     \"1-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"uno\": \"un\"},\n\t\t\t\t}, {\n\t\t\t\t\tID:     \"2-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"dos\": \"deux\"},\n\t\t\t\t}, {\n\t\t\t\t\tID:     \"3-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"tres\": \"troix\"},\n\t\t\t\t}}))\n\n\t\t\t\tids, err := client.XClaimJustID(ctx, &redis.XClaimArgs{\n\t\t\t\t\tStream:   \"stream\",\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer\",\n\t\t\t\t\tMessages: []string{\"1-0\", \"2-0\", \"3-0\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(ids).To(Equal([]string{\"1-0\", \"2-0\", \"3-0\"}))\n\t\t\t})\n\n\t\t\tIt(\"should XAck\", func() {\n\t\t\t\tn, err := client.XAck(ctx, \"stream\", \"group\", \"1-0\", \"2-0\", \"4-0\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(2)))\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup with CLAIM argument\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.3, \"XREADGROUP CLAIM requires Redis 8.3+\")\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer2\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t\tClaim:    50 * time.Millisecond,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(HaveLen(1))\n\t\t\t\tExpect(res[0].Stream).To(Equal(\"stream\"))\n\n\t\t\t\tmessages := res[0].Messages\n\t\t\t\tExpect(len(messages)).To(BeNumerically(\">=\", 1))\n\n\t\t\t\tfor _, msg := range messages {\n\t\t\t\t\tif msg.MillisElapsedFromDelivery > 0 {\n\t\t\t\t\t\tExpect(msg.MillisElapsedFromDelivery).To(BeNumerically(\">=\", 50))\n\t\t\t\t\t\tExpect(msg.DeliveredCount).To(BeNumerically(\">=\", 1))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup with CLAIM and COUNT\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.3, \"XREADGROUP CLAIM requires Redis 8.3+\")\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer3\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t\tClaim:    50 * time.Millisecond,\n\t\t\t\t\tCount:    2,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tif len(res) > 0 && len(res[0].Messages) > 0 {\n\t\t\t\t\tExpect(len(res[0].Messages)).To(BeNumerically(\"<=\", 2))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup with CLAIM and NOACK\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.3, \"XREADGROUP CLAIM requires Redis 8.3+\")\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer4\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t\tClaim:    50 * time.Millisecond,\n\t\t\t\t\tNoAck:    true,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tif len(res) > 0 {\n\t\t\t\t\tExpect(res[0].Stream).To(Equal(\"stream\"))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup CLAIM empties PEL after acknowledgment\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.3, \"XREADGROUP CLAIM requires Redis 8.3+\")\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer5\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t\tClaim:    50 * time.Millisecond,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tif len(res) > 0 && len(res[0].Messages) > 0 {\n\t\t\t\t\tids := make([]string, len(res[0].Messages))\n\t\t\t\t\tfor i, msg := range res[0].Messages {\n\t\t\t\t\t\tids[i] = msg.ID\n\t\t\t\t\t}\n\n\t\t\t\t\tn, err := client.XAck(ctx, \"stream\", \"group\", ids...).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(n).To(BeNumerically(\">=\", 1))\n\n\t\t\t\t\tpending, err := client.XPending(ctx, \"stream\", \"group\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(pending.Count).To(BeNumerically(\"<\", 3))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup backward compatibility without CLAIM\", func() {\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group\",\n\t\t\t\t\tConsumer: \"consumer_compat\",\n\t\t\t\t\tStreams:  []string{\"stream\", \"0\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(HaveLen(1))\n\t\t\t\tExpect(res[0].Stream).To(Equal(\"stream\"))\n\n\t\t\t\tfor _, msg := range res[0].Messages {\n\t\t\t\t\tExpect(msg.MillisElapsedFromDelivery).To(Equal(int64(0)))\n\t\t\t\t\tExpect(msg.DeliveredCount).To(Equal(int64(0)))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup CLAIM with multiple streams\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.3, \"XREADGROUP CLAIM requires Redis 8.3+\")\n\n\t\t\t\tid, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\t\tStream: \"stream2\",\n\t\t\t\t\tID:     \"1-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"field1\": \"value1\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(id).To(Equal(\"1-0\"))\n\n\t\t\t\tid, err = client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\t\tStream: \"stream2\",\n\t\t\t\t\tID:     \"2-0\",\n\t\t\t\t\tValues: map[string]interface{}{\"field2\": \"value2\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(id).To(Equal(\"2-0\"))\n\n\t\t\t\terr = client.XGroupCreate(ctx, \"stream2\", \"group2\", \"0\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t_, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group2\",\n\t\t\t\t\tConsumer: \"consumer1\",\n\t\t\t\t\tStreams:  []string{\"stream2\", \">\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group2\",\n\t\t\t\t\tConsumer: \"consumer2\",\n\t\t\t\t\tStreams:  []string{\"stream2\", \">\"},\n\t\t\t\t\tClaim:    50 * time.Millisecond,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tif len(res) > 0 {\n\t\t\t\t\tExpect(res[0].Stream).To(Equal(\"stream2\"))\n\t\t\t\t\tif len(res[0].Messages) > 0 {\n\t\t\t\t\t\tfor _, msg := range res[0].Messages {\n\t\t\t\t\t\t\tif msg.MillisElapsedFromDelivery > 0 {\n\t\t\t\t\t\t\t\tExpect(msg.DeliveredCount).To(BeNumerically(\">=\", 1))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should XReadGroup CLAIM work consistently on RESP2 and RESP3\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.3, \"XREADGROUP CLAIM requires Redis 8.3+\")\n\n\t\t\t\tstreamName := \"stream-resp-test\"\n\t\t\t\terr := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\t\tStream: streamName,\n\t\t\t\t\tValues: map[string]interface{}{\"field1\": \"value1\"},\n\t\t\t\t}).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\terr = client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\t\tStream: streamName,\n\t\t\t\t\tValues: map[string]interface{}{\"field2\": \"value2\"},\n\t\t\t\t}).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tgroupName := \"resp-test-group\"\n\t\t\t\terr = client.XGroupCreate(ctx, streamName, groupName, \"0\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t_, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    groupName,\n\t\t\t\t\tConsumer: \"consumer1\",\n\t\t\t\t\tStreams:  []string{streamName, \">\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\t// Test with RESP2 (protocol 2)\n\t\t\t\tresp2Client := redis.NewClient(&redis.Options{\n\t\t\t\t\tAddr:     redisAddr,\n\t\t\t\t\tProtocol: 2,\n\t\t\t\t})\n\t\t\t\tdefer resp2Client.Close()\n\n\t\t\t\tresp2Result, err := resp2Client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    groupName,\n\t\t\t\t\tConsumer: \"consumer2\",\n\t\t\t\t\tStreams:  []string{streamName, \"0\"},\n\t\t\t\t\tClaim:    50 * time.Millisecond,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resp2Result).To(HaveLen(1))\n\n\t\t\t\t// Test with RESP3 (protocol 3)\n\t\t\t\tresp3Client := redis.NewClient(&redis.Options{\n\t\t\t\t\tAddr:     redisAddr,\n\t\t\t\t\tProtocol: 3,\n\t\t\t\t})\n\t\t\t\tdefer resp3Client.Close()\n\n\t\t\t\tresp3Result, err := resp3Client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    groupName,\n\t\t\t\t\tConsumer: \"consumer3\",\n\t\t\t\t\tStreams:  []string{streamName, \"0\"},\n\t\t\t\t\tClaim:    50 * time.Millisecond,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resp3Result).To(HaveLen(1))\n\n\t\t\t\tExpect(len(resp2Result[0].Messages)).To(Equal(len(resp3Result[0].Messages)))\n\n\t\t\t\tfor i := range resp2Result[0].Messages {\n\t\t\t\t\tmsg2 := resp2Result[0].Messages[i]\n\t\t\t\t\tmsg3 := resp3Result[0].Messages[i]\n\n\t\t\t\t\tExpect(msg2.ID).To(Equal(msg3.ID))\n\n\t\t\t\t\tif msg2.MillisElapsedFromDelivery > 0 {\n\t\t\t\t\t\tExpect(msg3.MillisElapsedFromDelivery).To(BeNumerically(\">\", 0))\n\t\t\t\t\t\tExpect(msg2.DeliveredCount).To(Equal(msg3.DeliveredCount))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"xinfo\", func() {\n\t\t\tBeforeEach(func() {\n\t\t\t\terr := client.XGroupCreate(ctx, \"stream\", \"group1\", \"0\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tres, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group1\",\n\t\t\t\t\tConsumer: \"consumer1\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t\tCount:    2,\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t\t{\n\t\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t\t{ID: \"1-0\", Values: map[string]interface{}{\"uno\": \"un\"}},\n\t\t\t\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}))\n\n\t\t\t\tres, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group1\",\n\t\t\t\t\tConsumer: \"consumer2\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t\t{\n\t\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}))\n\n\t\t\t\terr = client.XGroupCreate(ctx, \"stream\", \"group2\", \"1-0\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tres, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\t\t\t\tGroup:    \"group2\",\n\t\t\t\t\tConsumer: \"consumer1\",\n\t\t\t\t\tStreams:  []string{\"stream\", \">\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]redis.XStream{\n\t\t\t\t\t{\n\t\t\t\t\t\tStream: \"stream\",\n\t\t\t\t\t\tMessages: []redis.XMessage{\n\t\t\t\t\t\t\t{ID: \"2-0\", Values: map[string]interface{}{\"dos\": \"deux\"}},\n\t\t\t\t\t\t\t{ID: \"3-0\", Values: map[string]interface{}{\"tres\": \"troix\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t})\n\n\t\t\tAfterEach(func() {\n\t\t\t\tn, err := client.XGroupDestroy(ctx, \"stream\", \"group1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(1)))\n\t\t\t\tn, err = client.XGroupDestroy(ctx, \"stream\", \"group2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(1)))\n\t\t\t})\n\n\t\t\tIt(\"should XINFO STREAM\", func() {\n\t\t\t\tres, err := client.XInfoStream(ctx, \"stream\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tres.RadixTreeKeys = 0\n\t\t\t\tres.RadixTreeNodes = 0\n\t\t\t\texpectedRes := &redis.XInfoStream{\n\t\t\t\t\tLength:            3,\n\t\t\t\t\tRadixTreeKeys:     0,\n\t\t\t\t\tRadixTreeNodes:    0,\n\t\t\t\t\tGroups:            2,\n\t\t\t\t\tLastGeneratedID:   \"3-0\",\n\t\t\t\t\tMaxDeletedEntryID: \"0-0\",\n\t\t\t\t\tEntriesAdded:      3,\n\t\t\t\t\tFirstEntry: redis.XMessage{\n\t\t\t\t\t\tID:     \"1-0\",\n\t\t\t\t\t\tValues: map[string]interface{}{\"uno\": \"un\"},\n\t\t\t\t\t},\n\t\t\t\t\tLastEntry: redis.XMessage{\n\t\t\t\t\t\tID:     \"3-0\",\n\t\t\t\t\t\tValues: map[string]interface{}{\"tres\": \"troix\"},\n\t\t\t\t\t},\n\t\t\t\t\tRecordedFirstEntryID: \"1-0\",\n\t\t\t\t\tIDMPDuration:         100,\n\t\t\t\t\tIDMPMaxSize:          100,\n\t\t\t\t\tPIDsTracked:          0,\n\t\t\t\t\tIIDsTracked:          0,\n\t\t\t\t\tIIDsAdded:            0,\n\t\t\t\t\tIIDsDuplicates:       0,\n\t\t\t\t}\n\t\t\t\tif RedisVersion < 8.6 {\n\t\t\t\t\texpectedRes.IDMPDuration = 0\n\t\t\t\t\texpectedRes.IDMPMaxSize = 0\n\t\t\t\t\texpectedRes.PIDsTracked = 0\n\t\t\t\t\texpectedRes.IIDsTracked = 0\n\t\t\t\t\texpectedRes.IIDsAdded = 0\n\t\t\t\t\texpectedRes.IIDsDuplicates = 0\n\t\t\t\t}\n\t\t\t\tExpect(res).To(Equal(expectedRes))\n\n\t\t\t\t// stream is empty\n\t\t\t\tn, err := client.XDel(ctx, \"stream\", \"1-0\", \"2-0\", \"3-0\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(3)))\n\n\t\t\t\tres, err = client.XInfoStream(ctx, \"stream\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tres.RadixTreeKeys = 0\n\t\t\t\tres.RadixTreeNodes = 0\n\t\t\t\texpectedRes = &redis.XInfoStream{\n\t\t\t\t\tLength:               0,\n\t\t\t\t\tRadixTreeKeys:        0,\n\t\t\t\t\tRadixTreeNodes:       0,\n\t\t\t\t\tGroups:               2,\n\t\t\t\t\tLastGeneratedID:      \"3-0\",\n\t\t\t\t\tMaxDeletedEntryID:    \"3-0\",\n\t\t\t\t\tEntriesAdded:         3,\n\t\t\t\t\tFirstEntry:           redis.XMessage{},\n\t\t\t\t\tLastEntry:            redis.XMessage{},\n\t\t\t\t\tRecordedFirstEntryID: \"0-0\",\n\t\t\t\t\tIDMPDuration:         100,\n\t\t\t\t\tIDMPMaxSize:          100,\n\t\t\t\t\tPIDsTracked:          0,\n\t\t\t\t\tIIDsTracked:          0,\n\t\t\t\t\tIIDsAdded:            0,\n\t\t\t\t\tIIDsDuplicates:       0,\n\t\t\t\t}\n\t\t\t\tif RedisVersion < 8.6 {\n\t\t\t\t\texpectedRes.IDMPDuration = 0\n\t\t\t\t\texpectedRes.IDMPMaxSize = 0\n\t\t\t\t\texpectedRes.PIDsTracked = 0\n\t\t\t\t\texpectedRes.IIDsTracked = 0\n\t\t\t\t\texpectedRes.IIDsAdded = 0\n\t\t\t\t\texpectedRes.IIDsDuplicates = 0\n\t\t\t\t}\n\n\t\t\t\tExpect(res).To(Equal(expectedRes))\n\t\t\t})\n\n\t\t\tIt(\"should XINFO STREAM FULL\", func() {\n\t\t\t\tres, err := client.XInfoStreamFull(ctx, \"stream\", 2).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tres.RadixTreeKeys = 0\n\t\t\t\tres.RadixTreeNodes = 0\n\n\t\t\t\t// Verify DeliveryTime\n\t\t\t\tnow := time.Now()\n\t\t\t\tmaxElapsed := 10 * time.Minute\n\t\t\t\tfor k, g := range res.Groups {\n\t\t\t\t\tfor k2, p := range g.Pending {\n\t\t\t\t\t\tExpect(now.Sub(p.DeliveryTime)).To(BeNumerically(\"<=\", maxElapsed))\n\t\t\t\t\t\tres.Groups[k].Pending[k2].DeliveryTime = time.Time{}\n\t\t\t\t\t}\n\t\t\t\t\tfor k3, c := range g.Consumers {\n\t\t\t\t\t\tExpect(now.Sub(c.SeenTime)).To(BeNumerically(\"<=\", maxElapsed))\n\t\t\t\t\t\tExpect(now.Sub(c.ActiveTime)).To(BeNumerically(\"<=\", maxElapsed))\n\t\t\t\t\t\tres.Groups[k].Consumers[k3].SeenTime = time.Time{}\n\t\t\t\t\t\tres.Groups[k].Consumers[k3].ActiveTime = time.Time{}\n\n\t\t\t\t\t\tfor k4, p := range c.Pending {\n\t\t\t\t\t\t\tExpect(now.Sub(p.DeliveryTime)).To(BeNumerically(\"<=\", maxElapsed))\n\t\t\t\t\t\t\tres.Groups[k].Consumers[k3].Pending[k4].DeliveryTime = time.Time{}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tExpect(res.Groups).To(Equal([]redis.XInfoStreamGroup{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:            \"group1\",\n\t\t\t\t\t\tLastDeliveredID: \"3-0\",\n\t\t\t\t\t\tEntriesRead:     3,\n\t\t\t\t\t\tLag:             0,\n\t\t\t\t\t\tPelCount:        3,\n\t\t\t\t\t\tPending: []redis.XInfoStreamGroupPending{\n\t\t\t\t\t\t\t{ID: \"1-0\", Consumer: \"consumer1\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t\t{ID: \"2-0\", Consumer: \"consumer1\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConsumers: []redis.XInfoStreamConsumer{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:       \"consumer1\",\n\t\t\t\t\t\t\t\tSeenTime:   time.Time{},\n\t\t\t\t\t\t\t\tActiveTime: time.Time{},\n\t\t\t\t\t\t\t\tPelCount:   2,\n\t\t\t\t\t\t\t\tPending: []redis.XInfoStreamConsumerPending{\n\t\t\t\t\t\t\t\t\t{ID: \"1-0\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t\t\t\t{ID: \"2-0\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:       \"consumer2\",\n\t\t\t\t\t\t\t\tSeenTime:   time.Time{},\n\t\t\t\t\t\t\t\tActiveTime: time.Time{},\n\t\t\t\t\t\t\t\tPelCount:   1,\n\t\t\t\t\t\t\t\tPending: []redis.XInfoStreamConsumerPending{\n\t\t\t\t\t\t\t\t\t{ID: \"3-0\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:            \"group2\",\n\t\t\t\t\t\tLastDeliveredID: \"3-0\",\n\t\t\t\t\t\tEntriesRead:     3,\n\t\t\t\t\t\tLag:             0,\n\t\t\t\t\t\tPelCount:        2,\n\t\t\t\t\t\tPending: []redis.XInfoStreamGroupPending{\n\t\t\t\t\t\t\t{ID: \"2-0\", Consumer: \"consumer1\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t\t{ID: \"3-0\", Consumer: \"consumer1\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConsumers: []redis.XInfoStreamConsumer{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:       \"consumer1\",\n\t\t\t\t\t\t\t\tSeenTime:   time.Time{},\n\t\t\t\t\t\t\t\tActiveTime: time.Time{},\n\t\t\t\t\t\t\t\tPelCount:   2,\n\t\t\t\t\t\t\t\tPending: []redis.XInfoStreamConsumerPending{\n\t\t\t\t\t\t\t\t\t{ID: \"2-0\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t\t\t\t{ID: \"3-0\", DeliveryTime: time.Time{}, DeliveryCount: 1},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}))\n\n\t\t\t\t// entries-read = nil\n\t\t\t\tExpect(client.Del(ctx, \"xinfo-stream-full-stream\").Err()).NotTo(HaveOccurred())\n\t\t\t\tid, err := client.XAdd(ctx, &redis.XAddArgs{\n\t\t\t\t\tStream: \"xinfo-stream-full-stream\",\n\t\t\t\t\tID:     \"*\",\n\t\t\t\t\tValues: []any{\"k1\", \"v1\"},\n\t\t\t\t}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.XGroupCreateMkStream(ctx, \"xinfo-stream-full-stream\", \"xinfo-stream-full-group\", \"0\").Err()).NotTo(HaveOccurred())\n\t\t\t\tres, err = client.XInfoStreamFull(ctx, \"xinfo-stream-full-stream\", 0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\texpectedRes := &redis.XInfoStreamFull{\n\t\t\t\t\tLength:            1,\n\t\t\t\t\tRadixTreeKeys:     1,\n\t\t\t\t\tRadixTreeNodes:    2,\n\t\t\t\t\tLastGeneratedID:   id,\n\t\t\t\t\tMaxDeletedEntryID: \"0-0\",\n\t\t\t\t\tEntriesAdded:      1,\n\t\t\t\t\tEntries:           []redis.XMessage{{ID: id, Values: map[string]any{\"k1\": \"v1\"}}},\n\t\t\t\t\tGroups: []redis.XInfoStreamGroup{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:            \"xinfo-stream-full-group\",\n\t\t\t\t\t\t\tLastDeliveredID: \"0-0\",\n\t\t\t\t\t\t\tEntriesRead:     0,\n\t\t\t\t\t\t\tLag:             1,\n\t\t\t\t\t\t\tPelCount:        0,\n\t\t\t\t\t\t\tPending:         []redis.XInfoStreamGroupPending{},\n\t\t\t\t\t\t\tConsumers:       []redis.XInfoStreamConsumer{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRecordedFirstEntryID: id,\n\t\t\t\t}\n\t\t\t\tif RedisVersion >= 8.6 {\n\t\t\t\t\texpectedRes.IDMPDuration = 100\n\t\t\t\t\texpectedRes.IDMPMaxSize = 100\n\t\t\t\t\texpectedRes.PIDsTracked = 0\n\t\t\t\t\texpectedRes.IIDsTracked = 0\n\t\t\t\t\texpectedRes.IIDsAdded = 0\n\t\t\t\t\texpectedRes.IIDsDuplicates = 0\n\t\t\t\t}\n\t\t\t\tExpect(res).To(Equal(expectedRes))\n\t\t\t})\n\n\t\t\tIt(\"should XINFO GROUPS\", func() {\n\t\t\t\tres, err := client.XInfoGroups(ctx, \"stream\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]redis.XInfoGroup{\n\t\t\t\t\t{Name: \"group1\", Consumers: 2, Pending: 3, LastDeliveredID: \"3-0\", EntriesRead: 3},\n\t\t\t\t\t{Name: \"group2\", Consumers: 1, Pending: 2, LastDeliveredID: \"3-0\", EntriesRead: 3},\n\t\t\t\t}))\n\t\t\t})\n\n\t\t\tIt(\"should return -1 for nil lag in XINFO GROUPS\", func() {\n\t\t\t\t_, err := client.XAdd(ctx, &redis.XAddArgs{Stream: \"s\", ID: \"0-1\", Values: []string{\"foo\", \"1\"}}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tclient.XAdd(ctx, &redis.XAddArgs{Stream: \"s\", ID: \"0-2\", Values: []string{\"foo\", \"2\"}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tclient.XAdd(ctx, &redis.XAddArgs{Stream: \"s\", ID: \"0-3\", Values: []string{\"foo\", \"3\"}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\terr = client.XGroupCreate(ctx, \"s\", \"g\", \"0\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\terr = client.XReadGroup(ctx, &redis.XReadGroupArgs{Group: \"g\", Consumer: \"c\", Streams: []string{\"s\", \">\"}, Count: 1, Block: -1, NoAck: false}).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tclient.XDel(ctx, \"s\", \"0-2\")\n\n\t\t\t\tres, err := client.XInfoGroups(ctx, \"s\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]redis.XInfoGroup{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:            \"g\",\n\t\t\t\t\t\tConsumers:       1,\n\t\t\t\t\t\tPending:         1,\n\t\t\t\t\t\tLastDeliveredID: \"0-1\",\n\t\t\t\t\t\tEntriesRead:     1,\n\t\t\t\t\t\tLag:             -1, // nil lag from Redis is reported as -1\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t})\n\n\t\t\tIt(\"should XINFO CONSUMERS\", func() {\n\t\t\t\tres, err := client.XInfoConsumers(ctx, \"stream\", \"group1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tfor i := range res {\n\t\t\t\t\tres[i].Idle = 0\n\t\t\t\t\tres[i].Inactive = 0\n\t\t\t\t}\n\n\t\t\t\tExpect(res).To(Equal([]redis.XInfoConsumer{\n\t\t\t\t\t{Name: \"consumer1\", Pending: 2, Idle: 0, Inactive: 0},\n\t\t\t\t\t{Name: \"consumer2\", Pending: 1, Idle: 0, Inactive: 0},\n\t\t\t\t}))\n\t\t\t})\n\t\t})\n\t})\n\n\tDescribe(\"Geo add and radius search\", func() {\n\t\tBeforeEach(func() {\n\t\t\tn, err := client.GeoAdd(\n\t\t\t\tctx,\n\t\t\t\t\"Sicily\",\n\t\t\t\t&redis.GeoLocation{Longitude: 13.361389, Latitude: 38.115556, Name: \"Palermo\"},\n\t\t\t\t&redis.GeoLocation{Longitude: 15.087269, Latitude: 37.502669, Name: \"Catania\"},\n\t\t\t).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\t\t})\n\n\t\tIt(\"should not add same geo location\", func() {\n\t\t\tgeoAdd := client.GeoAdd(\n\t\t\t\tctx,\n\t\t\t\t\"Sicily\",\n\t\t\t\t&redis.GeoLocation{Longitude: 13.361389, Latitude: 38.115556, Name: \"Palermo\"},\n\t\t\t)\n\t\t\tExpect(geoAdd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(geoAdd.Val()).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should search geo radius\", func() {\n\t\t\tres, err := client.GeoRadius(ctx, \"Sicily\", 15, 37, &redis.GeoRadiusQuery{\n\t\t\t\tRadius: 200,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(HaveLen(2))\n\t\t\tExpect(res[0].Name).To(Equal(\"Palermo\"))\n\t\t\tExpect(res[1].Name).To(Equal(\"Catania\"))\n\t\t})\n\n\t\tIt(\"should geo radius and store the result\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tn, err := client.GeoRadiusStore(ctx, \"Sicily\", 15, 37, &redis.GeoRadiusQuery{\n\t\t\t\tRadius: 200,\n\t\t\t\tStore:  \"result\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\n\t\t\tres, err := client.ZRangeWithScores(ctx, \"result\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(ContainElement(redis.Z{\n\t\t\t\tScore:  3.479099956230698e+15,\n\t\t\t\tMember: \"Palermo\",\n\t\t\t}))\n\t\t\tExpect(res).To(ContainElement(redis.Z{\n\t\t\t\tScore:  3.479447370796909e+15,\n\t\t\t\tMember: \"Catania\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should geo radius and store dist\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tn, err := client.GeoRadiusStore(ctx, \"Sicily\", 15, 37, &redis.GeoRadiusQuery{\n\t\t\t\tRadius:    200,\n\t\t\t\tStoreDist: \"result\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(2)))\n\n\t\t\tres, err := client.ZRangeWithScores(ctx, \"result\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(res)).To(Equal(2))\n\t\t\tvar palermo, catania redis.Z\n\t\t\tExpect(res).To(ContainElement(HaveField(\"Member\", \"Palermo\"), &palermo))\n\t\t\tExpect(res).To(ContainElement(HaveField(\"Member\", \"Catania\"), &catania))\n\t\t\tExpect(palermo.Score).To(BeNumerically(\"~\", 190, 1))\n\t\t\tExpect(catania.Score).To(BeNumerically(\"~\", 56, 1))\n\t\t})\n\n\t\tIt(\"should search geo radius with options\", func() {\n\t\t\tres, err := client.GeoRadius(ctx, \"Sicily\", 15, 37, &redis.GeoRadiusQuery{\n\t\t\t\tRadius:      200,\n\t\t\t\tUnit:        \"km\",\n\t\t\t\tWithGeoHash: true,\n\t\t\t\tWithCoord:   true,\n\t\t\t\tWithDist:    true,\n\t\t\t\tCount:       2,\n\t\t\t\tSort:        \"ASC\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(HaveLen(2))\n\t\t\tExpect(res[1].Name).To(Equal(\"Palermo\"))\n\t\t\tExpect(res[1].Dist).To(Equal(190.4424))\n\t\t\tExpect(res[1].GeoHash).To(Equal(int64(3479099956230698)))\n\t\t\tExpect(res[1].Longitude).To(Equal(13.361389338970184))\n\t\t\tExpect(res[1].Latitude).To(Equal(38.115556395496299))\n\t\t\tExpect(res[0].Name).To(Equal(\"Catania\"))\n\t\t\tExpect(res[0].Dist).To(Equal(56.4413))\n\t\t\tExpect(res[0].GeoHash).To(Equal(int64(3479447370796909)))\n\t\t\tExpect(res[0].Longitude).To(Equal(15.087267458438873))\n\t\t\tExpect(res[0].Latitude).To(Equal(37.50266842333162))\n\t\t})\n\n\t\tIt(\"should search geo radius with WithDist=false\", func() {\n\t\t\tres, err := client.GeoRadius(ctx, \"Sicily\", 15, 37, &redis.GeoRadiusQuery{\n\t\t\t\tRadius:      200,\n\t\t\t\tUnit:        \"km\",\n\t\t\t\tWithGeoHash: true,\n\t\t\t\tWithCoord:   true,\n\t\t\t\tCount:       2,\n\t\t\t\tSort:        \"ASC\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(HaveLen(2))\n\t\t\tExpect(res[1].Name).To(Equal(\"Palermo\"))\n\t\t\tExpect(res[1].Dist).To(Equal(float64(0)))\n\t\t\tExpect(res[1].GeoHash).To(Equal(int64(3479099956230698)))\n\t\t\tExpect(res[1].Longitude).To(Equal(13.361389338970184))\n\t\t\tExpect(res[1].Latitude).To(Equal(38.115556395496299))\n\t\t\tExpect(res[0].Name).To(Equal(\"Catania\"))\n\t\t\tExpect(res[0].Dist).To(Equal(float64(0)))\n\t\t\tExpect(res[0].GeoHash).To(Equal(int64(3479447370796909)))\n\t\t\tExpect(res[0].Longitude).To(Equal(15.087267458438873))\n\t\t\tExpect(res[0].Latitude).To(Equal(37.50266842333162))\n\t\t})\n\n\t\tIt(\"should search geo radius by member with options\", func() {\n\t\t\tres, err := client.GeoRadiusByMember(ctx, \"Sicily\", \"Catania\", &redis.GeoRadiusQuery{\n\t\t\t\tRadius:      200,\n\t\t\t\tUnit:        \"km\",\n\t\t\t\tWithGeoHash: true,\n\t\t\t\tWithCoord:   true,\n\t\t\t\tWithDist:    true,\n\t\t\t\tCount:       2,\n\t\t\t\tSort:        \"ASC\",\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(HaveLen(2))\n\t\t\tExpect(res[0].Name).To(Equal(\"Catania\"))\n\t\t\tExpect(res[0].Dist).To(Equal(0.0))\n\t\t\tExpect(res[0].GeoHash).To(Equal(int64(3479447370796909)))\n\t\t\tExpect(res[0].Longitude).To(Equal(15.087267458438873))\n\t\t\tExpect(res[0].Latitude).To(Equal(37.50266842333162))\n\t\t\tExpect(res[1].Name).To(Equal(\"Palermo\"))\n\t\t\tExpect(res[1].Dist).To(Equal(166.2742))\n\t\t\tExpect(res[1].GeoHash).To(Equal(int64(3479099956230698)))\n\t\t\tExpect(res[1].Longitude).To(Equal(13.361389338970184))\n\t\t\tExpect(res[1].Latitude).To(Equal(38.115556395496299))\n\t\t})\n\n\t\tIt(\"should search geo radius with no results\", func() {\n\t\t\tres, err := client.GeoRadius(ctx, \"Sicily\", 99, 37, &redis.GeoRadiusQuery{\n\t\t\t\tRadius:      200,\n\t\t\t\tUnit:        \"km\",\n\t\t\t\tWithGeoHash: true,\n\t\t\t\tWithCoord:   true,\n\t\t\t\tWithDist:    true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(HaveLen(0))\n\t\t})\n\n\t\tIt(\"should get geo distance with unit options\", func() {\n\t\t\t// From Redis CLI, note the difference in rounding in m vs\n\t\t\t// km on Redis itself.\n\t\t\t//\n\t\t\t// GEOADD Sicily 13.361389 38.115556 \"Palermo\" 15.087269 37.502669 \"Catania\"\n\t\t\t// GEODIST Sicily Palermo Catania m\n\t\t\t// \"166274.15156960033\"\n\t\t\t// GEODIST Sicily Palermo Catania km\n\t\t\t// \"166.27415156960032\"\n\t\t\tdist, err := client.GeoDist(ctx, \"Sicily\", \"Palermo\", \"Catania\", \"km\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(dist).To(BeNumerically(\"~\", 166.27, 0.01))\n\n\t\t\tdist, err = client.GeoDist(ctx, \"Sicily\", \"Palermo\", \"Catania\", \"m\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(dist).To(BeNumerically(\"~\", 166274.15, 0.01))\n\t\t})\n\n\t\tIt(\"should get geo hash in string representation\", func() {\n\t\t\thashes, err := client.GeoHash(ctx, \"Sicily\", \"Palermo\", \"Catania\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(hashes).To(ConsistOf([]string{\"sqc8b49rny0\", \"sqdtr74hyu0\"}))\n\t\t})\n\n\t\tIt(\"should return geo position\", func() {\n\t\t\tpos, err := client.GeoPos(ctx, \"Sicily\", \"Palermo\", \"Catania\", \"NonExisting\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(pos).To(ConsistOf([]*redis.GeoPos{\n\t\t\t\t{\n\t\t\t\t\tLongitude: 13.361389338970184,\n\t\t\t\t\tLatitude:  38.1155563954963,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tLongitude: 15.087267458438873,\n\t\t\t\t\tLatitude:  37.50266842333162,\n\t\t\t\t},\n\t\t\t\tnil,\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should geo search\", func() {\n\t\t\tq := &redis.GeoSearchQuery{\n\t\t\t\tMember:    \"Catania\",\n\t\t\t\tBoxWidth:  400,\n\t\t\t\tBoxHeight: 100,\n\t\t\t\tBoxUnit:   \"km\",\n\t\t\t\tSort:      \"asc\",\n\t\t\t}\n\t\t\tval, err := client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.BoxHeight = 400\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\", \"Palermo\"}))\n\n\t\t\tq.Count = 1\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.CountAny = true\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Palermo\"}))\n\n\t\t\tq = &redis.GeoSearchQuery{\n\t\t\t\tMember:     \"Catania\",\n\t\t\t\tRadius:     100,\n\t\t\t\tRadiusUnit: \"km\",\n\t\t\t\tSort:       \"asc\",\n\t\t\t}\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.Radius = 400\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\", \"Palermo\"}))\n\n\t\t\tq.Count = 1\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.CountAny = true\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Palermo\"}))\n\n\t\t\tq = &redis.GeoSearchQuery{\n\t\t\t\tLongitude: 15,\n\t\t\t\tLatitude:  37,\n\t\t\t\tBoxWidth:  200,\n\t\t\t\tBoxHeight: 200,\n\t\t\t\tBoxUnit:   \"km\",\n\t\t\t\tSort:      \"asc\",\n\t\t\t}\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.BoxWidth, q.BoxHeight = 400, 400\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\", \"Palermo\"}))\n\n\t\t\tq.Count = 1\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.CountAny = true\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Palermo\"}))\n\n\t\t\tq = &redis.GeoSearchQuery{\n\t\t\t\tLongitude:  15,\n\t\t\t\tLatitude:   37,\n\t\t\t\tRadius:     100,\n\t\t\t\tRadiusUnit: \"km\",\n\t\t\t\tSort:       \"asc\",\n\t\t\t}\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.Radius = 200\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\", \"Palermo\"}))\n\n\t\t\tq.Count = 1\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Catania\"}))\n\n\t\t\tq.CountAny = true\n\t\t\tval, err = client.GeoSearch(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]string{\"Palermo\"}))\n\t\t})\n\n\t\tIt(\"should geo search with options\", func() {\n\t\t\tq := &redis.GeoSearchLocationQuery{\n\t\t\t\tGeoSearchQuery: redis.GeoSearchQuery{\n\t\t\t\t\tLongitude:  15,\n\t\t\t\t\tLatitude:   37,\n\t\t\t\t\tRadius:     200,\n\t\t\t\t\tRadiusUnit: \"km\",\n\t\t\t\t\tSort:       \"asc\",\n\t\t\t\t},\n\t\t\t\tWithHash:  true,\n\t\t\t\tWithDist:  true,\n\t\t\t\tWithCoord: true,\n\t\t\t}\n\t\t\tval, err := client.GeoSearchLocation(ctx, \"Sicily\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal([]redis.GeoLocation{\n\t\t\t\t{\n\t\t\t\t\tName:      \"Catania\",\n\t\t\t\t\tLongitude: 15.08726745843887329,\n\t\t\t\t\tLatitude:  37.50266842333162032,\n\t\t\t\t\tDist:      56.4413,\n\t\t\t\t\tGeoHash:   3479447370796909,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"Palermo\",\n\t\t\t\t\tLongitude: 13.36138933897018433,\n\t\t\t\t\tLatitude:  38.11555639549629859,\n\t\t\t\t\tDist:      190.4424,\n\t\t\t\t\tGeoHash:   3479099956230698,\n\t\t\t\t},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should geo search store\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tq := &redis.GeoSearchStoreQuery{\n\t\t\t\tGeoSearchQuery: redis.GeoSearchQuery{\n\t\t\t\t\tLongitude:  15,\n\t\t\t\t\tLatitude:   37,\n\t\t\t\t\tRadius:     200,\n\t\t\t\t\tRadiusUnit: \"km\",\n\t\t\t\t\tSort:       \"asc\",\n\t\t\t\t},\n\t\t\t\tStoreDist: false,\n\t\t\t}\n\n\t\t\tval, err := client.GeoSearchStore(ctx, \"Sicily\", \"key1\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(int64(2)))\n\n\t\t\tq.StoreDist = true\n\t\t\tval, err = client.GeoSearchStore(ctx, \"Sicily\", \"key2\", q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(int64(2)))\n\n\t\t\tloc, err := client.GeoSearchLocation(ctx, \"key1\", &redis.GeoSearchLocationQuery{\n\t\t\t\tGeoSearchQuery: q.GeoSearchQuery,\n\t\t\t\tWithCoord:      true,\n\t\t\t\tWithDist:       true,\n\t\t\t\tWithHash:       true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(loc).To(Equal([]redis.GeoLocation{\n\t\t\t\t{\n\t\t\t\t\tName:      \"Catania\",\n\t\t\t\t\tLongitude: 15.08726745843887329,\n\t\t\t\t\tLatitude:  37.50266842333162032,\n\t\t\t\t\tDist:      56.4413,\n\t\t\t\t\tGeoHash:   3479447370796909,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"Palermo\",\n\t\t\t\t\tLongitude: 13.36138933897018433,\n\t\t\t\t\tLatitude:  38.11555639549629859,\n\t\t\t\t\tDist:      190.4424,\n\t\t\t\t\tGeoHash:   3479099956230698,\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\tv, err := client.ZRangeWithScores(ctx, \"key2\", 0, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(len(v)).To(Equal(2))\n\t\t\tvar palermo, catania redis.Z\n\t\t\tExpect(v).To(ContainElement(HaveField(\"Member\", \"Palermo\"), &palermo))\n\t\t\tExpect(v).To(ContainElement(HaveField(\"Member\", \"Catania\"), &catania))\n\t\t\tExpect(palermo.Score).To(BeNumerically(\"~\", 190, 1))\n\t\t\tExpect(catania.Score).To(BeNumerically(\"~\", 56, 1))\n\t\t})\n\t})\n\n\tDescribe(\"marshaling/unmarshaling\", func() {\n\t\ttype convTest struct {\n\t\t\tvalue  interface{}\n\t\t\twanted string\n\t\t\tdest   interface{}\n\t\t}\n\n\t\tconvTests := []convTest{\n\t\t\t{nil, \"\", nil},\n\t\t\t{\"hello\", \"hello\", new(string)},\n\t\t\t{[]byte(\"hello\"), \"hello\", new([]byte)},\n\t\t\t{1, \"1\", new(int)},\n\t\t\t{int8(1), \"1\", new(int8)},\n\t\t\t{int16(1), \"1\", new(int16)},\n\t\t\t{int32(1), \"1\", new(int32)},\n\t\t\t{int64(1), \"1\", new(int64)},\n\t\t\t{uint(1), \"1\", new(uint)},\n\t\t\t{uint8(1), \"1\", new(uint8)},\n\t\t\t{uint16(1), \"1\", new(uint16)},\n\t\t\t{uint32(1), \"1\", new(uint32)},\n\t\t\t{uint64(1), \"1\", new(uint64)},\n\t\t\t{float32(1.0), \"1\", new(float32)},\n\t\t\t{1.0, \"1\", new(float64)},\n\t\t\t{true, \"1\", new(bool)},\n\t\t\t{false, \"0\", new(bool)},\n\t\t}\n\n\t\tIt(\"should convert to string\", func() {\n\t\t\tfor _, test := range convTests {\n\t\t\t\terr := client.Set(ctx, \"key\", test.value, 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\ts, err := client.Get(ctx, \"key\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(s).To(Equal(test.wanted))\n\n\t\t\t\tif test.dest == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\terr = client.Get(ctx, \"key\").Scan(test.dest)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(deref(test.dest)).To(Equal(test.value))\n\t\t\t}\n\t\t})\n\t})\n\n\tDescribe(\"json marshaling/unmarshaling\", func() {\n\t\tBeforeEach(func() {\n\t\t\tvalue := &numberStruct{Number: 42}\n\t\t\terr := client.Set(ctx, \"key\", value, 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should marshal custom values using json\", func() {\n\t\t\ts, err := client.Get(ctx, \"key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(s).To(Equal(`{\"Number\":42}`))\n\t\t})\n\n\t\tIt(\"should scan custom values using json\", func() {\n\t\t\tvalue := &numberStruct{}\n\t\t\terr := client.Get(ctx, \"key\").Scan(value)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(value.Number).To(Equal(42))\n\t\t})\n\t})\n\n\tDescribe(\"Eval\", func() {\n\t\tIt(\"returns keys and values\", func() {\n\t\t\tvals, err := client.Eval(\n\t\t\t\tctx,\n\t\t\t\t\"return {KEYS[1],ARGV[1]}\",\n\t\t\t\t[]string{\"key\"},\n\t\t\t\t\"hello\",\n\t\t\t).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]interface{}{\"key\", \"hello\"}))\n\t\t})\n\n\t\tIt(\"returns all values after an error\", func() {\n\t\t\tvals, err := client.Eval(\n\t\t\t\tctx,\n\t\t\t\t`return {12, {err=\"error\"}, \"abc\"}`,\n\t\t\t\tnil,\n\t\t\t).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]interface{}{int64(12), proto.RedisError(\"error\"), \"abc\"}))\n\t\t})\n\n\t\tIt(\"returns empty values when args are nil\", func() {\n\t\t\tvals, err := client.Eval(\n\t\t\t\tctx,\n\t\t\t\t\"return {ARGV[1]}\",\n\t\t\t\t[]string{},\n\t\t\t\tnil,\n\t\t\t).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(BeEmpty())\n\t\t})\n\n\t\tIt(\"propagates NOSCRIPT errors on EVALSHA of an unknown digest\", func() {\n\t\t\tdigest := make([]byte, 32)\n\t\t\t_, err := rand.Read(digest)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t_, err = client.EvalSha(\n\t\t\t\tctx,\n\t\t\t\tfmt.Sprintf(\"%x\", sha1.Sum(digest)),\n\t\t\t\t[]string{},\n\t\t\t\tnil,\n\t\t\t).Result()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err).To(MatchError(redis.ErrNoScript))\n\t\t})\n\t})\n\n\tDescribe(\"EvalRO\", func() {\n\t\tIt(\"returns keys and values\", func() {\n\t\t\tvals, err := client.EvalRO(\n\t\t\t\tctx,\n\t\t\t\t\"return {KEYS[1],ARGV[1]}\",\n\t\t\t\t[]string{\"key\"},\n\t\t\t\t\"hello\",\n\t\t\t).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]interface{}{\"key\", \"hello\"}))\n\t\t})\n\n\t\tIt(\"returns all values after an error\", func() {\n\t\t\tvals, err := client.EvalRO(\n\t\t\t\tctx,\n\t\t\t\t`return {12, {err=\"error\"}, \"abc\"}`,\n\t\t\t\tnil,\n\t\t\t).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(Equal([]interface{}{int64(12), proto.RedisError(\"error\"), \"abc\"}))\n\t\t})\n\n\t\tIt(\"returns empty values when args are nil\", func() {\n\t\t\tvals, err := client.EvalRO(\n\t\t\t\tctx,\n\t\t\t\t\"return {ARGV[1]}\",\n\t\t\t\t[]string{},\n\t\t\t\tnil,\n\t\t\t).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(vals).To(BeEmpty())\n\t\t})\n\n\t\tIt(\"propagates NOSCRIPT errors on EVALSHA_RO of an unknown digest\", func() {\n\t\t\tdigest := make([]byte, 32)\n\t\t\t_, err := rand.Read(digest)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t_, err = client.EvalShaRO(\n\t\t\t\tctx,\n\t\t\t\tfmt.Sprintf(\"%x\", sha1.Sum(digest)),\n\t\t\t\t[]string{},\n\t\t\t\tnil,\n\t\t\t).Result()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err).To(MatchError(redis.ErrNoScript))\n\t\t})\n\t})\n\n\tDescribe(\"Functions\", func() {\n\t\tvar (\n\t\t\tq        redis.FunctionListQuery\n\t\t\tlib1Code string\n\t\t\tlib2Code string\n\t\t\tlib1     redis.Library\n\t\t\tlib2     redis.Library\n\t\t)\n\n\t\tBeforeEach(func() {\n\t\t\tflush := client.FunctionFlush(ctx)\n\t\t\tExpect(flush.Err()).NotTo(HaveOccurred())\n\n\t\t\tlib1 = redis.Library{\n\t\t\t\tName:   \"mylib1\",\n\t\t\t\tEngine: \"LUA\",\n\t\t\t\tFunctions: []redis.Function{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:        \"lib1_func1\",\n\t\t\t\t\t\tDescription: \"This is the func-1 of lib 1\",\n\t\t\t\t\t\tFlags:       []string{\"allow-oom\", \"allow-stale\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCode: `#!lua name=%s\n\t\t\t\t\t\n                     local function f1(keys, args)\n                        local hash = keys[1]  -- Get the key name\n                        local time = redis.call('TIME')[1]  -- Get the current time from the Redis server\n\n                        -- Add the current timestamp to the arguments that the user passed to the function, stored in args\n                        table.insert(args, '_updated_at')\n                        table.insert(args, time)\n\n                        -- Run HSET with the updated argument list\n                        return redis.call('HSET', hash, unpack(args))\n                     end\n\n\t\t\t\t\tredis.register_function{\n\t\t\t\t\t\tfunction_name='%s',\n\t\t\t\t\t\tdescription ='%s',\n\t\t\t\t\t\tcallback=f1,\n\t\t\t\t\t\tflags={'%s', '%s'}\n\t\t\t\t\t}`,\n\t\t\t}\n\n\t\t\tlib2 = redis.Library{\n\t\t\t\tName:   \"mylib2\",\n\t\t\t\tEngine: \"LUA\",\n\t\t\t\tFunctions: []redis.Function{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:  \"lib2_func1\",\n\t\t\t\t\t\tFlags: []string{},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:        \"lib2_func2\",\n\t\t\t\t\t\tDescription: \"This is the func-2 of lib 2\",\n\t\t\t\t\t\tFlags:       []string{\"no-writes\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCode: `#!lua name=%s\n\n\t\t\t\t\tlocal function f1(keys, args)\n\t\t\t\t\t\t return 'Function 1'\n\t\t\t\t\tend\n\t\t\t\t\t\n\t\t\t\t\tlocal function f2(keys, args)\n\t\t\t\t\t\t return 'Function 2'\n\t\t\t\t\tend\n\t\t\t\t\t\n\t\t\t\t\tredis.register_function('%s', f1)\n\t\t\t\t\tredis.register_function{\n\t\t\t\t\t\tfunction_name='%s',\n\t\t\t\t\t\tdescription ='%s',\n\t\t\t\t\t\tcallback=f2,\n\t\t\t\t\t\tflags={'%s'}\n\t\t\t\t\t}`,\n\t\t\t}\n\n\t\t\tlib1Code = fmt.Sprintf(lib1.Code, lib1.Name, lib1.Functions[0].Name,\n\t\t\t\tlib1.Functions[0].Description, lib1.Functions[0].Flags[0], lib1.Functions[0].Flags[1])\n\t\t\tlib2Code = fmt.Sprintf(lib2.Code, lib2.Name, lib2.Functions[0].Name,\n\t\t\t\tlib2.Functions[1].Name, lib2.Functions[1].Description, lib2.Functions[1].Flags[0])\n\n\t\t\tq = redis.FunctionListQuery{}\n\t\t})\n\n\t\tIt(\"Loads a new library\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tfunctionLoad := client.FunctionLoad(ctx, lib1Code)\n\t\t\tExpect(functionLoad.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(functionLoad.Val()).To(Equal(lib1.Name))\n\n\t\t\tfunctionList := client.FunctionList(ctx, q)\n\t\t\tExpect(functionList.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(functionList.Val()).To(HaveLen(1))\n\t\t})\n\n\t\tIt(\"Loads and replaces a new library\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\t// Load a library for the first time\n\t\t\terr := client.FunctionLoad(ctx, lib1Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tnewFuncName := \"replaces_func_name\"\n\t\t\tnewFuncDesc := \"replaces_func_desc\"\n\t\t\tflag1, flag2 := \"allow-stale\", \"no-cluster\"\n\t\t\tnewCode := fmt.Sprintf(lib1.Code, lib1.Name, newFuncName, newFuncDesc, flag1, flag2)\n\n\t\t\t// And then replace it\n\t\t\tfunctionLoadReplace := client.FunctionLoadReplace(ctx, newCode)\n\t\t\tExpect(functionLoadReplace.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(functionLoadReplace.Val()).To(Equal(lib1.Name))\n\n\t\t\tlib, err := client.FunctionList(ctx, q).First()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(lib.Functions).To(Equal([]redis.Function{\n\t\t\t\t{\n\t\t\t\t\tName:        newFuncName,\n\t\t\t\t\tDescription: newFuncDesc,\n\t\t\t\t\tFlags:       []string{flag1, flag2},\n\t\t\t\t},\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"Deletes a library\", func() {\n\t\t\terr := client.FunctionLoad(ctx, lib1Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.FunctionDelete(ctx, lib1.Name).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.FunctionList(ctx, redis.FunctionListQuery{\n\t\t\t\tLibraryNamePattern: lib1.Name,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(HaveLen(0))\n\t\t})\n\n\t\tIt(\"Flushes all libraries\", func() {\n\t\t\terr := client.FunctionLoad(ctx, lib1Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.FunctionLoad(ctx, lib2Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.FunctionFlush(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.FunctionList(ctx, q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(HaveLen(0))\n\t\t})\n\n\t\tIt(\"Flushes all libraries asynchronously\", func() {\n\t\t\tfunctionLoad := client.FunctionLoad(ctx, lib1Code)\n\t\t\tExpect(functionLoad.Err()).NotTo(HaveOccurred())\n\n\t\t\t// we only verify the command result.\n\t\t\tfunctionFlush := client.FunctionFlushAsync(ctx)\n\t\t\tExpect(functionFlush.Err()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"Kills a running function\", func() {\n\t\t\tfunctionKill := client.FunctionKill(ctx)\n\t\t\tExpect(functionKill.Err()).To(MatchError(\"NOTBUSY No scripts in execution right now.\"))\n\n\t\t\t// Add test for a long-running function, once we make the test for `function stats` pass\n\t\t})\n\n\t\tIt(\"Lists registered functions\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.FunctionLoad(ctx, lib1Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.FunctionList(ctx, redis.FunctionListQuery{\n\t\t\t\tLibraryNamePattern: \"*\",\n\t\t\t\tWithCode:           true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(HaveLen(1))\n\t\t\tExpect(val[0].Name).To(Equal(lib1.Name))\n\t\t\tExpect(val[0].Engine).To(Equal(lib1.Engine))\n\t\t\tExpect(val[0].Code).To(Equal(lib1Code))\n\t\t\tExpect(val[0].Functions).Should(ConsistOf(lib1.Functions))\n\n\t\t\terr = client.FunctionLoad(ctx, lib2Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err = client.FunctionList(ctx, redis.FunctionListQuery{\n\t\t\t\tWithCode: true,\n\t\t\t}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(HaveLen(2))\n\n\t\t\tlib, err := client.FunctionList(ctx, redis.FunctionListQuery{\n\t\t\t\tLibraryNamePattern: lib2.Name,\n\t\t\t\tWithCode:           false,\n\t\t\t}).First()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(lib.Name).To(Equal(lib2.Name))\n\t\t\tExpect(lib.Code).To(Equal(\"\"))\n\n\t\t\t_, err = client.FunctionList(ctx, redis.FunctionListQuery{\n\t\t\t\tLibraryNamePattern: \"non_lib\",\n\t\t\t\tWithCode:           true,\n\t\t\t}).First()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t})\n\n\t\tIt(\"Dump and restores all libraries\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\terr := client.FunctionLoad(ctx, lib1Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\terr = client.FunctionLoad(ctx, lib2Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tdump, err := client.FunctionDump(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(dump).NotTo(BeEmpty())\n\n\t\t\terr = client.FunctionRestore(ctx, dump).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\n\t\t\terr = client.FunctionFlush(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tlist, err := client.FunctionList(ctx, q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(list).To(HaveLen(0))\n\n\t\t\terr = client.FunctionRestore(ctx, dump).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tlist, err = client.FunctionList(ctx, q).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(list).To(HaveLen(2))\n\t\t})\n\n\t\tIt(\"Calls a function\", func() {\n\t\t\tlib1Code = fmt.Sprintf(lib1.Code, lib1.Name, lib1.Functions[0].Name,\n\t\t\t\tlib1.Functions[0].Description, lib1.Functions[0].Flags[0], lib1.Functions[0].Flags[1])\n\n\t\t\terr := client.FunctionLoad(ctx, lib1Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tx := client.FCall(ctx, lib1.Functions[0].Name, []string{\"my_hash\"}, \"a\", 1, \"b\", 2)\n\t\t\tExpect(x.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(x.Int()).To(Equal(3))\n\t\t})\n\n\t\tIt(\"Calls a function as read-only\", func() {\n\t\t\tlib1Code = fmt.Sprintf(lib1.Code, lib1.Name, lib1.Functions[0].Name,\n\t\t\t\tlib1.Functions[0].Description, lib1.Functions[0].Flags[0], lib1.Functions[0].Flags[1])\n\n\t\t\terr := client.FunctionLoad(ctx, lib1Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// This function doesn't have a \"no-writes\" flag\n\t\t\tx := client.FCallRo(ctx, lib1.Functions[0].Name, []string{\"my_hash\"}, \"a\", 1, \"b\", 2)\n\n\t\t\tExpect(x.Err()).To(HaveOccurred())\n\n\t\t\tlib2Code = fmt.Sprintf(lib2.Code, lib2.Name, lib2.Functions[0].Name, lib2.Functions[1].Name,\n\t\t\t\tlib2.Functions[1].Description, lib2.Functions[1].Flags[0])\n\n\t\t\t// This function has a \"no-writes\" flag\n\t\t\terr = client.FunctionLoad(ctx, lib2Code).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tx = client.FCallRo(ctx, lib2.Functions[1].Name, []string{})\n\n\t\t\tExpect(x.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(x.Text()).To(Equal(\"Function 2\"))\n\t\t})\n\n\t\tIt(\"Shows function stats\", func() {\n\t\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t\tdefer client.FunctionKill(ctx)\n\n\t\t\t// We can not run blocking commands in Redis functions, so we're using an infinite loop,\n\t\t\t// but we're killing the function after calling FUNCTION STATS\n\t\t\tlib := redis.Library{\n\t\t\t\tName:   \"mylib1\",\n\t\t\t\tEngine: \"LUA\",\n\t\t\t\tFunctions: []redis.Function{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:        \"lib1_func1\",\n\t\t\t\t\t\tDescription: \"This is the func-1 of lib 1\",\n\t\t\t\t\t\tFlags:       []string{\"no-writes\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCode: `#!lua name=%s\n\t\t\t\t\tlocal function f1(keys, args)\n\t\t\t\t\t\tlocal i = 0\n\t\t\t\t\t   \twhile true do\n\t\t\t\t\t\t\ti = i + 1\n\t\t\t\t\t   \tend\n\t\t\t\t\tend\n\n\t\t\t\t\tredis.register_function{\n\t\t\t\t\t\tfunction_name='%s',\n\t\t\t\t\t\tdescription ='%s',\n\t\t\t\t\t\tcallback=f1,\n\t\t\t\t\t\tflags={'%s'}\n\t\t\t\t\t}`,\n\t\t\t}\n\t\t\tlibCode := fmt.Sprintf(lib.Code, lib.Name, lib.Functions[0].Name,\n\t\t\t\tlib.Functions[0].Description, lib.Functions[0].Flags[0])\n\t\t\terr := client.FunctionLoad(ctx, libCode).Err()\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tr, err := client.FunctionStats(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(r.Engines)).To(Equal(1))\n\t\t\tExpect(r.Running()).To(BeFalse())\n\n\t\t\tstarted := make(chan bool)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tclient2 := redis.NewClient(redisOptions())\n\n\t\t\t\tstarted <- true\n\t\t\t\t_, err = client2.FCall(ctx, lib.Functions[0].Name, nil).Result()\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t}()\n\n\t\t\t<-started\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tr, err = client.FunctionStats(ctx).Result()\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(r.Engines)).To(Equal(1))\n\t\t\trs, isRunning := r.RunningScript()\n\t\t\tExpect(isRunning).To(BeTrue())\n\t\t\tExpect(rs.Name).To(Equal(lib.Functions[0].Name))\n\t\t\tExpect(rs.Duration > 0).To(BeTrue())\n\n\t\t\tclose(started)\n\t\t})\n\t})\n\n\tDescribe(\"SlowLog\", func() {\n\t\tIt(\"returns slow query result\", func() {\n\t\t\tconst key = \"slowlog-log-slower-than\"\n\n\t\t\told := client.ConfigGet(ctx, key).Val()\n\t\t\tclient.ConfigSet(ctx, key, \"0\")\n\t\t\tdefer client.ConfigSet(ctx, key, old[key])\n\n\t\t\terr := client.Do(ctx, \"slowlog\", \"reset\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tclient.Set(ctx, \"test\", \"true\", 0)\n\n\t\t\tresult, err := client.SlowLogGet(ctx, -1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(result)).NotTo(BeZero())\n\t\t})\n\n\t\tIt(\"returns the number of slow queries\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\t// Reset slowlog\n\t\t\terr := client.SlowLogReset(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tconst key = \"slowlog-log-slower-than\"\n\n\t\t\told := client.ConfigGet(ctx, key).Val()\n\t\t\t// first slowlog entry is the config set command itself\n\t\t\tclient.ConfigSet(ctx, key, \"0\")\n\t\t\tdefer client.ConfigSet(ctx, key, old[key])\n\n\t\t\t// Set a key to trigger a slow query, and this is the second slowlog entry\n\t\t\tclient.Set(ctx, \"test\", \"true\", 0)\n\t\t\tresult, err := client.SlowLogLen(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(result).Should(Equal(int64(2)))\n\n\t\t\t// Reset slowlog\n\t\t\terr = client.SlowLogReset(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Check if slowlog is empty, this is the first slowlog entry after reset\n\t\t\tresult, err = client.SlowLogLen(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(result).Should(Equal(int64(1)))\n\t\t})\n\t})\n\n\tDescribe(\"Latency\", Label(\"NonRedisEnterprise\"), func() {\n\t\tIt(\"returns latencies\", func() {\n\t\t\tconst key = \"latency-monitor-threshold\"\n\n\t\t\t// reset all latencies first to ensure clean state\n\t\t\terr := client.LatencyReset(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\told := client.ConfigGet(ctx, key).Val()\n\t\t\tclient.ConfigSet(ctx, key, \"1\")\n\t\t\tdefer client.ConfigSet(ctx, key, old[key])\n\n\t\t\terr = client.Do(ctx, \"DEBUG\", \"SLEEP\", 0.01).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tresult, err := client.Latency(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(result)).NotTo(BeZero())\n\t\t})\n\n\t\tIt(\"reset all latencies\", func() {\n\t\t\tconst key = \"latency-monitor-threshold\"\n\n\t\t\tresult, err := client.Latency(ctx).Result()\n\t\t\t// reset all latencies\n\t\t\terr = client.LatencyReset(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\told := client.ConfigGet(ctx, key).Val()\n\t\t\tclient.ConfigSet(ctx, key, \"1\")\n\t\t\tdefer client.ConfigSet(ctx, key, old[key])\n\n\t\t\t// get latency after reset\n\t\t\tresult, err = client.Latency(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(result)).Should(Equal(0))\n\n\t\t\t// create a new latency\n\t\t\terr = client.Do(ctx, \"DEBUG\", \"SLEEP\", 0.01).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// get latency after create a new latency\n\t\t\tresult, err = client.Latency(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(result)).Should(Equal(1))\n\n\t\t\t// reset all latencies again\n\t\t\terr = client.LatencyReset(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// get latency after reset again\n\t\t\tresult, err = client.Latency(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(result)).Should(Equal(0))\n\t\t})\n\n\t\tIt(\"reset latencies by add event name args\", func() {\n\t\t\tconst key = \"latency-monitor-threshold\"\n\n\t\t\t// reset all latencies first to ensure clean state\n\t\t\terr := client.LatencyReset(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\told := client.ConfigGet(ctx, key).Val()\n\t\t\t// Use a higher threshold (100ms) to avoid capturing normal operations\n\t\t\t// that could cause flakiness due to timing variations\n\t\t\tclient.ConfigSet(ctx, key, \"100\")\n\t\t\tdefer client.ConfigSet(ctx, key, old[key])\n\n\t\t\tresult, err := client.Latency(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(result)).Should(Equal(0))\n\n\t\t\t// Use a longer sleep (150ms) to ensure it exceeds the 100ms threshold\n\t\t\terr = client.Do(ctx, \"DEBUG\", \"SLEEP\", 0.15).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tresult, err = client.Latency(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(result)).Should(BeNumerically(\">=\", 1))\n\n\t\t\t// reset latency by event name\n\t\t\teventName := result[0].Name\n\t\t\terr = client.LatencyReset(ctx, eventName).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Verify the specific event was reset (not that all events are gone)\n\t\t\t// This avoids flakiness from other operations triggering latency events\n\t\t\tresult, err = client.Latency(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tfor _, event := range result {\n\t\t\t\tif event.Name == eventName {\n\t\t\t\t\tFail(\"Event \" + eventName + \" should have been reset\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n})\n\ntype numberStruct struct {\n\tNumber int\n}\n\nfunc (n numberStruct) MarshalBinary() ([]byte, error) {\n\treturn json.Marshal(n)\n}\n\nfunc (n *numberStruct) UnmarshalBinary(b []byte) error {\n\treturn json.Unmarshal(b, n)\n}\n\nfunc (n *numberStruct) ScanRedis(str string) error {\n\treturn json.Unmarshal([]byte(str), n)\n}\n\nfunc deref(viface interface{}) interface{} {\n\tv := reflect.ValueOf(viface)\n\tfor v.Kind() == reflect.Ptr {\n\t\tv = v.Elem()\n\t}\n\treturn v.Interface()\n}\n"
  },
  {
    "path": "dial_retry_backoff.go",
    "content": "package redis\n\nimport (\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n)\n\n// DialRetryBackoffConstant returns a dial retry backoff function that always returns d.\n// attempt is 0-based: attempt=0 is the delay after the 1st failed dial.\nfunc DialRetryBackoffConstant(d time.Duration) func(attempt int) time.Duration {\n\tif d < 0 {\n\t\td = 0\n\t}\n\treturn func(int) time.Duration { return d }\n}\n\n// DialRetryBackoffExponential returns a dial retry backoff function that uses exponential\n// backoff with jitter and a cap, using internal.RetryBackoff.\n//\n// attempt is 0-based: attempt=0 is the delay after the 1st failed dial.\nfunc DialRetryBackoffExponential(minBackoff, maxBackoff time.Duration) func(attempt int) time.Duration {\n\tif minBackoff < 0 {\n\t\tminBackoff = 0\n\t}\n\tif maxBackoff < 0 {\n\t\tmaxBackoff = 0\n\t}\n\tif minBackoff > maxBackoff {\n\t\tminBackoff = maxBackoff\n\t}\n\treturn func(attempt int) time.Duration {\n\t\t// internal.RetryBackoff expects retry >= 0.\n\t\tif attempt < 0 {\n\t\t\tattempt = 0\n\t\t}\n\t\treturn internal.RetryBackoff(attempt, minBackoff, maxBackoff)\n\t}\n}\n"
  },
  {
    "path": "digest_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/helper\"\n)\n\nfunc init() {\n\t// Initialize RedisVersion from environment variable for regular Go tests\n\t// (Ginkgo tests initialize this in BeforeSuite)\n\tif version := os.Getenv(\"REDIS_VERSION\"); version != \"\" {\n\t\tif v, err := strconv.ParseFloat(strings.Trim(version, \"\\\"\"), 64); err == nil && v > 0 {\n\t\t\tRedisVersion = v\n\t\t}\n\t}\n}\n\n// skipIfRedisBelow84 checks if Redis version is below 8.4 and skips the test if so\nfunc skipIfRedisBelow84(t *testing.T) {\n\tif RedisVersion < 8.4 {\n\t\tt.Skipf(\"Skipping test: Redis version %.1f < 8.4 (DIGEST command requires Redis 8.4+)\", RedisVersion)\n\t}\n}\n\n// TestDigestBasic validates that the Digest command returns a uint64 value\nfunc TestDigestBasic(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\tclient.Del(ctx, \"digest-test-key\")\n\n\t// Set a value\n\terr := client.Set(ctx, \"digest-test-key\", \"testvalue\", 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to set value: %v\", err)\n\t}\n\n\t// Get digest\n\tdigestCmd := client.Digest(ctx, \"digest-test-key\")\n\tif err := digestCmd.Err(); err != nil {\n\t\tt.Fatalf(\"Failed to get digest: %v\", err)\n\t}\n\n\tdigest := digestCmd.Val()\n\tif digest == 0 {\n\t\tt.Error(\"Digest should not be zero for non-empty value\")\n\t}\n\n\tt.Logf(\"Digest for 'testvalue': %d (0x%016x)\", digest, digest)\n\n\t// Verify same value produces same digest\n\tdigest2 := client.Digest(ctx, \"digest-test-key\").Val()\n\tif digest != digest2 {\n\t\tt.Errorf(\"Same value should produce same digest: %d != %d\", digest, digest2)\n\t}\n\n\tclient.Del(ctx, \"digest-test-key\")\n}\n\n// TestSetIFDEQWithDigest validates the SetIFDEQ command works with digests\nfunc TestSetIFDEQWithDigest(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\tclient.Del(ctx, \"cas-test-key\")\n\n\t// Set initial value\n\tinitialValue := \"initial-value\"\n\terr := client.Set(ctx, \"cas-test-key\", initialValue, 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to set initial value: %v\", err)\n\t}\n\n\t// Get current digest\n\tcorrectDigest := client.Digest(ctx, \"cas-test-key\").Val()\n\twrongDigest := uint64(12345) // arbitrary wrong digest\n\n\t// Test 1: SetIFDEQ with correct digest should succeed\n\tresult := client.SetIFDEQ(ctx, \"cas-test-key\", \"new-value\", correctDigest, 0)\n\tif err := result.Err(); err != nil {\n\t\tt.Errorf(\"SetIFDEQ with correct digest failed: %v\", err)\n\t} else {\n\t\tt.Logf(\"✓ SetIFDEQ with correct digest succeeded\")\n\t}\n\n\t// Verify value was updated\n\tval, err := client.Get(ctx, \"cas-test-key\").Result()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get value: %v\", err)\n\t}\n\tif val != \"new-value\" {\n\t\tt.Errorf(\"Value not updated: got %q, want %q\", val, \"new-value\")\n\t}\n\n\t// Test 2: SetIFDEQ with wrong digest should fail\n\tresult = client.SetIFDEQ(ctx, \"cas-test-key\", \"another-value\", wrongDigest, 0)\n\tif result.Err() != redis.Nil {\n\t\tt.Errorf(\"SetIFDEQ with wrong digest should return redis.Nil, got: %v\", result.Err())\n\t} else {\n\t\tt.Logf(\"✓ SetIFDEQ with wrong digest correctly failed\")\n\t}\n\n\t// Verify value was NOT updated\n\tval, err = client.Get(ctx, \"cas-test-key\").Result()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get value: %v\", err)\n\t}\n\tif val != \"new-value\" {\n\t\tt.Errorf(\"Value should not have changed: got %q, want %q\", val, \"new-value\")\n\t}\n\n\tclient.Del(ctx, \"cas-test-key\")\n}\n\n// TestSetIFDNEWithDigest validates the SetIFDNE command works with digests\nfunc TestSetIFDNEWithDigest(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\tclient.Del(ctx, \"cad-test-key\")\n\n\t// Set initial value\n\tinitialValue := \"initial-value\"\n\terr := client.Set(ctx, \"cad-test-key\", initialValue, 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to set initial value: %v\", err)\n\t}\n\n\t// Use an arbitrary different digest\n\twrongDigest := uint64(99999) // arbitrary different digest\n\n\t// Test 1: SetIFDNE with different digest should succeed\n\tresult := client.SetIFDNE(ctx, \"cad-test-key\", \"new-value\", wrongDigest, 0)\n\tif err := result.Err(); err != nil {\n\t\tt.Errorf(\"SetIFDNE with different digest failed: %v\", err)\n\t} else {\n\t\tt.Logf(\"✓ SetIFDNE with different digest succeeded\")\n\t}\n\n\t// Verify value was updated\n\tval, err := client.Get(ctx, \"cad-test-key\").Result()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get value: %v\", err)\n\t}\n\tif val != \"new-value\" {\n\t\tt.Errorf(\"Value not updated: got %q, want %q\", val, \"new-value\")\n\t}\n\n\t// Test 2: SetIFDNE with matching digest should fail\n\tnewDigest := client.Digest(ctx, \"cad-test-key\").Val()\n\tresult = client.SetIFDNE(ctx, \"cad-test-key\", \"another-value\", newDigest, 0)\n\tif result.Err() != redis.Nil {\n\t\tt.Errorf(\"SetIFDNE with matching digest should return redis.Nil, got: %v\", result.Err())\n\t} else {\n\t\tt.Logf(\"✓ SetIFDNE with matching digest correctly failed\")\n\t}\n\n\t// Verify value was NOT updated\n\tval, err = client.Get(ctx, \"cad-test-key\").Result()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get value: %v\", err)\n\t}\n\tif val != \"new-value\" {\n\t\tt.Errorf(\"Value should not have changed: got %q, want %q\", val, \"new-value\")\n\t}\n\n\tclient.Del(ctx, \"cad-test-key\")\n}\n\n// TestDelExArgsWithDigest validates DelExArgs works with digest matching\nfunc TestDelExArgsWithDigest(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\tclient.Del(ctx, \"del-test-key\")\n\n\t// Set a value\n\tvalue := \"delete-me\"\n\terr := client.Set(ctx, \"del-test-key\", value, 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to set value: %v\", err)\n\t}\n\n\t// Get correct digest\n\tcorrectDigest := client.Digest(ctx, \"del-test-key\").Val()\n\twrongDigest := uint64(54321)\n\n\t// Test 1: Delete with wrong digest should fail\n\tdeleted := client.DelExArgs(ctx, \"del-test-key\", redis.DelExArgs{\n\t\tMode:        \"IFDEQ\",\n\t\tMatchDigest: wrongDigest,\n\t}).Val()\n\n\tif deleted != 0 {\n\t\tt.Errorf(\"Delete with wrong digest should not delete: got %d deletions\", deleted)\n\t} else {\n\t\tt.Logf(\"✓ DelExArgs with wrong digest correctly refused to delete\")\n\t}\n\n\t// Verify key still exists\n\texists := client.Exists(ctx, \"del-test-key\").Val()\n\tif exists != 1 {\n\t\tt.Errorf(\"Key should still exist after failed delete\")\n\t}\n\n\t// Test 2: Delete with correct digest should succeed\n\tdeleted = client.DelExArgs(ctx, \"del-test-key\", redis.DelExArgs{\n\t\tMode:        \"IFDEQ\",\n\t\tMatchDigest: correctDigest,\n\t}).Val()\n\n\tif deleted != 1 {\n\t\tt.Errorf(\"Delete with correct digest should delete: got %d deletions\", deleted)\n\t} else {\n\t\tt.Logf(\"✓ DelExArgs with correct digest successfully deleted\")\n\t}\n\n\t// Verify key was deleted\n\texists = client.Exists(ctx, \"del-test-key\").Val()\n\tif exists != 0 {\n\t\tt.Errorf(\"Key should not exist after successful delete\")\n\t}\n}\n\n// TestDigestHelperMatchesRedis validates that helper.DigestString produces\n// the same digest as Redis DIGEST command\nfunc TestDigestHelperMatchesRedis(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\ttestCases := []struct {\n\t\tname  string\n\t\tvalue string\n\t}{\n\t\t{\"simple_string\", \"hello world\"},\n\t\t{\"empty_string\", \"\"},\n\t\t{\"single_char\", \"a\"},\n\t\t{\"numeric_string\", \"12345\"},\n\t\t{\"special_chars\", \"!@#$%^&*()\"},\n\t\t{\"unicode\", \"こんにちは世界\"},\n\t\t{\"json_like\", `{\"key\": \"value\", \"number\": 123}`},\n\t\t{\"long_string\", strings.Repeat(\"abcdefghij\", 100)}, // 1000 chars\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tkey := \"helper-test-\" + tc.name\n\n\t\t\t// Set value in Redis\n\t\t\terr := client.Set(ctx, key, tc.value, 0).Err()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to set value: %v\", err)\n\t\t\t}\n\n\t\t\t// Get digest from Redis\n\t\t\tredisDigest := client.Digest(ctx, key).Val()\n\n\t\t\t// Calculate digest using helper\n\t\t\thelperDigest := helper.DigestString(tc.value)\n\n\t\t\t// Compare\n\t\t\tif redisDigest != helperDigest {\n\t\t\t\tt.Errorf(\"Digest mismatch for %q:\\n  Redis:  0x%016x\\n  Helper: 0x%016x\",\n\t\t\t\t\ttc.value, redisDigest, helperDigest)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"✓ %s: Redis and helper digests match (0x%016x)\", tc.name, redisDigest)\n\t\t\t}\n\n\t\t\t// Cleanup\n\t\t\tclient.Del(ctx, key)\n\t\t})\n\t}\n}\n\n// TestDigestBytesHelperMatchesRedis validates that helper.DigestBytes produces\n// the same digest as Redis DIGEST command for binary data\nfunc TestDigestBytesHelperMatchesRedis(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\ttestCases := []struct {\n\t\tname  string\n\t\tvalue []byte\n\t}{\n\t\t{\"simple_bytes\", []byte(\"hello world\")},\n\t\t{\"empty_bytes\", []byte{}},\n\t\t{\"binary_data\", []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}},\n\t\t{\"jpeg_header\", []byte{0xFF, 0xD8, 0xFF, 0xE0}},\n\t\t{\"null_bytes\", []byte{0x00, 0x00, 0x00, 0x00}},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tkey := \"helper-bytes-test-\" + tc.name\n\n\t\t\t// Set value in Redis\n\t\t\terr := client.Set(ctx, key, tc.value, 0).Err()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to set value: %v\", err)\n\t\t\t}\n\n\t\t\t// Get digest from Redis\n\t\t\tredisDigest := client.Digest(ctx, key).Val()\n\n\t\t\t// Calculate digest using helper\n\t\t\thelperDigest := helper.DigestBytes(tc.value)\n\n\t\t\t// Compare\n\t\t\tif redisDigest != helperDigest {\n\t\t\t\tt.Errorf(\"Digest mismatch for binary data %v:\\n  Redis:  0x%016x\\n  Helper: 0x%016x\",\n\t\t\t\t\ttc.value, redisDigest, helperDigest)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"✓ %s: Redis and helper digests match (0x%016x)\", tc.name, redisDigest)\n\t\t\t}\n\n\t\t\t// Cleanup\n\t\t\tclient.Del(ctx, key)\n\t\t})\n\t}\n}\n\n// TestDigestHelperWithSetIFDEQ validates end-to-end optimistic locking using\n// client-side digest calculation\nfunc TestDigestHelperWithSetIFDEQ(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\tkey := \"helper-setifdeq-test\"\n\tclient.Del(ctx, key)\n\n\tinitialValue := \"version-1\"\n\terr := client.Set(ctx, key, initialValue, 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to set initial value: %v\", err)\n\t}\n\n\tclientDigest := helper.DigestString(initialValue)\n\tt.Logf(\"Client-calculated digest for %q: 0x%016x\", initialValue, clientDigest)\n\n\t// Use client-side digest for SetIFDEQ\n\tnewValue := \"version-2\"\n\tresult := client.SetIFDEQ(ctx, key, newValue, clientDigest, 0)\n\tif err := result.Err(); err != nil {\n\t\tt.Errorf(\"SetIFDEQ with client-calculated digest failed: %v\", err)\n\t} else {\n\t\tt.Logf(\"✓ SetIFDEQ with client-calculated digest succeeded\")\n\t}\n\n\t// Verify value was updated\n\tval, err := client.Get(ctx, key).Result()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get value: %v\", err)\n\t}\n\tif val != newValue {\n\t\tt.Errorf(\"Value not updated: got %q, want %q\", val, newValue)\n\t}\n\n\t// Now try with wrong client-calculated digest (should fail)\n\twrongDigest := helper.DigestString(\"wrong-value\")\n\tresult = client.SetIFDEQ(ctx, key, \"version-3\", wrongDigest, 0)\n\tif result.Err() != redis.Nil {\n\t\tt.Errorf(\"SetIFDEQ with wrong client digest should fail, got: %v\", result.Err())\n\t} else {\n\t\tt.Logf(\"✓ SetIFDEQ with wrong client-calculated digest correctly failed\")\n\t}\n\n\tclient.Del(ctx, key)\n}\n\n// TestDigestHelperWithDelExArgs validates conditional delete using\n// client-side digest calculation\nfunc TestDigestHelperWithDelExArgs(t *testing.T) {\n\tskipIfRedisBelow84(t)\n\n\tctx := context.Background()\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer client.Close()\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Skipf(\"Redis not available: %v\", err)\n\t}\n\n\tkey := \"helper-delexargs-test\"\n\tclient.Del(ctx, key)\n\n\t// Set value\n\tvalue := \"delete-me-please\"\n\terr := client.Set(ctx, key, value, 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to set value: %v\", err)\n\t}\n\n\t// Calculate digest client-side\n\tclientDigest := helper.DigestString(value)\n\tt.Logf(\"Client-calculated digest: 0x%016x\", clientDigest)\n\n\t// Try to delete with wrong digest (should fail)\n\twrongDigest := helper.DigestString(\"wrong\")\n\tdeleted := client.DelExArgs(ctx, key, redis.DelExArgs{\n\t\tMode:        \"IFDEQ\",\n\t\tMatchDigest: wrongDigest,\n\t}).Val()\n\n\tif deleted != 0 {\n\t\tt.Errorf(\"Delete with wrong client digest should fail\")\n\t} else {\n\t\tt.Logf(\"✓ DelExArgs with wrong client digest correctly refused\")\n\t}\n\n\t// Delete with correct client-calculated digest (should succeed)\n\tdeleted = client.DelExArgs(ctx, key, redis.DelExArgs{\n\t\tMode:        \"IFDEQ\",\n\t\tMatchDigest: clientDigest,\n\t}).Val()\n\n\tif deleted != 1 {\n\t\tt.Errorf(\"Delete with correct client digest should succeed\")\n\t} else {\n\t\tt.Logf(\"✓ DelExArgs with client-calculated digest succeeded\")\n\t}\n\n\t// Verify deletion\n\texists := client.Exists(ctx, key).Val()\n\tif exists != 0 {\n\t\tt.Errorf(\"Key should be deleted\")\n\t}\n}\n"
  },
  {
    "path": "doc.go",
    "content": "/*\nPackage redis implements a Redis client.\n*/\npackage redis\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "---\n\nx-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.6.0}\n\nservices:\n  redis:\n    image: *default-image\n    platform: linux/amd64\n    container_name: redis-standalone\n    environment:\n      - TLS_ENABLED=yes\n      - TLS_CLIENT_CNS=testcertuser\n      - TLS_AUTH_CLIENTS_USER=CN\n      - REDIS_CLUSTER=no\n      - PORT=6379\n      - TLS_PORT=6666\n    command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save \"\"}\n    ports:\n      - 6379:6379\n      - 6666:6666 # TLS port\n    volumes:\n      - \"./dockers/standalone:/redis/work\"\n    profiles:\n      - standalone\n      - sentinel\n      - all-stack\n      - all\n      - e2e\n\n  osscluster:\n    image: *default-image\n    platform: linux/amd64\n    container_name: redis-osscluster\n    environment:\n      - NODES=6\n      - PORT=16600\n    command: \"--cluster-enabled yes\"\n    ports:\n      - \"16600-16605:16600-16605\"\n    volumes:\n      - \"./dockers/osscluster:/redis/work\"\n    profiles:\n      - cluster\n      - all-stack\n      - all\n\n  cae-resp-proxy:\n    image: redislabs/client-resp-proxy:latest\n    container_name: cae-resp-proxy\n    environment:\n      - TARGET_HOST=redis\n      - TARGET_PORT=6379\n      - LISTEN_PORT=17000,17001,17002,17003  # 4 proxy nodes: initially show 3, swap in 4th during SMIGRATED\n      - LISTEN_HOST=0.0.0.0\n      - API_PORT=3000\n      - DEFAULT_INTERCEPTORS=cluster,hitless\n    ports:\n      - \"17000:17000\"  # Proxy node 1 (host:container)\n      - \"17001:17001\"  # Proxy node 2 (host:container)\n      - \"17002:17002\"  # Proxy node 3 (host:container)\n      - \"17003:17003\"  # Proxy node 4 (host:container) - hidden initially, swapped in during SMIGRATED\n      - \"18100:3000\"  # HTTP API port (host:container)\n    depends_on:\n      - redis\n    profiles:\n      - e2e\n      - all\n\n  proxy-fault-injector:\n    build:\n      context: .\n      dockerfile: maintnotifications/e2e/cmd/proxy-fi-server/Dockerfile\n    container_name: proxy-fault-injector\n    ports:\n      - \"15000:5000\"  # Fault injector API port (host:container)\n    depends_on:\n      - cae-resp-proxy\n    environment:\n      - PROXY_API_URL=http://cae-resp-proxy:3000\n    profiles:\n      - e2e\n      - all\n\n  osscluster-tls:\n    image: *default-image\n    platform: linux/amd64\n    container_name: redis-osscluster-tls\n    environment:\n      - NODES=6\n      - PORT=6430\n      - TLS_PORT=5430\n      - TLS_ENABLED=yes\n      - TLS_CLIENT_CNS=testcertuser\n      - TLS_AUTH_CLIENTS_USER=CN\n      - REDIS_CLUSTER=yes\n      - REPLICAS=1\n    command: \"--tls-auth-clients optional --cluster-announce-ip 127.0.0.1\"\n    ports:\n      - \"6430-6435:6430-6435\"      # Regular ports\n      - \"5430-5435:5430-5435\"      # TLS ports (set via TLS_PORT env var)\n      - \"16430-16435:16430-16435\"  # Cluster bus ports (PORT + 10000)\n    volumes:\n      - \"./dockers/osscluster-tls:/redis/work\"\n    profiles:\n      - cluster-tls\n      - all\n\n  sentinel-cluster:\n    image: *default-image\n    platform: linux/amd64\n    container_name: redis-sentinel-cluster\n    network_mode: \"host\"\n    environment:\n      - NODES=3\n      - TLS_ENABLED=yes\n      - TLS_CLIENT_CNS=testcertuser\n      - TLS_AUTH_CLIENTS_USER=CN\n      - REDIS_CLUSTER=no\n      - PORT=9121\n    command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save \"\"}\n    #ports:\n    #  - \"9121-9123:9121-9123\"\n    volumes:\n      - \"./dockers/sentinel-cluster:/redis/work\"\n    profiles:\n      - sentinel\n      - all-stack\n      - all\n\n  sentinel:\n    image: *default-image\n    platform: linux/amd64\n    container_name: redis-sentinel\n    depends_on:\n      - sentinel-cluster\n    environment:\n      - NODES=3\n      - REDIS_CLUSTER=no\n      - PORT=26379\n    command: ${REDIS_EXTRA_ARGS:---sentinel}\n    network_mode: \"host\"\n    #ports:\n    #  - 26379:26379\n    #  - 26380:26380\n    #  - 26381:26381\n    volumes:\n      - \"./dockers/sentinel.conf:/redis/config-default/redis.conf\"\n      - \"./dockers/sentinel:/redis/work\"\n    profiles:\n      - sentinel\n      - all-stack\n      - all\n\n  ring-cluster:\n    image: *default-image\n    platform: linux/amd64\n    container_name: redis-ring-cluster\n    environment:\n      - NODES=3\n      - TLS_ENABLED=yes\n      - TLS_CLIENT_CNS=testcertuser\n      - TLS_AUTH_CLIENTS_USER=CN\n      - REDIS_CLUSTER=no\n      - PORT=6390\n    command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save \"\"}\n    ports:\n      - 6390:6390\n      - 6391:6391\n      - 6392:6392\n    volumes:\n      - \"./dockers/ring:/redis/work\"\n    profiles:\n      - ring\n      - cluster\n      - all-stack\n      - all\n"
  },
  {
    "path": "dockers/.gitignore",
    "content": "osscluster/\nring/\nstandalone/\nsentinel-cluster/\nsentinel/\n\n"
  },
  {
    "path": "dockers/sentinel.conf",
    "content": "sentinel resolve-hostnames yes\nsentinel monitor go-redis-test 127.0.0.1 9121 2\nsentinel down-after-milliseconds go-redis-test 5000\nsentinel failover-timeout go-redis-test 60000\nsentinel parallel-syncs go-redis-test 1"
  },
  {
    "path": "doctests/Makefile",
    "content": "test:\n\t@if [ -z \"$(REDIS_VERSION)\" ]; then \\\n\t\techo \"REDIS_VERSION not set, running all tests\"; \\\n\t\tgo test -v ./...; \\\n\telse \\\n\t\tMAJOR_VERSION=$$(echo \"$(REDIS_VERSION)\" | cut -d. -f1); \\\n\t\tif [ \"$$MAJOR_VERSION\" -ge 8 ]; then \\\n\t\t\techo \"REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests\"; \\\n\t\t\tgo test -v ./...; \\\n\t\telse \\\n\t\t\techo \"REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests\"; \\\n\t\t\tgo test -v ./... -run '^(?!.*(?:vectorset|ExampleClient_vectorset)).*$$'; \\\n\t\tfi; \\\n\tfi\n\n.PHONY: test"
  },
  {
    "path": "doctests/README.md",
    "content": "# Command examples for redis.io\n\nThese examples appear on the [Redis documentation](https://redis.io) site as part of the tabbed examples interface.\n\n## How to add examples\n\n- Create a Go test file with a meaningful name in the current folder. \n- Create a single method prefixed with `Example` and write your test in it.\n- Determine the id for the example you're creating and add it as the first line of the file: `// EXAMPLE: set_and_get`.\n- We're using the [Testable Examples](https://go.dev/blog/examples) feature of Go to test the desired output has been written to stdout.\n\n### Special markup\n\nSee https://github.com/redis-stack/redis-stack-website#readme for more details.\n\n## How to test the examples\n\n- Start a Redis server locally on port 6379\n- CD into the `doctests` directory\n- Run `go test` to test all examples in the directory.\n- Run `go test filename.go` to test a single file\n\n"
  },
  {
    "path": "doctests/bf_tutorial_test.go",
    "content": "// EXAMPLE: bf_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_bloom() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:models\")\n\t// REMOVE_END\n\n\t// STEP_START bloom\n\tres1, err := rdb.BFReserve(ctx, \"bikes:models\", 0.01, 1000).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.BFAdd(ctx, \"bikes:models\", \"Smoky Mountain Striker\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> true\n\n\tres3, err := rdb.BFExists(ctx, \"bikes:models\", \"Smoky Mountain Striker\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> true\n\n\tres4, err := rdb.BFMAdd(ctx, \"bikes:models\",\n\t\t\"Rocky Mountain Racer\",\n\t\t\"Cloudy City Cruiser\",\n\t\t\"Windy City Wippet\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> [true true true]\n\n\tres5, err := rdb.BFMExists(ctx, \"bikes:models\",\n\t\t\"Rocky Mountain Racer\",\n\t\t\"Cloudy City Cruiser\",\n\t\t\"Windy City Wippet\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> [true true true]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// true\n\t// true\n\t// [true true true]\n\t// [true true true]\n}\n"
  },
  {
    "path": "doctests/bitfield_tutorial_test.go",
    "content": "// EXAMPLE: bitfield_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_bf() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike:1:stats\")\n\t// REMOVE_END\n\n\t// STEP_START bf\n\tres1, err := rdb.BitField(ctx, \"bike:1:stats\",\n\t\t\"set\", \"u32\", \"#0\", \"1000\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> [0]\n\n\tres2, err := rdb.BitField(ctx,\n\t\t\"bike:1:stats\",\n\t\t\"incrby\", \"u32\", \"#0\", \"-50\",\n\t\t\"incrby\", \"u32\", \"#1\", \"1\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> [950 1]\n\n\tres3, err := rdb.BitField(ctx,\n\t\t\"bike:1:stats\",\n\t\t\"incrby\", \"u32\", \"#0\", \"500\",\n\t\t\"incrby\", \"u32\", \"#1\", \"1\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> [1450 2]\n\n\tres4, err := rdb.BitField(ctx, \"bike:1:stats\",\n\t\t\"get\", \"u32\", \"#0\",\n\t\t\"get\", \"u32\", \"#1\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> [1450 2]\n\t// STEP_END\n\n\t// Output:\n\t// [0]\n\t// [950 1]\n\t// [1450 2]\n\t// [1450 2]\n}\n"
  },
  {
    "path": "doctests/bitmap_tutorial_test.go",
    "content": "// EXAMPLE: bitmap_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_ping() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"pings:2024-01-01-00:00\")\n\t// REMOVE_END\n\n\t// STEP_START ping\n\tres1, err := rdb.SetBit(ctx, \"pings:2024-01-01-00:00\", 123, 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> 0\n\n\tres2, err := rdb.GetBit(ctx, \"pings:2024-01-01-00:00\", 123).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> 1\n\n\tres3, err := rdb.GetBit(ctx, \"pings:2024-01-01-00:00\", 456).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> 0\n\t// STEP_END\n\n\t// Output:\n\t// 0\n\t// 1\n\t// 0\n}\n\nfunc ExampleClient_bitcount() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\t_, err := rdb.SetBit(ctx, \"pings:2024-01-01-00:00\", 123, 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// REMOVE_END\n\n\t// STEP_START bitcount\n\tres4, err := rdb.BitCount(ctx, \"pings:2024-01-01-00:00\",\n\t\t&redis.BitCount{\n\t\t\tStart: 0,\n\t\t\tEnd:   456,\n\t\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> 1\n\t// STEP_END\n\n\t// Output:\n\t// 1\n}\n"
  },
  {
    "path": "doctests/cmds_generic_test.go",
    "content": "// EXAMPLE: cmds_generic\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_del_cmd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"key1\", \"key2\", \"key3\")\n\t// REMOVE_END\n\n\t// STEP_START del\n\tdelResult1, err := rdb.Set(ctx, \"key1\", \"Hello\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(delResult1) // >>> OK\n\n\tdelResult2, err := rdb.Set(ctx, \"key2\", \"World\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(delResult2) // >>> OK\n\n\tdelResult3, err := rdb.Del(ctx, \"key1\", \"key2\", \"key3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(delResult3) // >>> 2\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// 2\n}\n\nfunc ExampleClient_expire_cmd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"mykey\")\n\t// REMOVE_END\n\n\t// STEP_START expire\n\texpireResult1, err := rdb.Set(ctx, \"mykey\", \"Hello\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(expireResult1) // >>> OK\n\n\texpireResult2, err := rdb.Expire(ctx, \"mykey\", 10*time.Second).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(expireResult2) // >>> true\n\n\texpireResult3, err := rdb.TTL(ctx, \"mykey\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(math.Round(expireResult3.Seconds())) // >>> 10\n\n\texpireResult4, err := rdb.Set(ctx, \"mykey\", \"Hello World\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(expireResult4) // >>> OK\n\n\texpireResult5, err := rdb.TTL(ctx, \"mykey\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(expireResult5) // >>> -1ns\n\n\texpireResult6, err := rdb.ExpireXX(ctx, \"mykey\", 10*time.Second).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(expireResult6) // >>> false\n\n\texpireResult7, err := rdb.TTL(ctx, \"mykey\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(expireResult7) // >>> -1ns\n\n\texpireResult8, err := rdb.ExpireNX(ctx, \"mykey\", 10*time.Second).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(expireResult8) // >>> true\n\n\texpireResult9, err := rdb.TTL(ctx, \"mykey\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(math.Round(expireResult9.Seconds())) // >>> 10\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// true\n\t// 10\n\t// OK\n\t// -1ns\n\t// false\n\t// -1ns\n\t// true\n\t// 10\n}\n\nfunc ExampleClient_ttl_cmd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"mykey\")\n\t// REMOVE_END\n\n\t// STEP_START ttl\n\tttlResult1, err := rdb.Set(ctx, \"mykey\", \"Hello\", 10*time.Second).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(ttlResult1) // >>> OK\n\n\tttlResult2, err := rdb.TTL(ctx, \"mykey\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(math.Round(ttlResult2.Seconds())) // >>> 10\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// 10\n}\n"
  },
  {
    "path": "doctests/cmds_hash_test.go",
    "content": "// EXAMPLE: cmds_hash\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_hset() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myhash\")\n\t// REMOVE_END\n\n\t// STEP_START hset\n\tres1, err := rdb.HSet(ctx, \"myhash\", \"field1\", \"Hello\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> 1\n\n\tres2, err := rdb.HGet(ctx, \"myhash\", \"field1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> Hello\n\n\tres3, err := rdb.HSet(ctx, \"myhash\",\n\t\t\"field2\", \"Hi\",\n\t\t\"field3\", \"World\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> 2\n\n\tres4, err := rdb.HGet(ctx, \"myhash\", \"field2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> Hi\n\n\tres5, err := rdb.HGet(ctx, \"myhash\", \"field3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> World\n\n\tres6, err := rdb.HGetAll(ctx, \"myhash\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tkeys := make([]string, 0, len(res6))\n\n\tfor key := range res6 {\n\t\tkeys = append(keys, key)\n\t}\n\n\tsort.Strings(keys)\n\n\tfor _, key := range keys {\n\t\tfmt.Printf(\"Key: %v, value: %v\\n\", key, res6[key])\n\t}\n\t// >>> Key: field1, value: Hello\n\t// >>> Key: field2, value: Hi\n\t// >>> Key: field3, value: World\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// Hello\n\t// 2\n\t// Hi\n\t// World\n\t// Key: field1, value: Hello\n\t// Key: field2, value: Hi\n\t// Key: field3, value: World\n}\n\nfunc ExampleClient_hget() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myhash\")\n\t// REMOVE_END\n\n\t// STEP_START hget\n\tres7, err := rdb.HSet(ctx, \"myhash\", \"field1\", \"foo\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> 1\n\n\tres8, err := rdb.HGet(ctx, \"myhash\", \"field1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> foo\n\n\tres9, err := rdb.HGet(ctx, \"myhash\", \"field2\").Result()\n\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\n\tfmt.Println(res9) // >>> <empty string>\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// foo\n\t// redis: nil\n}\n\nfunc ExampleClient_hgetall() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myhash\")\n\t// REMOVE_END\n\n\t// STEP_START hgetall\n\thGetAllResult1, err := rdb.HSet(ctx, \"myhash\",\n\t\t\"field1\", \"Hello\",\n\t\t\"field2\", \"World\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(hGetAllResult1) // >>> 2\n\n\thGetAllResult2, err := rdb.HGetAll(ctx, \"myhash\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tkeys := make([]string, 0, len(hGetAllResult2))\n\n\tfor key := range hGetAllResult2 {\n\t\tkeys = append(keys, key)\n\t}\n\n\tsort.Strings(keys)\n\n\tfor _, key := range keys {\n\t\tfmt.Printf(\"Key: %v, value: %v\\n\", key, hGetAllResult2[key])\n\t}\n\t// >>> Key: field1, value: Hello\n\t// >>> Key: field2, value: World\n\t// STEP_END\n\n\t// Output:\n\t// 2\n\t// Key: field1, value: Hello\n\t// Key: field2, value: World\n}\n\nfunc ExampleClient_hvals() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myhash\")\n\t// REMOVE_END\n\n\t// STEP_START hvals\n\thValsResult1, err := rdb.HSet(ctx, \"myhash\",\n\t\t\"field1\", \"Hello\",\n\t\t\"field2\", \"World\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(hValsResult1) // >>> 2\n\n\thValsResult2, err := rdb.HVals(ctx, \"myhash\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsort.Strings(hValsResult2)\n\n\tfmt.Println(hValsResult2) // >>> [Hello World]\n\t// STEP_END\n\n\t// Output:\n\t// 2\n\t// [Hello World]\n}\n"
  },
  {
    "path": "doctests/cmds_list_test.go",
    "content": "// EXAMPLE: cmds_list\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_cmd_llen() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"mylist\")\n\t// REMOVE_END\n\n\t// STEP_START llen\n\tlPushResult1, err := rdb.LPush(ctx, \"mylist\", \"World\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lPushResult1) // >>> 1\n\n\tlPushResult2, err := rdb.LPush(ctx, \"mylist\", \"Hello\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lPushResult2) // >>> 2\n\n\tlLenResult, err := rdb.LLen(ctx, \"mylist\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lLenResult) // >>> 2\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// 2\n}\nfunc ExampleClient_cmd_lpop() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"mylist\")\n\t// REMOVE_END\n\n\t// STEP_START lpop\n\tRPushResult, err := rdb.RPush(ctx,\n\t\t\"mylist\", \"one\", \"two\", \"three\", \"four\", \"five\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(RPushResult) // >>> 5\n\n\tlPopResult, err := rdb.LPop(ctx, \"mylist\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lPopResult) // >>> one\n\n\tlPopCountResult, err := rdb.LPopCount(ctx, \"mylist\", 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lPopCountResult) // >>> [two three]\n\n\tlRangeResult, err := rdb.LRange(ctx, \"mylist\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult) // >>> [four five]\n\t// STEP_END\n\n\t// Output:\n\t// 5\n\t// one\n\t// [two three]\n\t// [four five]\n}\n\nfunc ExampleClient_cmd_lpush() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"mylist\")\n\t// REMOVE_END\n\n\t// STEP_START lpush\n\tlPushResult1, err := rdb.LPush(ctx, \"mylist\", \"World\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lPushResult1) // >>> 1\n\n\tlPushResult2, err := rdb.LPush(ctx, \"mylist\", \"Hello\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lPushResult2) // >>> 2\n\n\tlRangeResult, err := rdb.LRange(ctx, \"mylist\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult) // >>> [Hello World]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// [Hello World]\n}\n\nfunc ExampleClient_cmd_lrange() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"mylist\")\n\t// REMOVE_END\n\n\t// STEP_START lrange\n\tRPushResult, err := rdb.RPush(ctx, \"mylist\",\n\t\t\"one\", \"two\", \"three\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(RPushResult) // >>> 3\n\n\tlRangeResult1, err := rdb.LRange(ctx, \"mylist\", 0, 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult1) // >>> [one]\n\n\tlRangeResult2, err := rdb.LRange(ctx, \"mylist\", -3, 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult2) // >>> [one two three]\n\n\tlRangeResult3, err := rdb.LRange(ctx, \"mylist\", -100, 100).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult3) // >>> [one two three]\n\n\tlRangeResult4, err := rdb.LRange(ctx, \"mylist\", 5, 10).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult4) // >>> []\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// [one]\n\t// [one two three]\n\t// [one two three]\n\t// []\n}\n\nfunc ExampleClient_cmd_rpop() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"mylist\")\n\t// REMOVE_END\n\n\t// STEP_START rpop\n\trPushResult, err := rdb.RPush(ctx, \"mylist\",\n\t\t\"one\", \"two\", \"three\", \"four\", \"five\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(rPushResult) // >>> 5\n\n\trPopResult, err := rdb.RPop(ctx, \"mylist\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(rPopResult) // >>> five\n\n\trPopCountResult, err := rdb.RPopCount(ctx, \"mylist\", 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(rPopCountResult) // >>> [four three]\n\n\tlRangeResult, err := rdb.LRange(ctx, \"mylist\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult) // >>> [one two]\n\t// STEP_END\n\n\t// Output:\n\t// 5\n\t// five\n\t// [four three]\n\t// [one two]\n}\n\nfunc ExampleClient_cmd_rpush() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"mylist\")\n\t// REMOVE_END\n\n\t// STEP_START rpush\n\trPushResult1, err := rdb.RPush(ctx, \"mylist\", \"Hello\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(rPushResult1) // >>> 1\n\n\trPushResult2, err := rdb.RPush(ctx, \"mylist\", \"World\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(rPushResult2) // >>> 2\n\n\tlRangeResult, err := rdb.LRange(ctx, \"mylist\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(lRangeResult) // >>> [Hello World]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// [Hello World]\n}\n"
  },
  {
    "path": "doctests/cmds_servermgmt_test.go",
    "content": "// EXAMPLE: cmds_servermgmt\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_cmd_flushall() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// STEP_START flushall\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Set(ctx, \"testkey1\", \"1\", 0)\n\trdb.Set(ctx, \"testkey2\", \"2\", 0)\n\trdb.Set(ctx, \"testkey3\", \"3\", 0)\n\t// REMOVE_END\n\tflushAllResult1, err := rdb.FlushAll(ctx).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(flushAllResult1) // >>> OK\n\n\tflushAllResult2, err := rdb.Keys(ctx, \"*\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(flushAllResult2) // >>> []\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// []\n}\n\nfunc ExampleClient_cmd_info() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// STEP_START info\n\tinfoResult, err := rdb.Info(ctx).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Check the first 8 characters (the full info string contains\n\t// much more text than this).\n\tfmt.Println(infoResult[:8]) // >>> # Server\n\t// STEP_END\n\n\t// Output:\n\t// # Server\n}\n"
  },
  {
    "path": "doctests/cmds_set_test.go",
    "content": "// EXAMPLE: cmds_set\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_sadd_cmd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"myset\")\n\t// REMOVE_END\n\n\t// STEP_START sadd\n\tsAddResult1, err := rdb.SAdd(ctx, \"myset\", \"Hello\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(sAddResult1) // >>> 1\n\n\tsAddResult2, err := rdb.SAdd(ctx, \"myset\", \"World\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(sAddResult2) // >>> 1\n\n\tsAddResult3, err := rdb.SAdd(ctx, \"myset\", \"World\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(sAddResult3) // >>> 0\n\n\tsMembersResult, err := rdb.SMembers(ctx, \"myset\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(sMembersResult) // >>> [Hello World]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 1\n\t// 0\n\t// [Hello World]\n}\n\nfunc ExampleClient_smembers_cmd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"myset\")\n\t// REMOVE_END\n\n\t// STEP_START smembers\n\tsAddResult, err := rdb.SAdd(ctx, \"myset\", \"Hello\", \"World\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(sAddResult) // >>> 2\n\n\tsMembersResult, err := rdb.SMembers(ctx, \"myset\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(sMembersResult) // >>> [Hello World]\n\t// STEP_END\n\n\t// Output:\n\t// 2\n\t// [Hello World]\n}\n"
  },
  {
    "path": "doctests/cmds_sorted_set_test.go",
    "content": "// EXAMPLE: cmds_sorted_set\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_zadd_cmd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myzset\")\n\t// REMOVE_END\n\n\t// STEP_START zadd\n\tzAddResult1, err := rdb.ZAdd(ctx, \"myzset\",\n\t\tredis.Z{Member: \"one\", Score: 1},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zAddResult1) // >>> 1\n\n\tzAddResult2, err := rdb.ZAdd(ctx, \"myzset\",\n\t\tredis.Z{Member: \"uno\", Score: 1},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zAddResult2)\n\n\tzAddResult3, err := rdb.ZAdd(ctx, \"myzset\",\n\t\tredis.Z{Member: \"two\", Score: 2},\n\t\tredis.Z{Member: \"three\", Score: 3},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zAddResult3) // >>> 2\n\n\tzAddResult4, err := rdb.ZRangeWithScores(ctx, \"myzset\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zAddResult4) // >>> [{1 one} {1 uno} {2 two} {3 three}]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 1\n\t// 2\n\t// [{1 one} {1 uno} {2 two} {3 three}]\n}\n\nfunc ExampleClient_zrange1() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myzset\")\n\t// REMOVE_END\n\n\t// STEP_START zrange1\n\tzrangeResult1, err := rdb.ZAdd(ctx, \"myzset\",\n\t\tredis.Z{Member: \"one\", Score: 1},\n\t\tredis.Z{Member: \"two\", Score: 2},\n\t\tredis.Z{Member: \"three\", Score: 3},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zrangeResult1) // >>> 3\n\n\tzrangeResult2, err := rdb.ZRange(ctx, \"myzset\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zrangeResult2) // >>> [one two three]\n\n\tzrangeResult3, err := rdb.ZRange(ctx, \"myzset\", 2, 3).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zrangeResult3) // >>> [three]\n\n\tzrangeResult4, err := rdb.ZRange(ctx, \"myzset\", -2, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zrangeResult4) // >>> [two three]\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// [one two three]\n\t// [three]\n\t// [two three]\n}\n\nfunc ExampleClient_zrange2() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myzset\")\n\t// REMOVE_END\n\n\t// STEP_START zrange2\n\tzRangeResult5, err := rdb.ZAdd(ctx, \"myzset\",\n\t\tredis.Z{Member: \"one\", Score: 1},\n\t\tredis.Z{Member: \"two\", Score: 2},\n\t\tredis.Z{Member: \"three\", Score: 3},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zRangeResult5) // >>> 3\n\n\tzRangeResult6, err := rdb.ZRangeWithScores(ctx, \"myzset\", 0, 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zRangeResult6) // >>> [{1 one} {2 two}]\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// [{1 one} {2 two}]\n}\n\nfunc ExampleClient_zrange3() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"myzset\")\n\t// REMOVE_END\n\n\t// STEP_START zrange3\n\tzRangeResult7, err := rdb.ZAdd(ctx, \"myzset\",\n\t\tredis.Z{Member: \"one\", Score: 1},\n\t\tredis.Z{Member: \"two\", Score: 2},\n\t\tredis.Z{Member: \"three\", Score: 3},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zRangeResult7) // >>> 3\n\n\tzRangeResult8, err := rdb.ZRangeArgs(ctx,\n\t\tredis.ZRangeArgs{\n\t\t\tKey:     \"myzset\",\n\t\t\tByScore: true,\n\t\t\tStart:   \"(1\",\n\t\t\tStop:    \"+inf\",\n\t\t\tOffset:  1,\n\t\t\tCount:   1,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(zRangeResult8) // >>> [three]\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// [three]\n}\n"
  },
  {
    "path": "doctests/cmds_string_test.go",
    "content": "// EXAMPLE: cmds_string\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_cmd_incr() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"mykey\")\n\t// REMOVE_END\n\n\t// STEP_START incr\n\tincrResult1, err := rdb.Set(ctx, \"mykey\", \"10\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(incrResult1) // >>> OK\n\n\tincrResult2, err := rdb.Incr(ctx, \"mykey\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(incrResult2) // >>> 11\n\n\tincrResult3, err := rdb.Get(ctx, \"mykey\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(incrResult3) // >>> 11\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// 11\n\t// 11\n}\n"
  },
  {
    "path": "doctests/cms_tutorial_test.go",
    "content": "// EXAMPLE: cms_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_cms() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:profit\")\n\t// REMOVE_END\n\n\t// STEP_START cms\n\tres1, err := rdb.CMSInitByProb(ctx, \"bikes:profit\", 0.001, 0.002).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.CMSIncrBy(ctx, \"bikes:profit\",\n\t\t\"Smoky Mountain Striker\", 100,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> [100]\n\n\tres3, err := rdb.CMSIncrBy(ctx, \"bikes:profit\",\n\t\t\"Rocky Mountain Racer\", 200,\n\t\t\"Cloudy City Cruiser\", 150,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> [200 150]\n\n\tres4, err := rdb.CMSQuery(ctx, \"bikes:profit\",\n\t\t\"Smoky Mountain Striker\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> [100]\n\n\tres5, err := rdb.CMSInfo(ctx, \"bikes:profit\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Width: %v, Depth: %v, Count: %v\",\n\t\tres5.Width, res5.Depth, res5.Count)\n\t// >>> Width: 2000, Depth: 9, Count: 450\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [100]\n\t// [200 150]\n\t// [100]\n\t// Width: 2000, Depth: 9, Count: 450\n}\n"
  },
  {
    "path": "doctests/cuckoo_tutorial_test.go",
    "content": "// EXAMPLE: cuckoo_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_cuckoo() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:models\")\n\t// REMOVE_END\n\n\t// STEP_START cuckoo\n\tres1, err := rdb.CFReserve(ctx, \"bikes:models\", 1000000).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.CFAdd(ctx, \"bikes:models\", \"Smoky Mountain Striker\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> true\n\n\tres3, err := rdb.CFExists(ctx, \"bikes:models\", \"Smoky Mountain Striker\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> true\n\n\tres4, err := rdb.CFExists(ctx, \"bikes:models\", \"Terrible Bike Name\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> false\n\n\tres5, err := rdb.CFDel(ctx, \"bikes:models\", \"Smoky Mountain Striker\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> true\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// true\n\t// true\n\t// false\n\t// true\n}\n"
  },
  {
    "path": "doctests/geo_index_test.go",
    "content": "// EXAMPLE: geoindex\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_geoindex() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.FTDropIndex(ctx, \"productidx\")\n\trdb.FTDropIndex(ctx, \"geomidx\")\n\trdb.Del(ctx, \"product:46885\", \"product:46886\", \"shape:1\", \"shape:2\", \"shape:3\", \"shape:4\")\n\t// REMOVE_END\n\n\t// STEP_START create_geo_idx\n\tgeoCreateResult, err := rdb.FTCreate(ctx,\n\t\t\"productidx\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"product:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.location\",\n\t\t\tAs:        \"location\",\n\t\t\tFieldType: redis.SearchFieldTypeGeo,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(geoCreateResult) // >>> OK\n\t// STEP_END\n\n\t// STEP_START add_geo_json\n\tprd46885 := map[string]interface{}{\n\t\t\"description\": \"Navy Blue Slippers\",\n\t\t\"price\":       45.99,\n\t\t\"city\":        \"Denver\",\n\t\t\"location\":    \"-104.991531, 39.742043\",\n\t}\n\n\tgjResult1, err := rdb.JSONSet(ctx, \"product:46885\", \"$\", prd46885).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(gjResult1) // >>> OK\n\n\tprd46886 := map[string]interface{}{\n\t\t\"description\": \"Bright Green Socks\",\n\t\t\"price\":       25.50,\n\t\t\"city\":        \"Fort Collins\",\n\t\t\"location\":    \"-105.0618814,40.5150098\",\n\t}\n\n\tgjResult2, err := rdb.JSONSet(ctx, \"product:46886\", \"$\", prd46886).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(gjResult2) // >>> OK\n\t// STEP_END\n\n\t// STEP_START geo_query\n\tgeoQueryResult, err := rdb.FTSearch(ctx, \"productidx\",\n\t\t\"@location:[-104.800644 38.846127 100 mi]\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(geoQueryResult)\n\t// >>> {1 [{product:46885...\n\t// STEP_END\n\n\t// STEP_START create_gshape_idx\n\tgeomCreateResult, err := rdb.FTCreate(ctx, \"geomidx\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"shape:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.name\",\n\t\t\tAs:        \"name\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName:         \"$.geom\",\n\t\t\tAs:                \"geom\",\n\t\t\tFieldType:         redis.SearchFieldTypeGeoShape,\n\t\t\tGeoShapeFieldType: \"FLAT\",\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(geomCreateResult) // >>> OK\n\t// STEP_END\n\n\t// STEP_START add_gshape_json\n\tshape1 := map[string]interface{}{\n\t\t\"name\": \"Green Square\",\n\t\t\"geom\": \"POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))\",\n\t}\n\n\tgmjResult1, err := rdb.JSONSet(ctx, \"shape:1\", \"$\", shape1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(gmjResult1) // >>> OK\n\n\tshape2 := map[string]interface{}{\n\t\t\"name\": \"Red Rectangle\",\n\t\t\"geom\": \"POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))\",\n\t}\n\n\tgmjResult2, err := rdb.JSONSet(ctx, \"shape:2\", \"$\", shape2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(gmjResult2) // >>> OK\n\n\tshape3 := map[string]interface{}{\n\t\t\"name\": \"Blue Triangle\",\n\t\t\"geom\": \"POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))\",\n\t}\n\n\tgmjResult3, err := rdb.JSONSet(ctx, \"shape:3\", \"$\", shape3).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(gmjResult3) // >>> OK\n\n\tshape4 := map[string]interface{}{\n\t\t\"name\": \"Purple Point\",\n\t\t\"geom\": \"POINT (2 2)\",\n\t}\n\n\tgmjResult4, err := rdb.JSONSet(ctx, \"shape:4\", \"$\", shape4).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(gmjResult4) // >>> OK\n\t// STEP_END\n\n\t// STEP_START gshape_query\n\tgeomQueryResult, err := rdb.FTSearchWithArgs(ctx, \"geomidx\",\n\t\t\"(-@name:(Green Square) @geom:[WITHIN $qshape])\",\n\t\t&redis.FTSearchOptions{\n\t\t\tParams: map[string]interface{}{\n\t\t\t\t\"qshape\": \"POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))\",\n\t\t\t},\n\t\t\tDialectVersion: 4,\n\t\t\tLimit:          1,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(geomQueryResult)\n\t// >>> {1 [{shape:4...\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// OK\n\t// {1 [{product:46885 <nil> <nil> <nil> map[$:{\"city\":\"Denver\",\"description\":\"Navy Blue Slippers\",\"location\":\"-104.991531, 39.742043\",\"price\":45.99}] <nil>}]}\n\t// OK\n\t// OK\n\t// OK\n\t// OK\n\t// OK\n\t// {1 [{shape:4 <nil> <nil> <nil> map[$:[{\"geom\":\"POINT (2 2)\",\"name\":\"Purple Point\"}]] <nil>}]}\n}\n"
  },
  {
    "path": "doctests/geo_tutorial_test.go",
    "content": "// EXAMPLE: geo_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_geoadd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:rentable\")\n\t// REMOVE_END\n\n\t// STEP_START geoadd\n\tres1, err := rdb.GeoAdd(ctx, \"bikes:rentable\",\n\t\t&redis.GeoLocation{\n\t\t\tLongitude: -122.27652,\n\t\t\tLatitude:  37.805186,\n\t\t\tName:      \"station:1\",\n\t\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> 1\n\n\tres2, err := rdb.GeoAdd(ctx, \"bikes:rentable\",\n\t\t&redis.GeoLocation{\n\t\t\tLongitude: -122.2674626,\n\t\t\tLatitude:  37.8062344,\n\t\t\tName:      \"station:2\",\n\t\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> 1\n\n\tres3, err := rdb.GeoAdd(ctx, \"bikes:rentable\",\n\t\t&redis.GeoLocation{\n\t\t\tLongitude: -122.2469854,\n\t\t\tLatitude:  37.8104049,\n\t\t\tName:      \"station:3\",\n\t\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> 1\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 1\n\t// 1\n}\n\nfunc ExampleClient_geosearch() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:rentable\")\n\n\t_, err := rdb.GeoAdd(ctx, \"bikes:rentable\",\n\t\t&redis.GeoLocation{\n\t\t\tLongitude: -122.27652,\n\t\t\tLatitude:  37.805186,\n\t\t\tName:      \"station:1\",\n\t\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.GeoAdd(ctx, \"bikes:rentable\",\n\t\t&redis.GeoLocation{\n\t\t\tLongitude: -122.2674626,\n\t\t\tLatitude:  37.8062344,\n\t\t\tName:      \"station:2\",\n\t\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.GeoAdd(ctx, \"bikes:rentable\",\n\t\t&redis.GeoLocation{\n\t\t\tLongitude: -122.2469854,\n\t\t\tLatitude:  37.8104049,\n\t\t\tName:      \"station:3\",\n\t\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// REMOVE_END\n\n\t// STEP_START geosearch\n\tres4, err := rdb.GeoSearch(ctx, \"bikes:rentable\",\n\t\t&redis.GeoSearchQuery{\n\t\t\tLongitude:  -122.27652,\n\t\t\tLatitude:   37.805186,\n\t\t\tRadius:     5,\n\t\t\tRadiusUnit: \"km\",\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> [station:1 station:2 station:3]\n\t// STEP_END\n\n\t// Output:\n\t// [station:1 station:2 station:3]\n}\n"
  },
  {
    "path": "doctests/hash_tutorial_test.go",
    "content": "// EXAMPLE: hash_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_set_get_all() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike:1\")\n\t// REMOVE_END\n\n\t// STEP_START set_get_all\n\thashFields := []string{\n\t\t\"model\", \"Deimos\",\n\t\t\"brand\", \"Ergonom\",\n\t\t\"type\", \"Enduro bikes\",\n\t\t\"price\", \"4972\",\n\t}\n\n\tres1, err := rdb.HSet(ctx, \"bike:1\", hashFields).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> 4\n\n\tres2, err := rdb.HGet(ctx, \"bike:1\", \"model\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> Deimos\n\n\tres3, err := rdb.HGet(ctx, \"bike:1\", \"price\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> 4972\n\n\tcmdReturn := rdb.HGetAll(ctx, \"bike:1\")\n\tres4, err := cmdReturn.Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4)\n\t// >>> map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes]\n\n\ttype BikeInfo struct {\n\t\tModel string `redis:\"model\"`\n\t\tBrand string `redis:\"brand\"`\n\t\tType  string `redis:\"type\"`\n\t\tPrice int    `redis:\"price\"`\n\t}\n\n\tvar res4a BikeInfo\n\n\tif err := cmdReturn.Scan(&res4a); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Model: %v, Brand: %v, Type: %v, Price: $%v\\n\",\n\t\tres4a.Model, res4a.Brand, res4a.Type, res4a.Price)\n\t// >>> Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972\n\t// STEP_END\n\n\t// Output:\n\t// 4\n\t// Deimos\n\t// 4972\n\t// map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes]\n\t// Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972\n}\n\nfunc ExampleClient_hmget() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike:1\")\n\t// REMOVE_END\n\n\thashFields := []string{\n\t\t\"model\", \"Deimos\",\n\t\t\"brand\", \"Ergonom\",\n\t\t\"type\", \"Enduro bikes\",\n\t\t\"price\", \"4972\",\n\t}\n\n\t_, err := rdb.HSet(ctx, \"bike:1\", hashFields).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START hmget\n\tcmdReturn := rdb.HMGet(ctx, \"bike:1\", \"model\", \"price\")\n\tres5, err := cmdReturn.Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> [Deimos 4972]\n\n\ttype BikeInfo struct {\n\t\tModel string `redis:\"model\"`\n\t\tBrand string `redis:\"-\"`\n\t\tType  string `redis:\"-\"`\n\t\tPrice int    `redis:\"price\"`\n\t}\n\n\tvar res5a BikeInfo\n\n\tif err := cmdReturn.Scan(&res5a); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Model: %v, Price: $%v\\n\", res5a.Model, res5a.Price)\n\t// >>> Model: Deimos, Price: $4972\n\t// STEP_END\n\n\t// Output:\n\t// [Deimos 4972]\n\t// Model: Deimos, Price: $4972\n}\n\nfunc ExampleClient_hincrby() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike:1\")\n\t// REMOVE_END\n\n\thashFields := []string{\n\t\t\"model\", \"Deimos\",\n\t\t\"brand\", \"Ergonom\",\n\t\t\"type\", \"Enduro bikes\",\n\t\t\"price\", \"4972\",\n\t}\n\n\t_, err := rdb.HSet(ctx, \"bike:1\", hashFields).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START hincrby\n\tres6, err := rdb.HIncrBy(ctx, \"bike:1\", \"price\", 100).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> 5072\n\n\tres7, err := rdb.HIncrBy(ctx, \"bike:1\", \"price\", -100).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> 4972\n\t// STEP_END\n\n\t// Output:\n\t// 5072\n\t// 4972\n}\n\nfunc ExampleClient_incrby_get_mget() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike:1:stats\")\n\t// REMOVE_END\n\n\t// STEP_START incrby_get_mget\n\tres8, err := rdb.HIncrBy(ctx, \"bike:1:stats\", \"rides\", 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> 1\n\n\tres9, err := rdb.HIncrBy(ctx, \"bike:1:stats\", \"rides\", 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> 2\n\n\tres10, err := rdb.HIncrBy(ctx, \"bike:1:stats\", \"rides\", 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> 3\n\n\tres11, err := rdb.HIncrBy(ctx, \"bike:1:stats\", \"crashes\", 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> 1\n\n\tres12, err := rdb.HIncrBy(ctx, \"bike:1:stats\", \"owners\", 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> 1\n\n\tres13, err := rdb.HGet(ctx, \"bike:1:stats\", \"rides\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13) // >>> 3\n\n\tres14, err := rdb.HMGet(ctx, \"bike:1:stats\", \"crashes\", \"owners\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14) // >>> [1 1]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// 3\n\t// 1\n\t// 1\n\t// 3\n\t// [1 1]\n}\n"
  },
  {
    "path": "doctests/hll_tutorial_test.go",
    "content": "// EXAMPLE: hll_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_pfadd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes\", \"commuter_bikes\", \"all_bikes\")\n\t// REMOVE_END\n\n\t// STEP_START pfadd\n\tres1, err := rdb.PFAdd(ctx, \"bikes\", \"Hyperion\", \"Deimos\", \"Phoebe\", \"Quaoar\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // 1\n\n\tres2, err := rdb.PFCount(ctx, \"bikes\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // 4\n\n\tres3, err := rdb.PFAdd(ctx, \"commuter_bikes\", \"Salacia\", \"Mimas\", \"Quaoar\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // 1\n\n\tres4, err := rdb.PFMerge(ctx, \"all_bikes\", \"bikes\", \"commuter_bikes\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // OK\n\n\tres5, err := rdb.PFCount(ctx, \"all_bikes\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // 6\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 4\n\t// 1\n\t// OK\n\t// 6\n}\n"
  },
  {
    "path": "doctests/home_json_example_test.go",
    "content": "// EXAMPLE: go_home_json\n// HIDE_START\npackage example_commands_test\n\n// HIDE_END\n// STEP_START import\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// STEP_END\n\nfunc ExampleClient_search_json() {\n\t// STEP_START connect\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\t// STEP_END\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"user:1\", \"user:2\", \"user:3\")\n\trdb.FTDropIndex(ctx, \"idx:users\")\n\t// REMOVE_END\n\n\t// STEP_START create_data\n\tuser1 := map[string]interface{}{\n\t\t\"name\":  \"Paul John\",\n\t\t\"email\": \"paul.john@example.com\",\n\t\t\"age\":   42,\n\t\t\"city\":  \"London\",\n\t}\n\n\tuser2 := map[string]interface{}{\n\t\t\"name\":  \"Eden Zamir\",\n\t\t\"email\": \"eden.zamir@example.com\",\n\t\t\"age\":   29,\n\t\t\"city\":  \"Tel Aviv\",\n\t}\n\n\tuser3 := map[string]interface{}{\n\t\t\"name\":  \"Paul Zamir\",\n\t\t\"email\": \"paul.zamir@example.com\",\n\t\t\"age\":   35,\n\t\t\"city\":  \"Tel Aviv\",\n\t}\n\t// STEP_END\n\n\t// STEP_START make_index\n\t_, err := rdb.FTCreate(\n\t\tctx,\n\t\t\"idx:users\",\n\t\t// Options:\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"user:\"},\n\t\t},\n\t\t// Index schema fields:\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.name\",\n\t\t\tAs:        \"name\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.city\",\n\t\t\tAs:        \"city\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.age\",\n\t\t\tAs:        \"age\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// STEP_END\n\n\t// STEP_START add_data\n\t_, err = rdb.JSONSet(ctx, \"user:1\", \"$\", user1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.JSONSet(ctx, \"user:2\", \"$\", user2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.JSONSet(ctx, \"user:3\", \"$\", user3).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// STEP_END\n\n\t// STEP_START query1\n\tfindPaulResult, err := rdb.FTSearch(\n\t\tctx,\n\t\t\"idx:users\",\n\t\t\"Paul @age:[30 40]\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(findPaulResult)\n\t// >>> {1 [{user:3 <nil> <nil> <nil> map[$:{\"age\":35,\"city\":\"Tel Aviv\"...\n\t// STEP_END\n\n\t// STEP_START query2\n\tcitiesResult, err := rdb.FTSearchWithArgs(\n\t\tctx,\n\t\t\"idx:users\",\n\t\t\"Paul\",\n\t\t&redis.FTSearchOptions{\n\t\t\tReturn: []redis.FTSearchReturn{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"$.city\",\n\t\t\t\t\tAs:        \"city\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsort.Slice(citiesResult.Docs, func(i, j int) bool {\n\t\treturn citiesResult.Docs[i].Fields[\"city\"] < citiesResult.Docs[j].Fields[\"city\"]\n\t})\n\n\tfor _, result := range citiesResult.Docs {\n\t\tfmt.Println(result.Fields[\"city\"])\n\t}\n\t// >>> London\n\t// >>> Tel Aviv\n\t// STEP_END\n\n\t// STEP_START query2count_only\n\tcitiesResult2, err := rdb.FTSearchWithArgs(\n\t\tctx,\n\t\t\"idx:users\",\n\t\t\"Paul\",\n\t\t&redis.FTSearchOptions{\n\t\t\tReturn: []redis.FTSearchReturn{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"$.city\",\n\t\t\t\t\tAs:        \"city\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tCountOnly: true,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// The `Total` field has the correct number of docs found\n\t// by the query but the `Docs` slice is empty.\n\tfmt.Println(len(citiesResult2.Docs)) // >>> 0\n\tfmt.Println(citiesResult2.Total)     // >>> 2\n\t// STEP_END\n\n\t// STEP_START query3\n\taggOptions := redis.FTAggregateOptions{\n\t\tGroupBy: []redis.FTAggregateGroupBy{\n\t\t\t{\n\t\t\t\tFields: []interface{}{\"@city\"},\n\t\t\t\tReduce: []redis.FTAggregateReducer{\n\t\t\t\t\t{\n\t\t\t\t\t\tReducer: redis.SearchCount,\n\t\t\t\t\t\tAs:      \"count\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\taggResult, err := rdb.FTAggregateWithArgs(\n\t\tctx,\n\t\t\"idx:users\",\n\t\t\"*\",\n\t\t&aggOptions,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsort.Slice(aggResult.Rows, func(i, j int) bool {\n\t\treturn aggResult.Rows[i].Fields[\"city\"].(string) <\n\t\t\taggResult.Rows[j].Fields[\"city\"].(string)\n\t})\n\n\tfor _, row := range aggResult.Rows {\n\t\tfmt.Printf(\"%v - %v\\n\",\n\t\t\trow.Fields[\"city\"], row.Fields[\"count\"],\n\t\t)\n\t}\n\t// >>> City: London - 1\n\t// >>> City: Tel Aviv - 2\n\t// STEP_END\n\n\t// Output:\n\t// {1 [{user:3 <nil> <nil> <nil> map[$:{\"age\":35,\"city\":\"Tel Aviv\",\"email\":\"paul.zamir@example.com\",\"name\":\"Paul Zamir\"}] <nil>}]}\n\t// London\n\t// Tel Aviv\n\t// 0\n\t// 2\n\t// London - 1\n\t// Tel Aviv - 2\n}\n\nfunc ExampleClient_search_hash() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"huser:1\", \"huser:2\", \"huser:3\")\n\trdb.FTDropIndex(ctx, \"hash-idx:users\")\n\t// REMOVE_END\n\n\t// STEP_START make_hash_index\n\t_, err := rdb.FTCreate(\n\t\tctx,\n\t\t\"hash-idx:users\",\n\t\t// Options:\n\t\t&redis.FTCreateOptions{\n\t\t\tOnHash: true,\n\t\t\tPrefix: []interface{}{\"huser:\"},\n\t\t},\n\t\t// Index schema fields:\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"name\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"city\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"age\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// STEP_END\n\n\tuser1 := map[string]interface{}{\n\t\t\"name\":  \"Paul John\",\n\t\t\"email\": \"paul.john@example.com\",\n\t\t\"age\":   42,\n\t\t\"city\":  \"London\",\n\t}\n\n\tuser2 := map[string]interface{}{\n\t\t\"name\":  \"Eden Zamir\",\n\t\t\"email\": \"eden.zamir@example.com\",\n\t\t\"age\":   29,\n\t\t\"city\":  \"Tel Aviv\",\n\t}\n\n\tuser3 := map[string]interface{}{\n\t\t\"name\":  \"Paul Zamir\",\n\t\t\"email\": \"paul.zamir@example.com\",\n\t\t\"age\":   35,\n\t\t\"city\":  \"Tel Aviv\",\n\t}\n\n\t// STEP_START add_hash_data\n\t_, err = rdb.HSet(ctx, \"huser:1\", user1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.HSet(ctx, \"huser:2\", user2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.HSet(ctx, \"huser:3\", user3).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// STEP_END\n\n\t// STEP_START query1_hash\n\tfindPaulHashResult, err := rdb.FTSearch(\n\t\tctx,\n\t\t\"hash-idx:users\",\n\t\t\"Paul @age:[30 40]\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(findPaulHashResult)\n\t// >>> {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv...\n\t// STEP_END\n\n\t// Output:\n\t// {1 [{huser:3 <nil> <nil> <nil> map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir] <nil>}]}\n}\n"
  },
  {
    "path": "doctests/home_prob_dts_test.go",
    "content": "// EXAMPLE: home_prob_dts\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_probabilistic_datatypes() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx,\n\t\t\"recorded_users\", \"other_users\",\n\t\t\"group:1\", \"group:2\", \"both_groups\",\n\t\t\"items_sold\",\n\t\t\"male_heights\", \"female_heights\", \"all_heights\",\n\t\t\"top_3_songs\")\n\t// REMOVE_END\n\n\t// STEP_START bloom\n\tres1, err := rdb.BFMAdd(\n\t\tctx,\n\t\t\"recorded_users\",\n\t\t\"andy\", \"cameron\", \"david\", \"michelle\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> [true true true true]\n\n\tres2, err := rdb.BFExists(ctx,\n\t\t\"recorded_users\", \"cameron\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> true\n\n\tres3, err := rdb.BFExists(ctx, \"recorded_users\", \"kaitlyn\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> false\n\t// STEP_END\n\n\t// STEP_START cuckoo\n\tres4, err := rdb.CFAdd(ctx, \"other_users\", \"paolo\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> true\n\n\tres5, err := rdb.CFAdd(ctx, \"other_users\", \"kaitlyn\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> true\n\n\tres6, err := rdb.CFAdd(ctx, \"other_users\", \"rachel\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> true\n\n\tres7, err := rdb.CFMExists(ctx,\n\t\t\"other_users\", \"paolo\", \"rachel\", \"andy\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> [true true false]\n\n\tres8, err := rdb.CFDel(ctx, \"other_users\", \"paolo\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> true\n\n\tres9, err := rdb.CFExists(ctx, \"other_users\", \"paolo\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> false\n\t// STEP_END\n\n\t// STEP_START hyperloglog\n\tres10, err := rdb.PFAdd(\n\t\tctx,\n\t\t\"group:1\",\n\t\t\"andy\", \"cameron\", \"david\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> 1\n\n\tres11, err := rdb.PFCount(ctx, \"group:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> 3\n\n\tres12, err := rdb.PFAdd(ctx,\n\t\t\"group:2\",\n\t\t\"kaitlyn\", \"michelle\", \"paolo\", \"rachel\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> 1\n\n\tres13, err := rdb.PFCount(ctx, \"group:2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13) // >>> 4\n\n\tres14, err := rdb.PFMerge(\n\t\tctx,\n\t\t\"both_groups\",\n\t\t\"group:1\", \"group:2\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14) // >>> OK\n\n\tres15, err := rdb.PFCount(ctx, \"both_groups\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res15) // >>> 7\n\t// STEP_END\n\n\t// STEP_START cms\n\t// Specify that you want to keep the counts within 0.01\n\t// (0.1%) of the true value with a 0.005 (0.05%) chance\n\t// of going outside this limit.\n\tres16, err := rdb.CMSInitByProb(ctx, \"items_sold\", 0.01, 0.005).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16) // >>> OK\n\n\t// The parameters for `CMSIncrBy()` are two lists. The count\n\t// for each item in the first list is incremented by the\n\t// value at the same index in the second list.\n\tres17, err := rdb.CMSIncrBy(ctx, \"items_sold\",\n\t\t\"bread\", 300,\n\t\t\"tea\", 200,\n\t\t\"coffee\", 200,\n\t\t\"beer\", 100,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res17) // >>> [300 200 200 100]\n\n\tres18, err := rdb.CMSIncrBy(ctx, \"items_sold\",\n\t\t\"bread\", 100,\n\t\t\"coffee\", 150,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res18) // >>> [400 350]\n\n\tres19, err := rdb.CMSQuery(ctx,\n\t\t\"items_sold\",\n\t\t\"bread\", \"tea\", \"coffee\", \"beer\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // >>> [400 200 350 100]\n\t// STEP_END\n\n\t// STEP_START tdigest\n\tres20, err := rdb.TDigestCreate(ctx, \"male_heights\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> OK\n\n\tres21, err := rdb.TDigestAdd(ctx, \"male_heights\",\n\t\t175.5, 181, 160.8, 152, 177, 196, 164,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res21) // >>> OK\n\n\tres22, err := rdb.TDigestMin(ctx, \"male_heights\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(res22) // >>> 152\n\n\tres23, err := rdb.TDigestMax(ctx, \"male_heights\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res23) // >>> 196\n\n\tres24, err := rdb.TDigestQuantile(ctx, \"male_heights\", 0.75).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res24) // >>> [181]\n\n\t// Note that the CDF value for 181 is not exactly\n\t// 0.75. Both values are estimates.\n\tres25, err := rdb.TDigestCDF(ctx, \"male_heights\", 181).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"%.4f\\n\", res25[0]) // >>> 0.7857\n\n\tres26, err := rdb.TDigestCreate(ctx, \"female_heights\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res26) // >>> OK\n\n\tres27, err := rdb.TDigestAdd(ctx, \"female_heights\",\n\t\t155.5, 161, 168.5, 170, 157.5, 163, 171,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res27) // >>> OK\n\n\tres28, err := rdb.TDigestQuantile(ctx, \"female_heights\", 0.75).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res28) // >>> [170]\n\n\tres29, err := rdb.TDigestMerge(ctx, \"all_heights\",\n\t\tnil,\n\t\t\"male_heights\", \"female_heights\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res29) // >>> OK\n\n\tres30, err := rdb.TDigestQuantile(ctx, \"all_heights\", 0.75).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res30) // >>> [175.5]\n\t// STEP_END\n\n\t// STEP_START topk\n\t// Create a TopK filter that keeps track of the top 3 items\n\tres31, err := rdb.TopKReserve(ctx, \"top_3_songs\", 3).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res31) // >>> OK\n\n\t// Add some items to the filter\n\tres32, err := rdb.TopKIncrBy(ctx,\n\t\t\"top_3_songs\",\n\t\t\"Starfish Trooper\", 3000,\n\t\t\"Only one more time\", 1850,\n\t\t\"Rock me, Handel\", 1325,\n\t\t\"How will anyone know?\", 3890,\n\t\t\"Average lover\", 4098,\n\t\t\"Road to everywhere\", 770,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res32)\n\t// >>> [   Rock me, Handel Only one more time ]\n\n\tres33, err := rdb.TopKList(ctx, \"top_3_songs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res33)\n\t// >>> [Average lover How will anyone know? Starfish Trooper]\n\n\t// Query the count for specific items\n\tres34, err := rdb.TopKQuery(\n\t\tctx,\n\t\t\"top_3_songs\",\n\t\t\"Starfish Trooper\", \"Road to everywhere\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res34) // >>> [true false]\n\t// STEP_END\n\n\t// Output:\n\t// [true true true true]\n\t// true\n\t// false\n\t// true\n\t// true\n\t// true\n\t// [true true false]\n\t// true\n\t// false\n\t// 1\n\t// 3\n\t// 1\n\t// 4\n\t// OK\n\t// 7\n\t// OK\n\t// [300 200 200 100]\n\t// [400 350]\n\t// [400 200 350 100]\n\t// OK\n\t// OK\n\t// 152\n\t// 196\n\t// [181]\n\t// 0.7857\n\t// OK\n\t// OK\n\t// [170]\n\t// OK\n\t// [175.5]\n\t// OK\n\t// [   Rock me, Handel Only one more time ]\n\t// [Average lover How will anyone know? Starfish Trooper]\n\t// [true false]\n}\n"
  },
  {
    "path": "doctests/json_tutorial_test.go",
    "content": "// EXAMPLE: json_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\nfunc ExampleClient_setget() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike\")\n\t// REMOVE_END\n\n\t// STEP_START set_get\n\tres1, err := rdb.JSONSet(ctx, \"bike\", \"$\",\n\t\t\"\\\"Hyperion\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.JSONGet(ctx, \"bike\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> [\"Hyperion\"]\n\n\tres3, err := rdb.JSONType(ctx, \"bike\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> [[string]]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [\"Hyperion\"]\n\t// [[string]]\n}\n\nfunc ExampleClient_str() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bike\", \"$\",\n\t\t\"\\\"Hyperion\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START str\n\tres4, err := rdb.JSONStrLen(ctx, \"bike\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(*res4[0]) // >>> 8\n\n\tres5, err := rdb.JSONStrAppend(ctx, \"bike\", \"$\", \"\\\" (Enduro bikes)\\\"\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(*res5[0]) // >>> 23\n\n\tres6, err := rdb.JSONGet(ctx, \"bike\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> [\"Hyperion (Enduro bikes)\"]\n\t// STEP_END\n\n\t// Output:\n\t// 8\n\t// 23\n\t// [\"Hyperion (Enduro bikes)\"]\n}\n\nfunc ExampleClient_num() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"crashes\")\n\t// REMOVE_END\n\n\t// STEP_START num\n\tres7, err := rdb.JSONSet(ctx, \"crashes\", \"$\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> OK\n\n\tres8, err := rdb.JSONNumIncrBy(ctx, \"crashes\", \"$\", 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> [1]\n\n\tres9, err := rdb.JSONNumIncrBy(ctx, \"crashes\", \"$\", 1.5).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> [2.5]\n\n\tres10, err := rdb.JSONNumIncrBy(ctx, \"crashes\", \"$\", -0.75).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> [1.75]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [1]\n\t// [2.5]\n\t// [1.75]\n}\n\nfunc ExampleClient_arr() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"newbike\")\n\t// REMOVE_END\n\n\t// STEP_START arr\n\tres11, err := rdb.JSONSet(ctx, \"newbike\", \"$\",\n\t\t[]interface{}{\n\t\t\t\"Deimos\",\n\t\t\tmap[string]interface{}{\"crashes\": 0},\n\t\t\tnil,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> OK\n\n\tres12, err := rdb.JSONGet(ctx, \"newbike\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> [[\"Deimos\",{\"crashes\":0},null]]\n\n\tres13, err := rdb.JSONGet(ctx, \"newbike\", \"$[1].crashes\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13) // >>> [0]\n\n\tres14, err := rdb.JSONDel(ctx, \"newbike\", \"$.[-1]\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14) // >>> 1\n\n\tres15, err := rdb.JSONGet(ctx, \"newbike\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res15) // >>> [[\"Deimos\",{\"crashes\":0}]]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [[\"Deimos\",{\"crashes\":0},null]]\n\t// [0]\n\t// 1\n\t// [[\"Deimos\",{\"crashes\":0}]]\n}\n\nfunc ExampleClient_arr2() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"riders\")\n\t// REMOVE_END\n\n\t// STEP_START arr2\n\tres16, err := rdb.JSONSet(ctx, \"riders\", \"$\", []interface{}{}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16) // >>> OK\n\n\tres17, err := rdb.JSONArrAppend(ctx, \"riders\", \"$\", \"\\\"Norem\\\"\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res17) // >>> [1]\n\n\tres18, err := rdb.JSONGet(ctx, \"riders\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res18) // >>> [[\"Norem\"]]\n\n\tres19, err := rdb.JSONArrInsert(ctx, \"riders\", \"$\", 1,\n\t\t\"\\\"Prickett\\\"\", \"\\\"Royce\\\"\", \"\\\"Castilla\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // [3]\n\n\tres20, err := rdb.JSONGet(ctx, \"riders\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> [[\"Norem\", \"Prickett\", \"Royce\", \"Castilla\"]]\n\n\trangeStop := 1\n\n\tres21, err := rdb.JSONArrTrimWithArgs(ctx, \"riders\", \"$\",\n\t\t&redis.JSONArrTrimArgs{Start: 1, Stop: &rangeStop},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res21) // >>> [1]\n\n\tres22, err := rdb.JSONGet(ctx, \"riders\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res22) // >>> [[\"Prickett\"]]\n\n\tres23, err := rdb.JSONArrPop(ctx, \"riders\", \"$\", -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res23) // >>> [[\"Prickett\"]]\n\n\tres24, err := rdb.JSONArrPop(ctx, \"riders\", \"$\", -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res24) // []\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [1]\n\t// [[\"Norem\"]]\n\t// [4]\n\t// [[\"Norem\",\"Prickett\",\"Royce\",\"Castilla\"]]\n\t// [1]\n\t// [[\"Prickett\"]]\n\t// [\"Prickett\"]\n\t// []\n}\n\nfunc ExampleClient_obj() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bike:1\")\n\t// REMOVE_END\n\n\t// STEP_START obj\n\tres25, err := rdb.JSONSet(ctx, \"bike:1\", \"$\",\n\t\tmap[string]interface{}{\n\t\t\t\"model\": \"Deimos\",\n\t\t\t\"brand\": \"Ergonom\",\n\t\t\t\"price\": 4972,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res25) // >>> OK\n\n\tres26, err := rdb.JSONObjLen(ctx, \"bike:1\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(*res26[0]) // >>> 3\n\n\tres27, err := rdb.JSONObjKeys(ctx, \"bike:1\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res27) // >>> [brand model price]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// 3\n\t// [[brand model price]]\n}\n\nvar inventory_json = map[string]interface{}{\n\t\"inventory\": map[string]interface{}{\n\t\t\"mountain_bikes\": []interface{}{\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"id\":    \"bike:1\",\n\t\t\t\t\"model\": \"Phoebe\",\n\t\t\t\t\"description\": \"This is a mid-travel trail slayer that is a fantastic \" +\n\t\t\t\t\t\"daily driver or one bike quiver. The Shimano Claris 8-speed groupset \" +\n\t\t\t\t\t\"gives plenty of gear range to tackle hills and there\\u2019s room for \" +\n\t\t\t\t\t\"mudguards and a rack too.  This is the bike for the rider who wants \" +\n\t\t\t\t\t\"trail manners with low fuss ownership.\",\n\t\t\t\t\"price\":  1920,\n\t\t\t\t\"specs\":  map[string]interface{}{\"material\": \"carbon\", \"weight\": 13.1},\n\t\t\t\t\"colors\": []interface{}{\"black\", \"silver\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"id\":    \"bike:2\",\n\t\t\t\t\"model\": \"Quaoar\",\n\t\t\t\t\"description\": \"Redesigned for the 2020 model year, this bike \" +\n\t\t\t\t\t\"impressed our testers and is the best all-around trail bike we've \" +\n\t\t\t\t\t\"ever tested. The Shimano gear system effectively does away with an \" +\n\t\t\t\t\t\"external cassette, so is super low maintenance in terms of wear \" +\n\t\t\t\t\t\"and tear. All in all it's an impressive package for the price, \" +\n\t\t\t\t\t\"making it very competitive.\",\n\t\t\t\t\"price\":  2072,\n\t\t\t\t\"specs\":  map[string]interface{}{\"material\": \"aluminium\", \"weight\": 7.9},\n\t\t\t\t\"colors\": []interface{}{\"black\", \"white\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"id\":    \"bike:3\",\n\t\t\t\t\"model\": \"Weywot\",\n\t\t\t\t\"description\": \"This bike gives kids aged six years and older \" +\n\t\t\t\t\t\"a durable and uberlight mountain bike for their first experience \" +\n\t\t\t\t\t\"on tracks and easy cruising through forests and fields. A set of \" +\n\t\t\t\t\t\"powerful Shimano hydraulic disc brakes provide ample stopping \" +\n\t\t\t\t\t\"ability. If you're after a budget option, this is one of the best \" +\n\t\t\t\t\t\"bikes you could get.\",\n\t\t\t\t\"price\": 3264,\n\t\t\t\t\"specs\": map[string]interface{}{\"material\": \"alloy\", \"weight\": 13.8},\n\t\t\t},\n\t\t},\n\t\t\"commuter_bikes\": []interface{}{\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"id\":    \"bike:4\",\n\t\t\t\t\"model\": \"Salacia\",\n\t\t\t\t\"description\": \"This bike is a great option for anyone who just \" +\n\t\t\t\t\t\"wants a bike to get about on With a slick-shifting Claris gears \" +\n\t\t\t\t\t\"from Shimano\\u2019s, this is a bike which doesn\\u2019t break the \" +\n\t\t\t\t\t\"bank and delivers craved performance.  It\\u2019s for the rider \" +\n\t\t\t\t\t\"who wants both efficiency and capability.\",\n\t\t\t\t\"price\":  1475,\n\t\t\t\t\"specs\":  map[string]interface{}{\"material\": \"aluminium\", \"weight\": 16.6},\n\t\t\t\t\"colors\": []interface{}{\"black\", \"silver\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"id\":    \"bike:5\",\n\t\t\t\t\"model\": \"Mimas\",\n\t\t\t\t\"description\": \"A real joy to ride, this bike got very high \" +\n\t\t\t\t\t\"scores in last years Bike of the year report. The carefully \" +\n\t\t\t\t\t\"crafted 50-34 tooth chainset and 11-32 tooth cassette give an \" +\n\t\t\t\t\t\"easy-on-the-legs bottom gear for climbing, and the high-quality \" +\n\t\t\t\t\t\"Vittoria Zaffiro tires give balance and grip.It includes \" +\n\t\t\t\t\t\"a low-step frame , our memory foam seat, bump-resistant shocks and \" +\n\t\t\t\t\t\"conveniently placed thumb throttle. Put it all together and you \" +\n\t\t\t\t\t\"get a bike that helps redefine what can be done for this price.\",\n\t\t\t\t\"price\": 3941,\n\t\t\t\t\"specs\": map[string]interface{}{\"material\": \"alloy\", \"weight\": 11.6},\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc ExampleClient_setbikes() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t// STEP_START set_bikes\n\tvar inventory_json = map[string]interface{}{\n\t\t\"inventory\": map[string]interface{}{\n\t\t\t\"mountain_bikes\": []interface{}{\n\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\"id\":    \"bike:1\",\n\t\t\t\t\t\"model\": \"Phoebe\",\n\t\t\t\t\t\"description\": \"This is a mid-travel trail slayer that is a fantastic \" +\n\t\t\t\t\t\t\"daily driver or one bike quiver. The Shimano Claris 8-speed groupset \" +\n\t\t\t\t\t\t\"gives plenty of gear range to tackle hills and there\\u2019s room for \" +\n\t\t\t\t\t\t\"mudguards and a rack too.  This is the bike for the rider who wants \" +\n\t\t\t\t\t\t\"trail manners with low fuss ownership.\",\n\t\t\t\t\t\"price\":  1920,\n\t\t\t\t\t\"specs\":  map[string]interface{}{\"material\": \"carbon\", \"weight\": 13.1},\n\t\t\t\t\t\"colors\": []interface{}{\"black\", \"silver\"},\n\t\t\t\t},\n\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\"id\":    \"bike:2\",\n\t\t\t\t\t\"model\": \"Quaoar\",\n\t\t\t\t\t\"description\": \"Redesigned for the 2020 model year, this bike \" +\n\t\t\t\t\t\t\"impressed our testers and is the best all-around trail bike we've \" +\n\t\t\t\t\t\t\"ever tested. The Shimano gear system effectively does away with an \" +\n\t\t\t\t\t\t\"external cassette, so is super low maintenance in terms of wear \" +\n\t\t\t\t\t\t\"and tear. All in all it's an impressive package for the price, \" +\n\t\t\t\t\t\t\"making it very competitive.\",\n\t\t\t\t\t\"price\":  2072,\n\t\t\t\t\t\"specs\":  map[string]interface{}{\"material\": \"aluminium\", \"weight\": 7.9},\n\t\t\t\t\t\"colors\": []interface{}{\"black\", \"white\"},\n\t\t\t\t},\n\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\"id\":    \"bike:3\",\n\t\t\t\t\t\"model\": \"Weywot\",\n\t\t\t\t\t\"description\": \"This bike gives kids aged six years and older \" +\n\t\t\t\t\t\t\"a durable and uberlight mountain bike for their first experience \" +\n\t\t\t\t\t\t\"on tracks and easy cruising through forests and fields. A set of \" +\n\t\t\t\t\t\t\"powerful Shimano hydraulic disc brakes provide ample stopping \" +\n\t\t\t\t\t\t\"ability. If you're after a budget option, this is one of the best \" +\n\t\t\t\t\t\t\"bikes you could get.\",\n\t\t\t\t\t\"price\": 3264,\n\t\t\t\t\t\"specs\": map[string]interface{}{\"material\": \"alloy\", \"weight\": 13.8},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"commuter_bikes\": []interface{}{\n\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\"id\":    \"bike:4\",\n\t\t\t\t\t\"model\": \"Salacia\",\n\t\t\t\t\t\"description\": \"This bike is a great option for anyone who just \" +\n\t\t\t\t\t\t\"wants a bike to get about on With a slick-shifting Claris gears \" +\n\t\t\t\t\t\t\"from Shimano\\u2019s, this is a bike which doesn\\u2019t break the \" +\n\t\t\t\t\t\t\"bank and delivers craved performance.  It\\u2019s for the rider \" +\n\t\t\t\t\t\t\"who wants both efficiency and capability.\",\n\t\t\t\t\t\"price\":  1475,\n\t\t\t\t\t\"specs\":  map[string]interface{}{\"material\": \"aluminium\", \"weight\": 16.6},\n\t\t\t\t\t\"colors\": []interface{}{\"black\", \"silver\"},\n\t\t\t\t},\n\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\"id\":    \"bike:5\",\n\t\t\t\t\t\"model\": \"Mimas\",\n\t\t\t\t\t\"description\": \"A real joy to ride, this bike got very high \" +\n\t\t\t\t\t\t\"scores in last years Bike of the year report. The carefully \" +\n\t\t\t\t\t\t\"crafted 50-34 tooth chainset and 11-32 tooth cassette give an \" +\n\t\t\t\t\t\t\"easy-on-the-legs bottom gear for climbing, and the high-quality \" +\n\t\t\t\t\t\t\"Vittoria Zaffiro tires give balance and grip.It includes \" +\n\t\t\t\t\t\t\"a low-step frame , our memory foam seat, bump-resistant shocks and \" +\n\t\t\t\t\t\t\"conveniently placed thumb throttle. Put it all together and you \" +\n\t\t\t\t\t\t\"get a bike that helps redefine what can be done for this price.\",\n\t\t\t\t\t\"price\": 3941,\n\t\t\t\t\t\"specs\": map[string]interface{}{\"material\": \"alloy\", \"weight\": 11.6},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tres1, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\t// STEP_END\n\n\t// Output:\n\t// OK\n}\n\nfunc ExampleClient_getbikes() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START get_bikes\n\tres2, err := rdb.JSONGetWithArgs(ctx, \"bikes:inventory\",\n\t\t&redis.JSONGetArgs{Indent: \"  \", Newline: \"\\n\", Space: \" \"},\n\t\t\"$.inventory.*\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2)\n\t// >>>\n\t// [\n\t//   [\n\t//     {\n\t//       \"colors\": [\n\t//         \"black\",\n\t//         \"silver\"\n\t// ...\n\t// STEP_END\n\n\t// Output:\n\t// [\n\t//   [\n\t//     {\n\t//       \"colors\": [\n\t//         \"black\",\n\t//         \"silver\"\n\t//       ],\n\t//       \"description\": \"This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  It’s for the rider who wants both efficiency and capability.\",\n\t//       \"id\": \"bike:4\",\n\t//       \"model\": \"Salacia\",\n\t//       \"price\": 1475,\n\t//       \"specs\": {\n\t//         \"material\": \"aluminium\",\n\t//         \"weight\": 16.6\n\t//       }\n\t//     },\n\t//     {\n\t//       \"description\": \"A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.\",\n\t//       \"id\": \"bike:5\",\n\t//       \"model\": \"Mimas\",\n\t//       \"price\": 3941,\n\t//       \"specs\": {\n\t//         \"material\": \"alloy\",\n\t//         \"weight\": 11.6\n\t//       }\n\t//     }\n\t//   ],\n\t//   [\n\t//     {\n\t//       \"colors\": [\n\t//         \"black\",\n\t//         \"silver\"\n\t//       ],\n\t//       \"description\": \"This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  This is the bike for the rider who wants trail manners with low fuss ownership.\",\n\t//       \"id\": \"bike:1\",\n\t//       \"model\": \"Phoebe\",\n\t//       \"price\": 1920,\n\t//       \"specs\": {\n\t//         \"material\": \"carbon\",\n\t//         \"weight\": 13.1\n\t//       }\n\t//     },\n\t//     {\n\t//       \"colors\": [\n\t//         \"black\",\n\t//         \"white\"\n\t//       ],\n\t//       \"description\": \"Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.\",\n\t//       \"id\": \"bike:2\",\n\t//       \"model\": \"Quaoar\",\n\t//       \"price\": 2072,\n\t//       \"specs\": {\n\t//         \"material\": \"aluminium\",\n\t//         \"weight\": 7.9\n\t//       }\n\t//     },\n\t//     {\n\t//       \"description\": \"This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.\",\n\t//       \"id\": \"bike:3\",\n\t//       \"model\": \"Weywot\",\n\t//       \"price\": 3264,\n\t//       \"specs\": {\n\t//         \"material\": \"alloy\",\n\t//         \"weight\": 13.8\n\t//       }\n\t//     }\n\t//   ]\n\t// ]\n}\n\nfunc ExampleClient_getmtnbikes() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START get_mtnbikes\n\tres3, err := rdb.JSONGet(ctx, \"bikes:inventory\",\n\t\t\"$.inventory.mountain_bikes[*].model\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3)\n\t// >>> [\"Phoebe\",\"Quaoar\",\"Weywot\"]\n\n\tres4, err := rdb.JSONGet(ctx,\n\t\t\"bikes:inventory\", \"$.inventory[\\\"mountain_bikes\\\"][*].model\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4)\n\t// >>> [\"Phoebe\",\"Quaoar\",\"Weywot\"]\n\n\tres5, err := rdb.JSONGet(ctx,\n\t\t\"bikes:inventory\", \"$..mountain_bikes[*].model\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5)\n\t// >>> [\"Phoebe\",\"Quaoar\",\"Weywot\"]\n\t// STEP_END\n\n\t// Output:\n\t// [\"Phoebe\",\"Quaoar\",\"Weywot\"]\n\t// [\"Phoebe\",\"Quaoar\",\"Weywot\"]\n\t// [\"Phoebe\",\"Quaoar\",\"Weywot\"]\n}\n\nfunc ExampleClient_getmodels() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START get_models\n\tres6, err := rdb.JSONGet(ctx, \"bikes:inventory\", \"$..model\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> [\"Salacia\",\"Mimas\",\"Phoebe\",\"Quaoar\",\"Weywot\"]\n\t// STEP_END\n\n\t// Output:\n\t// [\"Salacia\",\"Mimas\",\"Phoebe\",\"Quaoar\",\"Weywot\"]\n}\n\nfunc ExampleClient_get2mtnbikes() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START get2mtnbikes\n\tres7, err := rdb.JSONGet(ctx, \"bikes:inventory\", \"$..mountain_bikes[0:2].model\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> [\"Phoebe\",\"Quaoar\"]\n\t// STEP_END\n\n\t// Output:\n\t// [\"Phoebe\",\"Quaoar\"]\n}\n\nfunc ExampleClient_filter1() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START filter1\n\tres8, err := rdb.JSONGetWithArgs(ctx, \"bikes:inventory\",\n\t\t&redis.JSONGetArgs{Indent: \"  \", Newline: \"\\n\", Space: \" \"},\n\t\t\"$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8)\n\t// >>>\n\t// [\n\t//   {\n\t//     \"colors\": [\n\t//       \"black\",\n\t//       \"white\"\n\t//     ],\n\t//     \"description\": \"Redesigned for the 2020 model year\n\t// ...\n\t// STEP_END\n\n\t// Output:\n\t// [\n\t//   {\n\t//     \"colors\": [\n\t//       \"black\",\n\t//       \"white\"\n\t//     ],\n\t//     \"description\": \"Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.\",\n\t//     \"id\": \"bike:2\",\n\t//     \"model\": \"Quaoar\",\n\t//     \"price\": 2072,\n\t//     \"specs\": {\n\t//       \"material\": \"aluminium\",\n\t//       \"weight\": 7.9\n\t//     }\n\t//   }\n\t// ]\n}\n\nfunc ExampleClient_filter2() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START filter2\n\tres9, err := rdb.JSONGet(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$..[?(@.specs.material == 'alloy')].model\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> [\"Mimas\",\"Weywot\"]\n\t// STEP_END\n\n\t// Output:\n\t// [\"Mimas\",\"Weywot\"]\n}\n\nfunc ExampleClient_filter3() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START filter3\n\tres10, err := rdb.JSONGet(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$..[?(@.specs.material =~ '(?i)al')].model\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> [\"Salacia\",\"Mimas\",\"Quaoar\",\"Weywot\"]\n\t// STEP_END\n\n\t// Output:\n\t// [\"Salacia\",\"Mimas\",\"Quaoar\",\"Weywot\"]\n}\n\nfunc ExampleClient_filter4() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START filter4\n\tres11, err := rdb.JSONSet(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$.inventory.mountain_bikes[0].regex_pat\",\n\t\t\"\\\"(?i)al\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> OK\n\n\tres12, err := rdb.JSONSet(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$.inventory.mountain_bikes[1].regex_pat\",\n\t\t\"\\\"(?i)al\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> OK\n\n\tres13, err := rdb.JSONSet(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$.inventory.mountain_bikes[2].regex_pat\",\n\t\t\"\\\"(?i)al\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13) // >>> OK\n\n\tres14, err := rdb.JSONGet(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14) // >>> [\"Quaoar\",\"Weywot\"]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// OK\n\t// [\"Quaoar\",\"Weywot\"]\n}\n\nfunc ExampleClient_updatebikes() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START update_bikes\n\tres15, err := rdb.JSONGet(ctx, \"bikes:inventory\", \"$..price\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res15) // >>> [1475,3941,1920,2072,3264]\n\n\tres16, err := rdb.JSONNumIncrBy(ctx, \"bikes:inventory\", \"$..price\", -100).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16) // >>> [1375,3841,1820,1972,3164]\n\n\tres17, err := rdb.JSONNumIncrBy(ctx, \"bikes:inventory\", \"$..price\", 100).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res17) // >>> [1475,3941,1920,2072,3264]\n\t// STEP_END\n\n\t// Output:\n\t// [1475,3941,1920,2072,3264]\n\t// [1375,3841,1820,1972,3164]\n\t// [1475,3941,1920,2072,3264]\n}\n\nfunc ExampleClient_updatefilters1() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START update_filters1\n\tres18, err := rdb.JSONSet(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$.inventory.*[?(@.price<2000)].price\",\n\t\t1500,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res18) // >>> OK\n\n\tres19, err := rdb.JSONGet(ctx, \"bikes:inventory\", \"$..price\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // >>> [1500,3941,1500,2072,3264]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [1500,3941,1500,2072,3264]\n}\n\nfunc ExampleClient_updatefilters2() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:inventory\")\n\t// REMOVE_END\n\n\t_, err := rdb.JSONSet(ctx, \"bikes:inventory\", \"$\", inventory_json).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START update_filters2\n\tres20, err := rdb.JSONArrAppend(ctx,\n\t\t\"bikes:inventory\",\n\t\t\"$.inventory.*[?(@.price<2000)].colors\",\n\t\t\"\\\"pink\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> [3 3]\n\n\tres21, err := rdb.JSONGet(ctx, \"bikes:inventory\", \"$..[*].colors\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res21)\n\t// >>> [[\"black\",\"silver\",\"pink\"],[\"black\",\"silver\",\"pink\"],[\"black\",\"white\"]]\n\t// STEP_END\n\n\t// Output:\n\t// [3 3]\n\t// [[\"black\",\"silver\",\"pink\"],[\"black\",\"silver\",\"pink\"],[\"black\",\"white\"]]\n}\n"
  },
  {
    "path": "doctests/list_tutorial_test.go",
    "content": "// EXAMPLE: list_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_queue() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START queue\n\tres1, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> 1\n\n\tres2, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> 2\n\n\tres3, err := rdb.RPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> bike:1\n\n\tres4, err := rdb.RPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> bike:2\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// bike:1\n\t// bike:2\n}\n\nfunc ExampleClient_stack() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START stack\n\tres5, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> 1\n\n\tres6, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> 2\n\n\tres7, err := rdb.LPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> bike:2\n\n\tres8, err := rdb.LPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> bike:1\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// bike:2\n\t// bike:1\n}\n\nfunc ExampleClient_llen() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START llen\n\tres9, err := rdb.LLen(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> 0\n\t// STEP_END\n\n\t// Output:\n\t// 0\n}\n\nfunc ExampleClient_lmove_lrange() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\trdb.Del(ctx, \"bikes:finished\")\n\t// REMOVE_END\n\n\t// STEP_START lmove_lrange\n\tres10, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> 1\n\n\tres11, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> 2\n\n\tres12, err := rdb.LMove(ctx, \"bikes:repairs\", \"bikes:finished\", \"LEFT\", \"LEFT\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> bike:2\n\n\tres13, err := rdb.LRange(ctx, \"bikes:repairs\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13) // >>> [bike:1]\n\n\tres14, err := rdb.LRange(ctx, \"bikes:finished\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14) // >>> [bike:2]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// bike:2\n\t// [bike:1]\n\t// [bike:2]\n}\n\nfunc ExampleClient_lpush_rpush() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START lpush_rpush\n\tres15, err := rdb.RPush(ctx, \"bikes:repairs\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res15) // >>> 1\n\n\tres16, err := rdb.RPush(ctx, \"bikes:repairs\", \"bike:2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16) // >>> 2\n\n\tres17, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:important_bike\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res17) // >>> 3\n\n\tres18, err := rdb.LRange(ctx, \"bikes:repairs\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res18) // >>> [bike:important_bike bike:1 bike:2]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// 3\n\t// [bike:important_bike bike:1 bike:2]\n}\n\nfunc ExampleClient_variadic() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START variadic\n\tres19, err := rdb.RPush(ctx, \"bikes:repairs\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // >>> 3\n\n\tres20, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:important_bike\", \"bike:very_important_bike\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> 5\n\n\tres21, err := rdb.LRange(ctx, \"bikes:repairs\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res21) // >>> [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3]\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// 5\n\t// [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3]\n}\n\nfunc ExampleClient_lpop_rpop() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START lpop_rpop\n\tres22, err := rdb.RPush(ctx, \"bikes:repairs\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res22) // >>> 3\n\n\tres23, err := rdb.RPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res23) // >>> bike:3\n\n\tres24, err := rdb.LPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res24) // >>> bike:1\n\n\tres25, err := rdb.RPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res25) // >>> bike:2\n\n\tres26, err := rdb.RPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tfmt.Println(err) // >>> redis: nil\n\t}\n\n\tfmt.Println(res26) // >>> <empty string>\n\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// bike:3\n\t// bike:1\n\t// bike:2\n\t// redis: nil\n\t//\n}\n\nfunc ExampleClient_ltrim() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START ltrim\n\tres27, err := rdb.RPush(ctx, \"bikes:repairs\", \"bike:1\", \"bike:2\", \"bike:3\", \"bike:4\", \"bike:5\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res27) // >>> 5\n\n\tres28, err := rdb.LTrim(ctx, \"bikes:repairs\", 0, 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res28) // >>> OK\n\n\tres29, err := rdb.LRange(ctx, \"bikes:repairs\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res29) // >>> [bike:1 bike:2 bike:3]\n\t// STEP_END\n\n\t// Output:\n\t// 5\n\t// OK\n\t// [bike:1 bike:2 bike:3]\n}\n\nfunc ExampleClient_ltrim_end_of_list() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START ltrim_end_of_list\n\tres30, err := rdb.RPush(ctx, \"bikes:repairs\", \"bike:1\", \"bike:2\", \"bike:3\", \"bike:4\", \"bike:5\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res30) // >>> 5\n\n\tres31, err := rdb.LTrim(ctx, \"bikes:repairs\", -3, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res31) // >>> OK\n\n\tres32, err := rdb.LRange(ctx, \"bikes:repairs\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res32) // >>> [bike:3 bike:4 bike:5]\n\t// STEP_END\n\n\t// Output:\n\t// 5\n\t// OK\n\t// [bike:3 bike:4 bike:5]\n}\n\nfunc ExampleClient_brpop() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START brpop\n\tres33, err := rdb.RPush(ctx, \"bikes:repairs\", \"bike:1\", \"bike:2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res33) // >>> 2\n\n\tres34, err := rdb.BRPop(ctx, 1, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res34) // >>> [bikes:repairs bike:2]\n\n\tres35, err := rdb.BRPop(ctx, 1, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res35) // >>> [bikes:repairs bike:1]\n\n\tres36, err := rdb.BRPop(ctx, 1, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tfmt.Println(err) // >>> redis: nil\n\t}\n\n\tfmt.Println(res36) // >>> []\n\t// STEP_END\n\n\t// Output:\n\t// 2\n\t// [bikes:repairs bike:2]\n\t// [bikes:repairs bike:1]\n\t// redis: nil\n\t// []\n}\n\nfunc ExampleClient_rule1() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"new_bikes\")\n\t// REMOVE_END\n\n\t// STEP_START rule_1\n\tres37, err := rdb.Del(ctx, \"new_bikes\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res37) // >>> 0\n\n\tres38, err := rdb.LPush(ctx, \"new_bikes\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res38) // >>> 3\n\t// STEP_END\n\n\t// Output:\n\t// 0\n\t// 3\n}\n\nfunc ExampleClient_rule11() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"new_bikes\")\n\t// REMOVE_END\n\n\t// STEP_START rule_1.1\n\tres39, err := rdb.Set(ctx, \"new_bikes\", \"bike:1\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res39) // >>> OK\n\n\tres40, err := rdb.Type(ctx, \"new_bikes\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res40) // >>> string\n\n\tres41, err := rdb.LPush(ctx, \"new_bikes\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\t// >>> WRONGTYPE Operation against a key holding the wrong kind of value\n\t}\n\n\tfmt.Println(res41)\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// string\n\t// WRONGTYPE Operation against a key holding the wrong kind of value\n\t// 0\n}\n\nfunc ExampleClient_rule2() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START rule_2\n\tres42, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res42) // >>> 3\n\n\tres43, err := rdb.Exists(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res43) // >>> 1\n\n\tres44, err := rdb.LPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res44) // >>> bike:3\n\n\tres45, err := rdb.LPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res45) // >>> bike:2\n\n\tres46, err := rdb.LPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res46) // >>> bike:1\n\n\tres47, err := rdb.Exists(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res47) // >>> 0\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// 1\n\t// bike:3\n\t// bike:2\n\t// bike:1\n\t// 0\n}\n\nfunc ExampleClient_rule3() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START rule_3\n\tres48, err := rdb.Del(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res48) // >>> 0\n\n\tres49, err := rdb.LLen(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res49) // >>> 0\n\n\tres50, err := rdb.LPop(ctx, \"bikes:repairs\").Result()\n\n\tif err != nil {\n\t\tfmt.Println(err) // >>> redis: nil\n\t}\n\n\tfmt.Println(res50) // >>> <empty string>\n\t// STEP_END\n\n\t// Output:\n\t// 0\n\t// 0\n\t// redis: nil\n\t//\n}\n\nfunc ExampleClient_ltrim1() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\trdb.Del(ctx, \"bikes:repairs\")\n\t// REMOVE_END\n\n\t// STEP_START ltrim.1\n\tres51, err := rdb.LPush(ctx, \"bikes:repairs\", \"bike:1\", \"bike:2\", \"bike:3\", \"bike:4\", \"bike:5\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res51) // >>> 5\n\n\tres52, err := rdb.LTrim(ctx, \"bikes:repairs\", 0, 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res52) // >>> OK\n\n\tres53, err := rdb.LRange(ctx, \"bikes:repairs\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res53) // >>> [bike:5 bike:4 bike:3]\n\t// STEP_END\n\n\t// Output:\n\t// 5\n\t// OK\n\t// [bike:5 bike:4 bike:3]\n}\n"
  },
  {
    "path": "doctests/lpush_lrange_test.go",
    "content": "// EXAMPLE: lpush_and_lrange\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient_LPush_and_lrange() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// HIDE_END\n\n\t// REMOVE_START\n\terrFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test\n\tif errFlush != nil {\n\t\tpanic(errFlush)\n\t}\n\t// REMOVE_END\n\n\tlistSize, err := rdb.LPush(ctx, \"my_bikes\", \"bike:1\", \"bike:2\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(listSize)\n\ttime.Sleep(10 * time.Millisecond) // Simulate some delay\n\n\tvalue, err := rdb.LRange(ctx, \"my_bikes\", 0, -1).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(value)\n\t// HIDE_START\n\n\t// Output: 2\n\t// [bike:2 bike:1]\n}\n\n// HIDE_END\n"
  },
  {
    "path": "doctests/main_test.go",
    "content": "package example_commands_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar RedisVersion float64\n\nfunc init() {\n\t// read REDIS_VERSION from env\n\tRedisVersion, _ = strconv.ParseFloat(strings.Trim(os.Getenv(\"REDIS_VERSION\"), \"\\\"\"), 64)\n\tfmt.Printf(\"REDIS_VERSION: %.1f\\n\", RedisVersion)\n}\n"
  },
  {
    "path": "doctests/pipe_trans_example_test.go",
    "content": "// EXAMPLE: pipe_trans_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_transactions() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\tfor i := 0; i < 5; i++ {\n\t\trdb.Del(ctx, fmt.Sprintf(\"seat:%d\", i))\n\t}\n\n\trdb.Del(ctx, \"counter:1\", \"counter:2\", \"counter:3\", \"shellpath\")\n\t// REMOVE_END\n\n\t// STEP_START basic_pipe\n\tpipe := rdb.Pipeline()\n\n\tfor i := 0; i < 5; i++ {\n\t\tpipe.Set(ctx, fmt.Sprintf(\"seat:%v\", i), fmt.Sprintf(\"#%v\", i), 0)\n\t}\n\n\tcmds, err := pipe.Exec(ctx)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor _, c := range cmds {\n\t\tfmt.Printf(\"%v;\", c.(*redis.StatusCmd).Val())\n\t}\n\n\tfmt.Println(\"\")\n\t// >>> OK;OK;OK;OK;OK;\n\n\tpipe = rdb.Pipeline()\n\n\tget0Result := pipe.Get(ctx, \"seat:0\")\n\tget3Result := pipe.Get(ctx, \"seat:3\")\n\tget4Result := pipe.Get(ctx, \"seat:4\")\n\n\tcmds, err = pipe.Exec(ctx)\n\n\t// The results are available only after the pipeline\n\t// has finished executing.\n\tfmt.Println(get0Result.Val()) // >>> #0\n\tfmt.Println(get3Result.Val()) // >>> #3\n\tfmt.Println(get4Result.Val()) // >>> #4\n\t// STEP_END\n\n\t// STEP_START basic_pipe_pipelined\n\tvar pd0Result *redis.StatusCmd\n\tvar pd3Result *redis.StatusCmd\n\tvar pd4Result *redis.StatusCmd\n\n\tcmds, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\tpd0Result = (*redis.StatusCmd)(pipe.Get(ctx, \"seat:0\"))\n\t\tpd3Result = (*redis.StatusCmd)(pipe.Get(ctx, \"seat:3\"))\n\t\tpd4Result = (*redis.StatusCmd)(pipe.Get(ctx, \"seat:4\"))\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// The results are available only after the pipeline\n\t// has finished executing.\n\tfmt.Println(pd0Result.Val()) // >>> #0\n\tfmt.Println(pd3Result.Val()) // >>> #3\n\tfmt.Println(pd4Result.Val()) // >>> #4\n\t// STEP_END\n\n\t// STEP_START basic_trans\n\ttrans := rdb.TxPipeline()\n\n\ttrans.IncrBy(ctx, \"counter:1\", 1)\n\ttrans.IncrBy(ctx, \"counter:2\", 2)\n\ttrans.IncrBy(ctx, \"counter:3\", 3)\n\n\tcmds, err = trans.Exec(ctx)\n\n\tfor _, c := range cmds {\n\t\tfmt.Println(c.(*redis.IntCmd).Val())\n\t}\n\t// >>> 1\n\t// >>> 2\n\t// >>> 3\n\t// STEP_END\n\n\t// STEP_START basic_trans_txpipelined\n\tvar tx1Result *redis.IntCmd\n\tvar tx2Result *redis.IntCmd\n\tvar tx3Result *redis.IntCmd\n\n\tcmds, err = rdb.TxPipelined(ctx, func(trans redis.Pipeliner) error {\n\t\ttx1Result = trans.IncrBy(ctx, \"counter:1\", 1)\n\t\ttx2Result = trans.IncrBy(ctx, \"counter:2\", 2)\n\t\ttx3Result = trans.IncrBy(ctx, \"counter:3\", 3)\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(tx1Result.Val()) // >>> 2\n\tfmt.Println(tx2Result.Val()) // >>> 4\n\tfmt.Println(tx3Result.Val()) // >>> 6\n\t// STEP_END\n\n\t// STEP_START trans_watch\n\t// Set initial value of `shellpath`.\n\trdb.Set(ctx, \"shellpath\", \"/usr/syscmds/\", 0)\n\n\tconst maxRetries = 1000\n\n\t// Retry if the key has been changed.\n\tfor i := 0; i < maxRetries; i++ {\n\t\terr := rdb.Watch(ctx,\n\t\t\tfunc(tx *redis.Tx) error {\n\t\t\t\tcurrentPath, err := rdb.Get(ctx, \"shellpath\").Result()\n\t\t\t\tnewPath := currentPath + \":/usr/mycmds/\"\n\n\t\t\t\t_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Set(ctx, \"shellpath\", newPath, 0)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\n\t\t\t\treturn err\n\t\t\t},\n\t\t\t\"shellpath\",\n\t\t)\n\n\t\tif err == nil {\n\t\t\t// Success.\n\t\t\tbreak\n\t\t} else if err == redis.TxFailedErr {\n\t\t\t// Optimistic lock lost. Retry the transaction.\n\t\t\tcontinue\n\t\t} else {\n\t\t\t// Panic for any other error.\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tfmt.Println(rdb.Get(ctx, \"shellpath\").Val())\n\t// >>> /usr/syscmds/:/usr/mycmds/\n\t// STEP_END\n\n\t// Output:\n\t// OK;OK;OK;OK;OK;\n\t// #0\n\t// #3\n\t// #4\n\t// #0\n\t// #3\n\t// #4\n\t// 1\n\t// 2\n\t// 3\n\t// 2\n\t// 4\n\t// 6\n\t// /usr/syscmds/:/usr/mycmds/\n}\n"
  },
  {
    "path": "doctests/query_agg_test.go",
    "content": "// EXAMPLE: query_agg\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient_query_agg() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\t// HIDE_END\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.FTDropIndex(ctx, \"idx:bicycle\")\n\trdb.FTDropIndex(ctx, \"idx:email\")\n\t// REMOVE_END\n\n\t_, err := rdb.FTCreate(ctx, \"idx:bicycle\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"bicycle:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.brand\",\n\t\t\tAs:        \"brand\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.model\",\n\t\t\tAs:        \"model\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.description\",\n\t\t\tAs:        \"description\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.price\",\n\t\t\tAs:        \"price\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.condition\",\n\t\t\tAs:        \"condition\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\texampleJsons := []map[string]interface{}{\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, \" +\n\t\t\t\t\"-74.0610 40.6678, -74.0610 40.7578))\",\n\t\t\t\"store_location\": \"-74.0060,40.7128\",\n\t\t\t\"brand\":          \"Velorim\",\n\t\t\t\"model\":          \"Jigger\",\n\t\t\t\"price\":          270,\n\t\t\t\"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! \" +\n\t\t\t\t\"This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger \" +\n\t\t\t\t\"is the vehicle of choice for the rare tenacious little rider raring to go.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, \" +\n\t\t\t\t\"-118.2887 33.9872, -118.2887 34.0972))\",\n\t\t\t\"store_location\": \"-118.2437,34.0522\",\n\t\t\t\"brand\":          \"Bicyk\",\n\t\t\t\"model\":          \"Hillcraft\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"Kids want to ride with as little weight as possible. Especially \" +\n\t\t\t\t\"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming \" +\n\t\t\t\t\"off a 24'' bike. The Hillcraft 26 is just the solution they need!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, \" +\n\t\t\t\t\"-87.6848 41.8231, -87.6848 41.9331))\",\n\t\t\t\"store_location\": \"-87.6298,41.8781\",\n\t\t\t\"brand\":          \"Nord\",\n\t\t\t\"model\":          \"Chook air 5\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"The Chook Air 5  gives kids aged six years and older a durable \" +\n\t\t\t\t\"and uberlight mountain bike for their first experience on tracks and easy cruising through \" +\n\t\t\t\t\"forests and fields. The lower  top tube makes it easy to mount and dismount in any \" +\n\t\t\t\t\"situation, giving your kids greater safety on the trails.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, \" +\n\t\t\t\t\"-80.2433 25.6967, -80.2433 25.8067))\",\n\t\t\t\"store_location\": \"-80.1918,25.7617\",\n\t\t\t\"brand\":          \"Eva\",\n\t\t\t\"model\":          \"Eva 291\",\n\t\t\t\"price\":          3400,\n\t\t\t\"description\": \"The sister company to Nord, Eva launched in 2005 as the first \" +\n\t\t\t\t\"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes \" +\n\t\t\t\t\"are optimized for the feminine physique using analytics from a body metrics database. \" +\n\t\t\t\t\"If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This \" +\n\t\t\t\t\"full-suspension, cross-country ride has been designed for velocity. The 291 has \" +\n\t\t\t\t\"100mm of front and rear travel, a superlight aluminum frame and fast-rolling \" +\n\t\t\t\t\"29-inch wheels. Yippee!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, \" +\n\t\t\t\t\"-122.4644 37.7099, -122.4644 37.8199))\",\n\t\t\t\"store_location\": \"-122.4194,37.7749\",\n\t\t\t\"brand\":          \"Noka Bikes\",\n\t\t\t\"model\":          \"Kahuna\",\n\t\t\t\"price\":          3200,\n\t\t\t\"description\": \"Whether you want to try your hand at XC racing or are looking \" +\n\t\t\t\t\"for a lively trail bike that's just as inspiring on the climbs as it is over rougher \" +\n\t\t\t\t\"ground, the Wilder is one heck of a bike built specifically for short women. Both the \" +\n\t\t\t\t\"frames and components have been tweaked to include a women’s saddle, different bars \" +\n\t\t\t\t\"and unique colourway.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, \" +\n\t\t\t\t\"-0.1778 51.4024, -0.1778 51.5524))\",\n\t\t\t\"store_location\": \"-0.1278,51.5074\",\n\t\t\t\"brand\":          \"Breakout\",\n\t\t\t\"model\":          \"XBN 2.1 Alloy\",\n\t\t\t\"price\":          810,\n\t\t\t\"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s \" +\n\t\t\t\t\"not to say that it’s a basic machine. With an internal weld aluminium frame, a full \" +\n\t\t\t\t\"carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which \" +\n\t\t\t\t\"doesn’t break the bank and delivers craved performance.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, \" +\n\t\t\t\t\"2.1767 48.5516, 2.1767 48.9016))\",\n\t\t\t\"store_location\": \"2.3522,48.8566\",\n\t\t\t\"brand\":          \"ScramBikes\",\n\t\t\t\"model\":          \"WattBike\",\n\t\t\t\"price\":          2300,\n\t\t\t\"description\": \"The WattBike is the best e-bike for people who still \" +\n\t\t\t\t\"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH \" +\n\t\t\t\t\"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one \" +\n\t\t\t\t\"charge. It’s great for tackling hilly terrain or if you just fancy a more \" +\n\t\t\t\t\"leisurely ride. With three working modes, you can choose between E-bike, \" +\n\t\t\t\t\"assisted bicycle, and normal bike modes.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, \" +\n\t\t\t\t\"13.3260 52.2700, 13.3260 52.5700))\",\n\t\t\t\"store_location\": \"13.4050,52.5200\",\n\t\t\t\"brand\":          \"Peaknetic\",\n\t\t\t\"model\":          \"Secto\",\n\t\t\t\"price\":          430,\n\t\t\t\"description\": \"If you struggle with stiff fingers or a kinked neck or \" +\n\t\t\t\t\"back after a few minutes on the road, this lightweight, aluminum bike alleviates \" +\n\t\t\t\t\"those issues and allows you to enjoy the ride. From the ergonomic grips to the \" +\n\t\t\t\t\"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. \" +\n\t\t\t\t\"The rear-inclined seat tube facilitates stability by allowing you to put a foot \" +\n\t\t\t\t\"on the ground to balance at a stop, and the low step-over frame makes it \" +\n\t\t\t\t\"accessible for all ability and mobility levels. The saddle is very soft, with \" +\n\t\t\t\t\"a wide back to support your hip joints and a cutout in the center to redistribute \" +\n\t\t\t\t\"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires \" +\n\t\t\t\t\"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts \" +\n\t\t\t\t\"facilitate setting up the Roll Low-Entry as your preferred commuter, and the \" +\n\t\t\t\t\"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, \" +\n\t\t\t\t\"1.9450 41.1987, 1.9450 41.4301))\",\n\t\t\t\"store_location\": \"2.1734, 41.3851\",\n\t\t\t\"brand\":          \"nHill\",\n\t\t\t\"model\":          \"Summit\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"This budget mountain bike from nHill performs well both \" +\n\t\t\t\t\"on bike paths and on the trail. The fork with 100mm of travel absorbs rough \" +\n\t\t\t\t\"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. \" +\n\t\t\t\t\"The Shimano Tourney drivetrain offered enough gears for finding a comfortable \" +\n\t\t\t\t\"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. \" +\n\t\t\t\t\"Whether you want an affordable bike that you can take to work, but also take \" +\n\t\t\t\t\"trail in mountains on the weekends or you’re just after a stable, comfortable \" +\n\t\t\t\t\"ride for the bike path, the Summit gives a good value for money.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, \" +\n\t\t\t\t\"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\",\n\t\t\t\"store_location\": \"12.4964,41.9028\",\n\t\t\t\"model\":          \"ThrillCycle\",\n\t\t\t\"brand\":          \"BikeShind\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"An artsy,  retro-inspired bicycle that’s as \" +\n\t\t\t\t\"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. \" +\n\t\t\t\t\"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t \" +\n\t\t\t\t\"suggest taking it to the mountains. Fenders protect you from mud, and a rear \" +\n\t\t\t\t\"basket lets you transport groceries, flowers and books. The ThrillCycle comes \" +\n\t\t\t\t\"with a limited lifetime warranty, so this little guy will last you long \" +\n\t\t\t\t\"past graduation.\",\n\t\t\t\"condition\": \"refurbished\",\n\t\t},\n\t}\n\n\tfor i, json := range exampleJsons {\n\t\t_, err := rdb.JSONSet(ctx, fmt.Sprintf(\"bicycle:%v\", i), \"$\", json).Result()\n\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// STEP_START agg1\n\tres1, err := rdb.FTAggregateWithArgs(ctx,\n\t\t\"idx:bicycle\",\n\t\t\"@condition:{new}\",\n\t\t&redis.FTAggregateOptions{\n\t\t\tApply: []redis.FTAggregateApply{\n\t\t\t\t{\n\t\t\t\t\tField: \"@price - (@price * 0.1)\",\n\t\t\t\t\tAs:    \"discounted\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad: []redis.FTAggregateLoad{\n\t\t\t\t{Field: \"__key\"},\n\t\t\t\t{Field: \"price\"},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(len(res1.Rows)) // >>> 5\n\n\tsort.Slice(res1.Rows, func(i, j int) bool {\n\t\treturn res1.Rows[i].Fields[\"__key\"].(string) <\n\t\t\tres1.Rows[j].Fields[\"__key\"].(string)\n\t})\n\n\tfor _, row := range res1.Rows {\n\t\tfmt.Printf(\n\t\t\t\"__key=%v, discounted=%v, price=%v\\n\",\n\t\t\trow.Fields[\"__key\"],\n\t\t\trow.Fields[\"discounted\"],\n\t\t\trow.Fields[\"price\"],\n\t\t)\n\t}\n\t// >>> __key=bicycle:0, discounted=243, price=270\n\t// >>> __key=bicycle:5, discounted=729, price=810\n\t// >>> __key=bicycle:6, discounted=2070, price=2300\n\t// >>> __key=bicycle:7, discounted=387, price=430\n\t// >>> __key=bicycle:8, discounted=1080, price=1200\n\t// STEP_END\n\n\t// STEP_START agg2\n\tres2, err := rdb.FTAggregateWithArgs(ctx,\n\t\t\"idx:bicycle\", \"*\",\n\t\t&redis.FTAggregateOptions{\n\t\t\tLoad: []redis.FTAggregateLoad{\n\t\t\t\t{Field: \"price\"},\n\t\t\t},\n\t\t\tApply: []redis.FTAggregateApply{\n\t\t\t\t{\n\t\t\t\t\tField: \"@price<1000\",\n\t\t\t\t\tAs:    \"price_category\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroupBy: []redis.FTAggregateGroupBy{\n\t\t\t\t{\n\t\t\t\t\tFields: []interface{}{\"@condition\"},\n\t\t\t\t\tReduce: []redis.FTAggregateReducer{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReducer: redis.SearchSum,\n\t\t\t\t\t\t\tArgs:    []interface{}{\"@price_category\"},\n\t\t\t\t\t\t\tAs:      \"num_affordable\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(len(res2.Rows)) // >>> 3\n\n\tsort.Slice(res2.Rows, func(i, j int) bool {\n\t\treturn res2.Rows[i].Fields[\"condition\"].(string) <\n\t\t\tres2.Rows[j].Fields[\"condition\"].(string)\n\t})\n\n\tfor _, row := range res2.Rows {\n\t\tfmt.Printf(\n\t\t\t\"condition=%v, num_affordable=%v\\n\",\n\t\t\trow.Fields[\"condition\"],\n\t\t\trow.Fields[\"num_affordable\"],\n\t\t)\n\t}\n\t// >>> condition=new, num_affordable=3\n\t// >>> condition=refurbished, num_affordable=1\n\t// >>> condition=used, num_affordable=1\n\t// STEP_END\n\n\t// STEP_START agg3\n\n\tres3, err := rdb.FTAggregateWithArgs(ctx,\n\t\t\"idx:bicycle\", \"*\",\n\t\t&redis.FTAggregateOptions{\n\t\t\tApply: []redis.FTAggregateApply{\n\t\t\t\t{\n\t\t\t\t\tField: \"'bicycle'\",\n\t\t\t\t\tAs:    \"type\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroupBy: []redis.FTAggregateGroupBy{\n\t\t\t\t{\n\t\t\t\t\tFields: []interface{}{\"@type\"},\n\t\t\t\t\tReduce: []redis.FTAggregateReducer{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReducer: redis.SearchCount,\n\t\t\t\t\t\t\tAs:      \"num_total\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(len(res3.Rows)) // >>> 1\n\n\tfor _, row := range res3.Rows {\n\t\tfmt.Printf(\n\t\t\t\"type=%v, num_total=%v\\n\",\n\t\t\trow.Fields[\"type\"],\n\t\t\trow.Fields[\"num_total\"],\n\t\t)\n\t}\n\t// type=bicycle, num_total=10\n\t// STEP_END\n\n\t// STEP_START agg4\n\tres4, err := rdb.FTAggregateWithArgs(ctx,\n\t\t\"idx:bicycle\", \"*\",\n\t\t&redis.FTAggregateOptions{\n\t\t\tLoad: []redis.FTAggregateLoad{\n\t\t\t\t{Field: \"__key\"},\n\t\t\t},\n\t\t\tGroupBy: []redis.FTAggregateGroupBy{\n\t\t\t\t{\n\t\t\t\t\tFields: []interface{}{\"@condition\"},\n\t\t\t\t\tReduce: []redis.FTAggregateReducer{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReducer: redis.SearchToList,\n\t\t\t\t\t\t\tArgs:    []interface{}{\"__key\"},\n\t\t\t\t\t\t\tAs:      \"bicycles\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(len(res4.Rows)) // >>> 3\n\n\tsort.Slice(res4.Rows, func(i, j int) bool {\n\t\treturn res4.Rows[i].Fields[\"condition\"].(string) <\n\t\t\tres4.Rows[j].Fields[\"condition\"].(string)\n\t})\n\n\tfor _, row := range res4.Rows {\n\t\trowBikes := row.Fields[\"bicycles\"].([]interface{})\n\t\tbikes := make([]string, len(rowBikes))\n\n\t\tfor i, rowBike := range rowBikes {\n\t\t\tbikes[i] = rowBike.(string)\n\t\t}\n\n\t\tsort.Slice(bikes, func(i, j int) bool {\n\t\t\treturn bikes[i] < bikes[j]\n\t\t})\n\n\t\tfmt.Printf(\n\t\t\t\"condition=%v, bicycles=%v\\n\",\n\t\t\trow.Fields[\"condition\"],\n\t\t\tbikes,\n\t\t)\n\t}\n\t// >>> condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8]\n\t// >>> condition=refurbished, bicycles=[bicycle:9]\n\t// >>> condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4]\n\t// STEP_END\n\n\t// Output:\n\t// 5\n\t// __key=bicycle:0, discounted=243, price=270\n\t// __key=bicycle:5, discounted=729, price=810\n\t// __key=bicycle:6, discounted=2070, price=2300\n\t// __key=bicycle:7, discounted=387, price=430\n\t// __key=bicycle:8, discounted=1080, price=1200\n\t// 3\n\t// condition=new, num_affordable=3\n\t// condition=refurbished, num_affordable=1\n\t// condition=used, num_affordable=1\n\t// 1\n\t// type=bicycle, num_total=10\n\t// 3\n\t// condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8]\n\t// condition=refurbished, bicycles=[bicycle:9]\n\t// condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4]\n}\n"
  },
  {
    "path": "doctests/query_em_test.go",
    "content": "// EXAMPLE: query_em\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient_query_em() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\n\t// HIDE_END\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.FTDropIndex(ctx, \"idx:bicycle\")\n\trdb.FTDropIndex(ctx, \"idx:email\")\n\t// REMOVE_END\n\n\t_, err := rdb.FTCreate(ctx, \"idx:bicycle\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"bicycle:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.brand\",\n\t\t\tAs:        \"brand\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.model\",\n\t\t\tAs:        \"model\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.description\",\n\t\t\tAs:        \"description\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.price\",\n\t\t\tAs:        \"price\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.condition\",\n\t\t\tAs:        \"condition\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\texampleJsons := []map[string]interface{}{\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, \" +\n\t\t\t\t\"-74.0610 40.6678, -74.0610 40.7578))\",\n\t\t\t\"store_location\": \"-74.0060,40.7128\",\n\t\t\t\"brand\":          \"Velorim\",\n\t\t\t\"model\":          \"Jigger\",\n\t\t\t\"price\":          270,\n\t\t\t\"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! \" +\n\t\t\t\t\"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger \" +\n\t\t\t\t\"is the vehicle of choice for the rare tenacious little rider raring to go.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, \" +\n\t\t\t\t\"-118.2887 33.9872, -118.2887 34.0972))\",\n\t\t\t\"store_location\": \"-118.2437,34.0522\",\n\t\t\t\"brand\":          \"Bicyk\",\n\t\t\t\"model\":          \"Hillcraft\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"Kids want to ride with as little weight as possible. Especially \" +\n\t\t\t\t\"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming \" +\n\t\t\t\t\"off a 24'' bike. The Hillcraft 26 is just the solution they need!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, \" +\n\t\t\t\t\"-87.6848 41.8231, -87.6848 41.9331))\",\n\t\t\t\"store_location\": \"-87.6298,41.8781\",\n\t\t\t\"brand\":          \"Nord\",\n\t\t\t\"model\":          \"Chook air 5\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"The Chook Air 5  gives kids aged six years and older a durable \" +\n\t\t\t\t\"and uberlight mountain bike for their first experience on tracks and easy cruising through \" +\n\t\t\t\t\"forests and fields. The lower  top tube makes it easy to mount and dismount in any \" +\n\t\t\t\t\"situation, giving your kids greater safety on the trails.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, \" +\n\t\t\t\t\"-80.2433 25.6967, -80.2433 25.8067))\",\n\t\t\t\"store_location\": \"-80.1918,25.7617\",\n\t\t\t\"brand\":          \"Eva\",\n\t\t\t\"model\":          \"Eva 291\",\n\t\t\t\"price\":          3400,\n\t\t\t\"description\": \"The sister company to Nord, Eva launched in 2005 as the first \" +\n\t\t\t\t\"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes \" +\n\t\t\t\t\"are optimized for the feminine physique using analytics from a body metrics database. \" +\n\t\t\t\t\"If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This \" +\n\t\t\t\t\"full-suspension, cross-country ride has been designed for velocity. The 291 has \" +\n\t\t\t\t\"100mm of front and rear travel, a superlight aluminum frame and fast-rolling \" +\n\t\t\t\t\"29-inch wheels. Yippee!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, \" +\n\t\t\t\t\"-122.4644 37.7099, -122.4644 37.8199))\",\n\t\t\t\"store_location\": \"-122.4194,37.7749\",\n\t\t\t\"brand\":          \"Noka Bikes\",\n\t\t\t\"model\":          \"Kahuna\",\n\t\t\t\"price\":          3200,\n\t\t\t\"description\": \"Whether you want to try your hand at XC racing or are looking \" +\n\t\t\t\t\"for a lively trail bike that's just as inspiring on the climbs as it is over rougher \" +\n\t\t\t\t\"ground, the Wilder is one heck of a bike built specifically for short women. Both the \" +\n\t\t\t\t\"frames and components have been tweaked to include a women’s saddle, different bars \" +\n\t\t\t\t\"and unique colourway.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, \" +\n\t\t\t\t\"-0.1778 51.4024, -0.1778 51.5524))\",\n\t\t\t\"store_location\": \"-0.1278,51.5074\",\n\t\t\t\"brand\":          \"Breakout\",\n\t\t\t\"model\":          \"XBN 2.1 Alloy\",\n\t\t\t\"price\":          810,\n\t\t\t\"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s \" +\n\t\t\t\t\"not to say that it’s a basic machine. With an internal weld aluminium frame, a full \" +\n\t\t\t\t\"carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which \" +\n\t\t\t\t\"doesn’t break the bank and delivers craved performance.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, \" +\n\t\t\t\t\"2.1767 48.5516, 2.1767 48.9016))\",\n\t\t\t\"store_location\": \"2.3522,48.8566\",\n\t\t\t\"brand\":          \"ScramBikes\",\n\t\t\t\"model\":          \"WattBike\",\n\t\t\t\"price\":          2300,\n\t\t\t\"description\": \"The WattBike is the best e-bike for people who still \" +\n\t\t\t\t\"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH \" +\n\t\t\t\t\"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one \" +\n\t\t\t\t\"charge. It’s great for tackling hilly terrain or if you just fancy a more \" +\n\t\t\t\t\"leisurely ride. With three working modes, you can choose between E-bike, \" +\n\t\t\t\t\"assisted bicycle, and normal bike modes.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, \" +\n\t\t\t\t\"13.3260 52.2700, 13.3260 52.5700))\",\n\t\t\t\"store_location\": \"13.4050,52.5200\",\n\t\t\t\"brand\":          \"Peaknetic\",\n\t\t\t\"model\":          \"Secto\",\n\t\t\t\"price\":          430,\n\t\t\t\"description\": \"If you struggle with stiff fingers or a kinked neck or \" +\n\t\t\t\t\"back after a few minutes on the road, this lightweight, aluminum bike alleviates \" +\n\t\t\t\t\"those issues and allows you to enjoy the ride. From the ergonomic grips to the \" +\n\t\t\t\t\"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. \" +\n\t\t\t\t\"The rear-inclined seat tube facilitates stability by allowing you to put a foot \" +\n\t\t\t\t\"on the ground to balance at a stop, and the low step-over frame makes it \" +\n\t\t\t\t\"accessible for all ability and mobility levels. The saddle is very soft, with \" +\n\t\t\t\t\"a wide back to support your hip joints and a cutout in the center to redistribute \" +\n\t\t\t\t\"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires \" +\n\t\t\t\t\"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts \" +\n\t\t\t\t\"facilitate setting up the Roll Low-Entry as your preferred commuter, and the \" +\n\t\t\t\t\"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, \" +\n\t\t\t\t\"1.9450 41.1987, 1.9450 41.4301))\",\n\t\t\t\"store_location\": \"2.1734, 41.3851\",\n\t\t\t\"brand\":          \"nHill\",\n\t\t\t\"model\":          \"Summit\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"This budget mountain bike from nHill performs well both \" +\n\t\t\t\t\"on bike paths and on the trail. The fork with 100mm of travel absorbs rough \" +\n\t\t\t\t\"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. \" +\n\t\t\t\t\"The Shimano Tourney drivetrain offered enough gears for finding a comfortable \" +\n\t\t\t\t\"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. \" +\n\t\t\t\t\"Whether you want an affordable bike that you can take to work, but also take \" +\n\t\t\t\t\"trail in mountains on the weekends or you’re just after a stable, comfortable \" +\n\t\t\t\t\"ride for the bike path, the Summit gives a good value for money.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, \" +\n\t\t\t\t\"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\",\n\t\t\t\"store_location\": \"12.4964,41.9028\",\n\t\t\t\"model\":          \"ThrillCycle\",\n\t\t\t\"brand\":          \"BikeShind\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"An artsy,  retro-inspired bicycle that’s as \" +\n\t\t\t\t\"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. \" +\n\t\t\t\t\"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t \" +\n\t\t\t\t\"suggest taking it to the mountains. Fenders protect you from mud, and a rear \" +\n\t\t\t\t\"basket lets you transport groceries, flowers and books. The ThrillCycle comes \" +\n\t\t\t\t\"with a limited lifetime warranty, so this little guy will last you long \" +\n\t\t\t\t\"past graduation.\",\n\t\t\t\"condition\": \"refurbished\",\n\t\t},\n\t}\n\n\tfor i, json := range exampleJsons {\n\t\t_, err := rdb.JSONSet(ctx, fmt.Sprintf(\"bicycle:%v\", i), \"$\", json).Result()\n\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// STEP_START em1\n\tres1, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"@price:[270 270]\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1.Total) // >>> 1\n\n\tfor _, doc := range res1.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:0\n\n\tres2, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\",\n\t\t\"*\",\n\t\t&redis.FTSearchOptions{\n\t\t\tFilters: []redis.FTSearchFilter{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t\tMin:       270,\n\t\t\t\t\tMax:       270,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2.Total) // >>> 1\n\n\tfor _, doc := range res2.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:0\n\t// STEP_END\n\n\t// STEP_START em2\n\tres3, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"@condition:{new}\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3.Total) // >>> 5\n\n\tdocs := res3.Docs\n\tslices.SortFunc(docs, func(a, b redis.Document) int {\n\t\treturn strings.Compare(a.ID, b.ID)\n\t})\n\n\tfor _, doc := range docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:0\n\t// >>> bicycle:5\n\t// >>> bicycle:6\n\t// >>> bicycle:7\n\t// >>> bicycle:8\n\t// STEP_END\n\n\t// STEP_START em3\n\tres4, err := rdb.FTCreate(ctx,\n\t\t\"idx:email\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"key:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.email\",\n\t\t\tAs:        \"email\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> OK\n\n\tres5, err := rdb.JSONSet(ctx, \"key:1\", \"$\",\n\t\tmap[string]interface{}{\n\t\t\t\"email\": \"test@redis.com\",\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> OK\n\n\tres6, err := rdb.FTSearch(ctx, \"idx:email\",\n\t\t\"@email:{test\\\\@redis\\\\.com}\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6.Total) // >>> 1\n\t// STEP_END\n\n\t// STEP_START em4\n\tres7, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"@description:\\\"rough terrain\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7.Total) // >>> 1\n\n\tfor _, doc := range res7.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:8\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// bicycle:0\n\t// 1\n\t// bicycle:0\n\t// 5\n\t// bicycle:0\n\t// bicycle:5\n\t// bicycle:6\n\t// bicycle:7\n\t// bicycle:8\n\t// OK\n\t// OK\n\t// 1\n\t// 1\n\t// bicycle:8\n}\n"
  },
  {
    "path": "doctests/query_ft_test.go",
    "content": "// EXAMPLE: query_ft\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient_query_ft() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\t// HIDE_END\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.FTDropIndex(ctx, \"idx:bicycle\")\n\trdb.FTDropIndex(ctx, \"idx:email\")\n\t// REMOVE_END\n\n\t_, err := rdb.FTCreate(ctx, \"idx:bicycle\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"bicycle:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.brand\",\n\t\t\tAs:        \"brand\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.model\",\n\t\t\tAs:        \"model\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.description\",\n\t\t\tAs:        \"description\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.price\",\n\t\t\tAs:        \"price\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.condition\",\n\t\t\tAs:        \"condition\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\texampleJsons := []map[string]interface{}{\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, \" +\n\t\t\t\t\"-74.0610 40.6678, -74.0610 40.7578))\",\n\t\t\t\"store_location\": \"-74.0060,40.7128\",\n\t\t\t\"brand\":          \"Velorim\",\n\t\t\t\"model\":          \"Jigger\",\n\t\t\t\"price\":          270,\n\t\t\t\"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! \" +\n\t\t\t\t\"This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger \" +\n\t\t\t\t\"is the vehicle of choice for the rare tenacious little rider raring to go.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, \" +\n\t\t\t\t\"-118.2887 33.9872, -118.2887 34.0972))\",\n\t\t\t\"store_location\": \"-118.2437,34.0522\",\n\t\t\t\"brand\":          \"Bicyk\",\n\t\t\t\"model\":          \"Hillcraft\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"Kids want to ride with as little weight as possible. Especially \" +\n\t\t\t\t\"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming \" +\n\t\t\t\t\"off a 24'' bike. The Hillcraft 26 is just the solution they need!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, \" +\n\t\t\t\t\"-87.6848 41.8231, -87.6848 41.9331))\",\n\t\t\t\"store_location\": \"-87.6298,41.8781\",\n\t\t\t\"brand\":          \"Nord\",\n\t\t\t\"model\":          \"Chook air 5\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"The Chook Air 5  gives kids aged six years and older a durable \" +\n\t\t\t\t\"and uberlight mountain bike for their first experience on tracks and easy cruising through \" +\n\t\t\t\t\"forests and fields. The lower  top tube makes it easy to mount and dismount in any \" +\n\t\t\t\t\"situation, giving your kids greater safety on the trails.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, \" +\n\t\t\t\t\"-80.2433 25.6967, -80.2433 25.8067))\",\n\t\t\t\"store_location\": \"-80.1918,25.7617\",\n\t\t\t\"brand\":          \"Eva\",\n\t\t\t\"model\":          \"Eva 291\",\n\t\t\t\"price\":          3400,\n\t\t\t\"description\": \"The sister company to Nord, Eva launched in 2005 as the first \" +\n\t\t\t\t\"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes \" +\n\t\t\t\t\"are optimized for the feminine physique using analytics from a body metrics database. \" +\n\t\t\t\t\"If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This \" +\n\t\t\t\t\"full-suspension, cross-country ride has been designed for velocity. The 291 has \" +\n\t\t\t\t\"100mm of front and rear travel, a superlight aluminum frame and fast-rolling \" +\n\t\t\t\t\"29-inch wheels. Yippee!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, \" +\n\t\t\t\t\"-122.4644 37.7099, -122.4644 37.8199))\",\n\t\t\t\"store_location\": \"-122.4194,37.7749\",\n\t\t\t\"brand\":          \"Noka Bikes\",\n\t\t\t\"model\":          \"Kahuna\",\n\t\t\t\"price\":          3200,\n\t\t\t\"description\": \"Whether you want to try your hand at XC racing or are looking \" +\n\t\t\t\t\"for a lively trail bike that's just as inspiring on the climbs as it is over rougher \" +\n\t\t\t\t\"ground, the Wilder is one heck of a bike built specifically for short women. Both the \" +\n\t\t\t\t\"frames and components have been tweaked to include a women’s saddle, different bars \" +\n\t\t\t\t\"and unique colourway.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, \" +\n\t\t\t\t\"-0.1778 51.4024, -0.1778 51.5524))\",\n\t\t\t\"store_location\": \"-0.1278,51.5074\",\n\t\t\t\"brand\":          \"Breakout\",\n\t\t\t\"model\":          \"XBN 2.1 Alloy\",\n\t\t\t\"price\":          810,\n\t\t\t\"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s \" +\n\t\t\t\t\"not to say that it’s a basic machine. With an internal weld aluminium frame, a full \" +\n\t\t\t\t\"carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which \" +\n\t\t\t\t\"doesn’t break the bank and delivers craved performance.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, \" +\n\t\t\t\t\"2.1767 48.5516, 2.1767 48.9016))\",\n\t\t\t\"store_location\": \"2.3522,48.8566\",\n\t\t\t\"brand\":          \"ScramBikes\",\n\t\t\t\"model\":          \"WattBike\",\n\t\t\t\"price\":          2300,\n\t\t\t\"description\": \"The WattBike is the best e-bike for people who still \" +\n\t\t\t\t\"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH \" +\n\t\t\t\t\"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one \" +\n\t\t\t\t\"charge. It’s great for tackling hilly terrain or if you just fancy a more \" +\n\t\t\t\t\"leisurely ride. With three working modes, you can choose between E-bike, \" +\n\t\t\t\t\"assisted bicycle, and normal bike modes.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, \" +\n\t\t\t\t\"13.3260 52.2700, 13.3260 52.5700))\",\n\t\t\t\"store_location\": \"13.4050,52.5200\",\n\t\t\t\"brand\":          \"Peaknetic\",\n\t\t\t\"model\":          \"Secto\",\n\t\t\t\"price\":          430,\n\t\t\t\"description\": \"If you struggle with stiff fingers or a kinked neck or \" +\n\t\t\t\t\"back after a few minutes on the road, this lightweight, aluminum bike alleviates \" +\n\t\t\t\t\"those issues and allows you to enjoy the ride. From the ergonomic grips to the \" +\n\t\t\t\t\"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. \" +\n\t\t\t\t\"The rear-inclined seat tube facilitates stability by allowing you to put a foot \" +\n\t\t\t\t\"on the ground to balance at a stop, and the low step-over frame makes it \" +\n\t\t\t\t\"accessible for all ability and mobility levels. The saddle is very soft, with \" +\n\t\t\t\t\"a wide back to support your hip joints and a cutout in the center to redistribute \" +\n\t\t\t\t\"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires \" +\n\t\t\t\t\"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts \" +\n\t\t\t\t\"facilitate setting up the Roll Low-Entry as your preferred commuter, and the \" +\n\t\t\t\t\"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, \" +\n\t\t\t\t\"1.9450 41.1987, 1.9450 41.4301))\",\n\t\t\t\"store_location\": \"2.1734, 41.3851\",\n\t\t\t\"brand\":          \"nHill\",\n\t\t\t\"model\":          \"Summit\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"This budget mountain bike from nHill performs well both \" +\n\t\t\t\t\"on bike paths and on the trail. The fork with 100mm of travel absorbs rough \" +\n\t\t\t\t\"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. \" +\n\t\t\t\t\"The Shimano Tourney drivetrain offered enough gears for finding a comfortable \" +\n\t\t\t\t\"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. \" +\n\t\t\t\t\"Whether you want an affordable bike that you can take to work, but also take \" +\n\t\t\t\t\"trail in mountains on the weekends or you’re just after a stable, comfortable \" +\n\t\t\t\t\"ride for the bike path, the Summit gives a good value for money.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, \" +\n\t\t\t\t\"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\",\n\t\t\t\"store_location\": \"12.4964,41.9028\",\n\t\t\t\"model\":          \"ThrillCycle\",\n\t\t\t\"brand\":          \"BikeShind\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"An artsy,  retro-inspired bicycle that’s as \" +\n\t\t\t\t\"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. \" +\n\t\t\t\t\"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t \" +\n\t\t\t\t\"suggest taking it to the mountains. Fenders protect you from mud, and a rear \" +\n\t\t\t\t\"basket lets you transport groceries, flowers and books. The ThrillCycle comes \" +\n\t\t\t\t\"with a limited lifetime warranty, so this little guy will last you long \" +\n\t\t\t\t\"past graduation.\",\n\t\t\t\"condition\": \"refurbished\",\n\t\t},\n\t}\n\n\tfor i, json := range exampleJsons {\n\t\t_, err := rdb.JSONSet(ctx, fmt.Sprintf(\"bicycle:%v\", i), \"$\", json).Result()\n\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// STEP_START ft1\n\tres1, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"@description: kids\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1.Total) // >>> 2\n\n\tsort.Slice(res1.Docs, func(i, j int) bool {\n\t\treturn res1.Docs[i].ID < res1.Docs[j].ID\n\t})\n\n\tfor _, doc := range res1.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:1\n\t// >>> bicycle:2\n\t// STEP_END\n\n\t// STEP_START ft2\n\tres2, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"@model: ka*\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2.Total) // >>> 1\n\n\tfor _, doc := range res2.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:4\n\t// STEP_END\n\n\t// STEP_START ft3\n\tres3, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"@brand: *bikes\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3.Total) // >>> 2\n\n\tsort.Slice(res3.Docs, func(i, j int) bool {\n\t\treturn res3.Docs[i].ID < res3.Docs[j].ID\n\t})\n\tfor _, doc := range res3.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:4\n\t// >>> bicycle:6\n\t// STEP_END\n\n\t// STEP_START ft4\n\tres4, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"%optamized%\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4.Total) // >>> 1\n\n\tfor _, doc := range res4.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:3\n\t// STEP_END\n\n\t// STEP_START ft5\n\tres5, err := rdb.FTSearch(ctx,\n\t\t\"idx:bicycle\", \"%%optamised%%\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5.Total) // >>> 1\n\n\tfor _, doc := range res5.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:3\n\t// STEP_END\n\n\t// Output:\n\t// 2\n\t// bicycle:1\n\t// bicycle:2\n\t// 1\n\t// bicycle:4\n\t// 2\n\t// bicycle:4\n\t// bicycle:6\n\t// 1\n\t// bicycle:3\n\t// 1\n\t// bicycle:3\n}\n"
  },
  {
    "path": "doctests/query_geo_test.go",
    "content": "// EXAMPLE: query_geo\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient_query_geo() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\t// HIDE_END\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.FTDropIndex(ctx, \"idx:bicycle\")\n\t// REMOVE_END\n\n\t_, err := rdb.FTCreate(ctx, \"idx:bicycle\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"bicycle:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.brand\",\n\t\t\tAs:        \"brand\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.model\",\n\t\t\tAs:        \"model\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.description\",\n\t\t\tAs:        \"description\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.price\",\n\t\t\tAs:        \"price\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.condition\",\n\t\t\tAs:        \"condition\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.store_location\",\n\t\t\tAs:        \"store_location\",\n\t\t\tFieldType: redis.SearchFieldTypeGeo,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName:         \"$.pickup_zone\",\n\t\t\tAs:                \"pickup_zone\",\n\t\t\tFieldType:         redis.SearchFieldTypeGeoShape,\n\t\t\tGeoShapeFieldType: \"FLAT\",\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\texampleJsons := []map[string]interface{}{\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, \" +\n\t\t\t\t\"-74.0610 40.6678, -74.0610 40.7578))\",\n\t\t\t\"store_location\": \"-74.0060,40.7128\",\n\t\t\t\"brand\":          \"Velorim\",\n\t\t\t\"model\":          \"Jigger\",\n\t\t\t\"price\":          270,\n\t\t\t\"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! \" +\n\t\t\t\t\"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger \" +\n\t\t\t\t\"is the vehicle of choice for the rare tenacious little rider raring to go.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, \" +\n\t\t\t\t\"-118.2887 33.9872, -118.2887 34.0972))\",\n\t\t\t\"store_location\": \"-118.2437,34.0522\",\n\t\t\t\"brand\":          \"Bicyk\",\n\t\t\t\"model\":          \"Hillcraft\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"Kids want to ride with as little weight as possible. Especially \" +\n\t\t\t\t\"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming \" +\n\t\t\t\t\"off a 24'' bike. The Hillcraft 26 is just the solution they need!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, \" +\n\t\t\t\t\"-87.6848 41.8231, -87.6848 41.9331))\",\n\t\t\t\"store_location\": \"-87.6298,41.8781\",\n\t\t\t\"brand\":          \"Nord\",\n\t\t\t\"model\":          \"Chook air 5\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"The Chook Air 5  gives kids aged six years and older a durable \" +\n\t\t\t\t\"and uberlight mountain bike for their first experience on tracks and easy cruising through \" +\n\t\t\t\t\"forests and fields. The lower  top tube makes it easy to mount and dismount in any \" +\n\t\t\t\t\"situation, giving your kids greater safety on the trails.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, \" +\n\t\t\t\t\"-80.2433 25.6967, -80.2433 25.8067))\",\n\t\t\t\"store_location\": \"-80.1918,25.7617\",\n\t\t\t\"brand\":          \"Eva\",\n\t\t\t\"model\":          \"Eva 291\",\n\t\t\t\"price\":          3400,\n\t\t\t\"description\": \"The sister company to Nord, Eva launched in 2005 as the first \" +\n\t\t\t\t\"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes \" +\n\t\t\t\t\"are optimized for the feminine physique using analytics from a body metrics database. \" +\n\t\t\t\t\"If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This \" +\n\t\t\t\t\"full-suspension, cross-country ride has been designed for velocity. The 291 has \" +\n\t\t\t\t\"100mm of front and rear travel, a superlight aluminum frame and fast-rolling \" +\n\t\t\t\t\"29-inch wheels. Yippee!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, \" +\n\t\t\t\t\"-122.4644 37.7099, -122.4644 37.8199))\",\n\t\t\t\"store_location\": \"-122.4194,37.7749\",\n\t\t\t\"brand\":          \"Noka Bikes\",\n\t\t\t\"model\":          \"Kahuna\",\n\t\t\t\"price\":          3200,\n\t\t\t\"description\": \"Whether you want to try your hand at XC racing or are looking \" +\n\t\t\t\t\"for a lively trail bike that's just as inspiring on the climbs as it is over rougher \" +\n\t\t\t\t\"ground, the Wilder is one heck of a bike built specifically for short women. Both the \" +\n\t\t\t\t\"frames and components have been tweaked to include a women’s saddle, different bars \" +\n\t\t\t\t\"and unique colourway.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, \" +\n\t\t\t\t\"-0.1778 51.4024, -0.1778 51.5524))\",\n\t\t\t\"store_location\": \"-0.1278,51.5074\",\n\t\t\t\"brand\":          \"Breakout\",\n\t\t\t\"model\":          \"XBN 2.1 Alloy\",\n\t\t\t\"price\":          810,\n\t\t\t\"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s \" +\n\t\t\t\t\"not to say that it’s a basic machine. With an internal weld aluminium frame, a full \" +\n\t\t\t\t\"carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which \" +\n\t\t\t\t\"doesn’t break the bank and delivers craved performance.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, \" +\n\t\t\t\t\"2.1767 48.5516, 2.1767 48.9016))\",\n\t\t\t\"store_location\": \"2.3522,48.8566\",\n\t\t\t\"brand\":          \"ScramBikes\",\n\t\t\t\"model\":          \"WattBike\",\n\t\t\t\"price\":          2300,\n\t\t\t\"description\": \"The WattBike is the best e-bike for people who still \" +\n\t\t\t\t\"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH \" +\n\t\t\t\t\"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one \" +\n\t\t\t\t\"charge. It’s great for tackling hilly terrain or if you just fancy a more \" +\n\t\t\t\t\"leisurely ride. With three working modes, you can choose between E-bike, \" +\n\t\t\t\t\"assisted bicycle, and normal bike modes.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, \" +\n\t\t\t\t\"13.3260 52.2700, 13.3260 52.5700))\",\n\t\t\t\"store_location\": \"13.4050,52.5200\",\n\t\t\t\"brand\":          \"Peaknetic\",\n\t\t\t\"model\":          \"Secto\",\n\t\t\t\"price\":          430,\n\t\t\t\"description\": \"If you struggle with stiff fingers or a kinked neck or \" +\n\t\t\t\t\"back after a few minutes on the road, this lightweight, aluminum bike alleviates \" +\n\t\t\t\t\"those issues and allows you to enjoy the ride. From the ergonomic grips to the \" +\n\t\t\t\t\"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. \" +\n\t\t\t\t\"The rear-inclined seat tube facilitates stability by allowing you to put a foot \" +\n\t\t\t\t\"on the ground to balance at a stop, and the low step-over frame makes it \" +\n\t\t\t\t\"accessible for all ability and mobility levels. The saddle is very soft, with \" +\n\t\t\t\t\"a wide back to support your hip joints and a cutout in the center to redistribute \" +\n\t\t\t\t\"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires \" +\n\t\t\t\t\"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts \" +\n\t\t\t\t\"facilitate setting up the Roll Low-Entry as your preferred commuter, and the \" +\n\t\t\t\t\"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, \" +\n\t\t\t\t\"1.9450 41.1987, 1.9450 41.4301))\",\n\t\t\t\"store_location\": \"2.1734, 41.3851\",\n\t\t\t\"brand\":          \"nHill\",\n\t\t\t\"model\":          \"Summit\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"This budget mountain bike from nHill performs well both \" +\n\t\t\t\t\"on bike paths and on the trail. The fork with 100mm of travel absorbs rough \" +\n\t\t\t\t\"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. \" +\n\t\t\t\t\"The Shimano Tourney drivetrain offered enough gears for finding a comfortable \" +\n\t\t\t\t\"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. \" +\n\t\t\t\t\"Whether you want an affordable bike that you can take to work, but also take \" +\n\t\t\t\t\"trail in mountains on the weekends or you’re just after a stable, comfortable \" +\n\t\t\t\t\"ride for the bike path, the Summit gives a good value for money.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, \" +\n\t\t\t\t\"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\",\n\t\t\t\"store_location\": \"12.4964,41.9028\",\n\t\t\t\"model\":          \"ThrillCycle\",\n\t\t\t\"brand\":          \"BikeShind\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"An artsy,  retro-inspired bicycle that’s as \" +\n\t\t\t\t\"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. \" +\n\t\t\t\t\"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t \" +\n\t\t\t\t\"suggest taking it to the mountains. Fenders protect you from mud, and a rear \" +\n\t\t\t\t\"basket lets you transport groceries, flowers and books. The ThrillCycle comes \" +\n\t\t\t\t\"with a limited lifetime warranty, so this little guy will last you long \" +\n\t\t\t\t\"past graduation.\",\n\t\t\t\"condition\": \"refurbished\",\n\t\t},\n\t}\n\n\tfor i, json := range exampleJsons {\n\t\t_, err := rdb.JSONSet(ctx, fmt.Sprintf(\"bicycle:%v\", i), \"$\", json).Result()\n\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// STEP_START geo1\n\tres1, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\", \"@store_location:[$lon $lat $radius $units]\",\n\t\t&redis.FTSearchOptions{\n\t\t\tParams: map[string]interface{}{\n\t\t\t\t\"lon\":    -0.1778,\n\t\t\t\t\"lat\":    51.5524,\n\t\t\t\t\"radius\": 20,\n\t\t\t\t\"units\":  \"mi\",\n\t\t\t},\n\t\t\tDialectVersion: 2,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1.Total) // >>> 1\n\n\tfor _, doc := range res1.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:5\n\t// STEP_END\n\n\t// STEP_START geo2\n\tres2, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\",\n\t\t\"@pickup_zone:[CONTAINS $bike]\",\n\t\t&redis.FTSearchOptions{\n\t\t\tParams: map[string]interface{}{\n\t\t\t\t\"bike\": \"POINT(-0.1278 51.5074)\",\n\t\t\t},\n\t\t\tDialectVersion: 3,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2.Total) // >>> 1\n\n\tfor _, doc := range res2.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:5\n\t// STEP_END\n\n\t// STEP_START geo3\n\tres3, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\",\n\t\t\"@pickup_zone:[WITHIN $europe]\",\n\t\t&redis.FTSearchOptions{\n\t\t\tParams: map[string]interface{}{\n\t\t\t\t\"europe\": \"POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))\",\n\t\t\t},\n\t\t\tDialectVersion: 3,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3.Total) // >>> 5\n\n\tsort.Slice(res3.Docs, func(i, j int) bool {\n\t\treturn res3.Docs[i].ID < res3.Docs[j].ID\n\t})\n\n\tfor _, doc := range res3.Docs {\n\t\tfmt.Println(doc.ID)\n\t}\n\t// >>> bicycle:5\n\t// >>> bicycle:6\n\t// >>> bicycle:7\n\t// >>> bicycle:8\n\t// >>> bicycle:9\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// bicycle:5\n\t// 1\n\t// bicycle:5\n\t// 5\n\t// bicycle:5\n\t// bicycle:6\n\t// bicycle:7\n\t// bicycle:8\n\t// bicycle:9\n}\n"
  },
  {
    "path": "doctests/query_range_test.go",
    "content": "// EXAMPLE: query_range\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient_query_range() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\n\t// HIDE_END\n\t// REMOVE_START\n\trdb.FTDropIndex(ctx, \"idx:bicycle\")\n\trdb.FTDropIndex(ctx, \"idx:email\")\n\t// REMOVE_END\n\n\t_, err := rdb.FTCreate(ctx, \"idx:bicycle\",\n\t\t&redis.FTCreateOptions{\n\t\t\tOnJSON: true,\n\t\t\tPrefix: []interface{}{\"bicycle:\"},\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.brand\",\n\t\t\tAs:        \"brand\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.model\",\n\t\t\tAs:        \"model\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.description\",\n\t\t\tAs:        \"description\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.price\",\n\t\t\tAs:        \"price\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t},\n\t\t&redis.FieldSchema{\n\t\t\tFieldName: \"$.condition\",\n\t\t\tAs:        \"condition\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\texampleJsons := []map[string]interface{}{\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, \" +\n\t\t\t\t\"-74.0610 40.6678, -74.0610 40.7578))\",\n\t\t\t\"store_location\": \"-74.0060,40.7128\",\n\t\t\t\"brand\":          \"Velorim\",\n\t\t\t\"model\":          \"Jigger\",\n\t\t\t\"price\":          270,\n\t\t\t\"description\": \"Small and powerful, the Jigger is the best ride for the smallest of tikes! \" +\n\t\t\t\t\"This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger \" +\n\t\t\t\t\"is the vehicle of choice for the rare tenacious little rider raring to go.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, \" +\n\t\t\t\t\"-118.2887 33.9872, -118.2887 34.0972))\",\n\t\t\t\"store_location\": \"-118.2437,34.0522\",\n\t\t\t\"brand\":          \"Bicyk\",\n\t\t\t\"model\":          \"Hillcraft\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"Kids want to ride with as little weight as possible. Especially \" +\n\t\t\t\t\"on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming \" +\n\t\t\t\t\"off a 24'' bike. The Hillcraft 26 is just the solution they need!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, \" +\n\t\t\t\t\"-87.6848 41.8231, -87.6848 41.9331))\",\n\t\t\t\"store_location\": \"-87.6298,41.8781\",\n\t\t\t\"brand\":          \"Nord\",\n\t\t\t\"model\":          \"Chook air 5\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"The Chook Air 5  gives kids aged six years and older a durable \" +\n\t\t\t\t\"and uberlight mountain bike for their first experience on tracks and easy cruising through \" +\n\t\t\t\t\"forests and fields. The lower  top tube makes it easy to mount and dismount in any \" +\n\t\t\t\t\"situation, giving your kids greater safety on the trails.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, \" +\n\t\t\t\t\"-80.2433 25.6967, -80.2433 25.8067))\",\n\t\t\t\"store_location\": \"-80.1918,25.7617\",\n\t\t\t\"brand\":          \"Eva\",\n\t\t\t\"model\":          \"Eva 291\",\n\t\t\t\"price\":          3400,\n\t\t\t\"description\": \"The sister company to Nord, Eva launched in 2005 as the first \" +\n\t\t\t\t\"and only women-dedicated bicycle brand. Designed by women for women, allEva bikes \" +\n\t\t\t\t\"are optimized for the feminine physique using analytics from a body metrics database. \" +\n\t\t\t\t\"If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This \" +\n\t\t\t\t\"full-suspension, cross-country ride has been designed for velocity. The 291 has \" +\n\t\t\t\t\"100mm of front and rear travel, a superlight aluminum frame and fast-rolling \" +\n\t\t\t\t\"29-inch wheels. Yippee!\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, \" +\n\t\t\t\t\"-122.4644 37.7099, -122.4644 37.8199))\",\n\t\t\t\"store_location\": \"-122.4194,37.7749\",\n\t\t\t\"brand\":          \"Noka Bikes\",\n\t\t\t\"model\":          \"Kahuna\",\n\t\t\t\"price\":          3200,\n\t\t\t\"description\": \"Whether you want to try your hand at XC racing or are looking \" +\n\t\t\t\t\"for a lively trail bike that's just as inspiring on the climbs as it is over rougher \" +\n\t\t\t\t\"ground, the Wilder is one heck of a bike built specifically for short women. Both the \" +\n\t\t\t\t\"frames and components have been tweaked to include a women’s saddle, different bars \" +\n\t\t\t\t\"and unique colourway.\",\n\t\t\t\"condition\": \"used\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, \" +\n\t\t\t\t\"-0.1778 51.4024, -0.1778 51.5524))\",\n\t\t\t\"store_location\": \"-0.1278,51.5074\",\n\t\t\t\"brand\":          \"Breakout\",\n\t\t\t\"model\":          \"XBN 2.1 Alloy\",\n\t\t\t\"price\":          810,\n\t\t\t\"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s \" +\n\t\t\t\t\"not to say that it’s a basic machine. With an internal weld aluminium frame, a full \" +\n\t\t\t\t\"carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which \" +\n\t\t\t\t\"doesn’t break the bank and delivers craved performance.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, \" +\n\t\t\t\t\"2.1767 48.5516, 2.1767 48.9016))\",\n\t\t\t\"store_location\": \"2.3522,48.8566\",\n\t\t\t\"brand\":          \"ScramBikes\",\n\t\t\t\"model\":          \"WattBike\",\n\t\t\t\"price\":          2300,\n\t\t\t\"description\": \"The WattBike is the best e-bike for people who still \" +\n\t\t\t\t\"feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH \" +\n\t\t\t\t\"Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one \" +\n\t\t\t\t\"charge. It’s great for tackling hilly terrain or if you just fancy a more \" +\n\t\t\t\t\"leisurely ride. With three working modes, you can choose between E-bike, \" +\n\t\t\t\t\"assisted bicycle, and normal bike modes.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, \" +\n\t\t\t\t\"13.3260 52.2700, 13.3260 52.5700))\",\n\t\t\t\"store_location\": \"13.4050,52.5200\",\n\t\t\t\"brand\":          \"Peaknetic\",\n\t\t\t\"model\":          \"Secto\",\n\t\t\t\"price\":          430,\n\t\t\t\"description\": \"If you struggle with stiff fingers or a kinked neck or \" +\n\t\t\t\t\"back after a few minutes on the road, this lightweight, aluminum bike alleviates \" +\n\t\t\t\t\"those issues and allows you to enjoy the ride. From the ergonomic grips to the \" +\n\t\t\t\t\"lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. \" +\n\t\t\t\t\"The rear-inclined seat tube facilitates stability by allowing you to put a foot \" +\n\t\t\t\t\"on the ground to balance at a stop, and the low step-over frame makes it \" +\n\t\t\t\t\"accessible for all ability and mobility levels. The saddle is very soft, with \" +\n\t\t\t\t\"a wide back to support your hip joints and a cutout in the center to redistribute \" +\n\t\t\t\t\"that pressure. Rim brakes deliver satisfactory braking control, and the wide tires \" +\n\t\t\t\t\"provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts \" +\n\t\t\t\t\"facilitate setting up the Roll Low-Entry as your preferred commuter, and the \" +\n\t\t\t\t\"BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, \" +\n\t\t\t\t\"1.9450 41.1987, 1.9450 41.4301))\",\n\t\t\t\"store_location\": \"2.1734, 41.3851\",\n\t\t\t\"brand\":          \"nHill\",\n\t\t\t\"model\":          \"Summit\",\n\t\t\t\"price\":          1200,\n\t\t\t\"description\": \"This budget mountain bike from nHill performs well both \" +\n\t\t\t\t\"on bike paths and on the trail. The fork with 100mm of travel absorbs rough \" +\n\t\t\t\t\"terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. \" +\n\t\t\t\t\"The Shimano Tourney drivetrain offered enough gears for finding a comfortable \" +\n\t\t\t\t\"pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. \" +\n\t\t\t\t\"Whether you want an affordable bike that you can take to work, but also take \" +\n\t\t\t\t\"trail in mountains on the weekends or you’re just after a stable, comfortable \" +\n\t\t\t\t\"ride for the bike path, the Summit gives a good value for money.\",\n\t\t\t\"condition\": \"new\",\n\t\t},\n\t\t{\n\t\t\t\"pickup_zone\": \"POLYGON((12.4464 42.1028, 12.5464 42.1028, \" +\n\t\t\t\t\"12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))\",\n\t\t\t\"store_location\": \"12.4964,41.9028\",\n\t\t\t\"model\":          \"ThrillCycle\",\n\t\t\t\"brand\":          \"BikeShind\",\n\t\t\t\"price\":          815,\n\t\t\t\"description\": \"An artsy,  retro-inspired bicycle that’s as \" +\n\t\t\t\t\"functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. \" +\n\t\t\t\t\"A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t \" +\n\t\t\t\t\"suggest taking it to the mountains. Fenders protect you from mud, and a rear \" +\n\t\t\t\t\"basket lets you transport groceries, flowers and books. The ThrillCycle comes \" +\n\t\t\t\t\"with a limited lifetime warranty, so this little guy will last you long \" +\n\t\t\t\t\"past graduation.\",\n\t\t\t\"condition\": \"refurbished\",\n\t\t},\n\t}\n\n\tfor i, json := range exampleJsons {\n\t\t_, err := rdb.JSONSet(ctx, fmt.Sprintf(\"bicycle:%v\", i), \"$\", json).Result()\n\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// STEP_START range1\n\tres1, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\", \"@price:[500 1000]\",\n\t\t&redis.FTSearchOptions{\n\t\t\tReturn: []redis.FTSearchReturn{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tSortBy: []redis.FTSearchSortBy{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t\tAsc:       true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1.Total) // >>> 3\n\n\tfor _, doc := range res1.Docs {\n\t\tfmt.Printf(\"%v : price %v\\n\", doc.ID, doc.Fields[\"price\"])\n\t}\n\t// >>> bicycle:2 : price 815\n\t// >>> bicycle:5 : price 810\n\t// >>> bicycle:9 : price 815\n\t// STEP_END\n\n\t// STEP_START range2\n\tres2, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\", \"*\",\n\t\t&redis.FTSearchOptions{\n\t\t\tFilters: []redis.FTSearchFilter{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t\tMin:       500,\n\t\t\t\t\tMax:       1000,\n\t\t\t\t},\n\t\t\t},\n\t\t\tReturn: []redis.FTSearchReturn{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tSortBy: []redis.FTSearchSortBy{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t\tAsc:       true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2.Total) // >>> 3\n\n\tfor _, doc := range res2.Docs {\n\t\tfmt.Printf(\"%v : price %v\\n\", doc.ID, doc.Fields[\"price\"])\n\t}\n\t// >>> bicycle:2 : price 815\n\t// >>> bicycle:5 : price 810\n\t// >>> bicycle:9 : price 815\n\t// STEP_END\n\n\t// STEP_START range3\n\tres3, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\", \"*\",\n\t\t&redis.FTSearchOptions{\n\t\t\tReturn: []redis.FTSearchReturn{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tSortBy: []redis.FTSearchSortBy{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t\tAsc:       true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tFilters: []redis.FTSearchFilter{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t\tMin:       \"(1000\",\n\t\t\t\t\tMax:       \"+inf\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3.Total) // >>> 5\n\n\tfor _, doc := range res3.Docs {\n\t\tfmt.Printf(\"%v : price %v\\n\", doc.ID, doc.Fields[\"price\"])\n\t}\n\t// >>> bicycle:1 : price 1200\n\t// >>> bicycle:4 : price 3200\n\t// >>> bicycle:6 : price 2300\n\t// >>> bicycle:3 : price 3400\n\t// >>> bicycle:8 : price 1200\n\t// STEP_END\n\n\t// STEP_START range4\n\tres4, err := rdb.FTSearchWithArgs(ctx,\n\t\t\"idx:bicycle\",\n\t\t\"@price:[-inf 2000]\",\n\t\t&redis.FTSearchOptions{\n\t\t\tReturn: []redis.FTSearchReturn{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tSortBy: []redis.FTSearchSortBy{\n\t\t\t\t{\n\t\t\t\t\tFieldName: \"price\",\n\t\t\t\t\tAsc:       true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       5,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4.Total) // >>> 7\n\n\tfor _, doc := range res4.Docs {\n\t\tfmt.Printf(\"%v : price %v\\n\", doc.ID, doc.Fields[\"price\"])\n\t}\n\t// >>> bicycle:0 : price 270\n\t// >>> bicycle:7 : price 430\n\t// >>> bicycle:5 : price 810\n\t// >>> bicycle:2 : price 815\n\t// >>> bicycle:9 : price 815\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// bicycle:5 : price 810\n\t// bicycle:2 : price 815\n\t// bicycle:9 : price 815\n\t// 3\n\t// bicycle:5 : price 810\n\t// bicycle:2 : price 815\n\t// bicycle:9 : price 815\n\t// 5\n\t// bicycle:1 : price 1200\n\t// bicycle:8 : price 1200\n\t// bicycle:6 : price 2300\n\t// bicycle:4 : price 3200\n\t// bicycle:3 : price 3400\n\t// 7\n\t// bicycle:0 : price 270\n\t// bicycle:7 : price 430\n\t// bicycle:5 : price 810\n\t// bicycle:2 : price 815\n\t// bicycle:9 : price 815\n}\n"
  },
  {
    "path": "doctests/search_quickstart_test.go",
    "content": "// EXAMPLE: search_quickstart\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nvar bicycles = []interface{}{\n\tmap[string]interface{}{\n\t\t\"brand\": \"Velorim\",\n\t\t\"model\": \"Jigger\",\n\t\t\"price\": 270,\n\t\t\"description\": \"Small and powerful, the Jigger is the best ride \" +\n\t\t\t\"for the smallest of tikes! This is the tiniest \" +\n\t\t\t\"kids’ pedal bike on the market available without\" +\n\t\t\t\" a coaster brake, the Jigger is the vehicle of \" +\n\t\t\t\"choice for the rare tenacious little rider \" +\n\t\t\t\"raring to go.\",\n\t\t\"condition\": \"new\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"Bicyk\",\n\t\t\"model\": \"Hillcraft\",\n\t\t\"price\": 1200,\n\t\t\"description\": \"Kids want to ride with as little weight as possible.\" +\n\t\t\t\" Especially on an incline! They may be at the age \" +\n\t\t\t\"when a 27.5\\\" wheel bike is just too clumsy coming \" +\n\t\t\t\"off a 24\\\" bike. The Hillcraft 26 is just the solution\" +\n\t\t\t\" they need!\",\n\t\t\"condition\": \"used\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"Nord\",\n\t\t\"model\": \"Chook air 5\",\n\t\t\"price\": 815,\n\t\t\"description\": \"The Chook Air 5  gives kids aged six years and older \" +\n\t\t\t\"a durable and uberlight mountain bike for their first\" +\n\t\t\t\" experience on tracks and easy cruising through forests\" +\n\t\t\t\" and fields. The lower  top tube makes it easy to mount\" +\n\t\t\t\" and dismount in any situation, giving your kids greater\" +\n\t\t\t\" safety on the trails.\",\n\t\t\"condition\": \"used\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"Eva\",\n\t\t\"model\": \"Eva 291\",\n\t\t\"price\": 3400,\n\t\t\"description\": \"The sister company to Nord, Eva launched in 2005 as the\" +\n\t\t\t\" first and only women-dedicated bicycle brand. Designed\" +\n\t\t\t\" by women for women, allEva bikes are optimized for the\" +\n\t\t\t\" feminine physique using analytics from a body metrics\" +\n\t\t\t\" database. If you like 29ers, try the Eva 291. It’s a \" +\n\t\t\t\"brand new bike for 2022.. This full-suspension, \" +\n\t\t\t\"cross-country ride has been designed for velocity. The\" +\n\t\t\t\" 291 has 100mm of front and rear travel, a superlight \" +\n\t\t\t\"aluminum frame and fast-rolling 29-inch wheels. Yippee!\",\n\t\t\"condition\": \"used\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"Noka Bikes\",\n\t\t\"model\": \"Kahuna\",\n\t\t\"price\": 3200,\n\t\t\"description\": \"Whether you want to try your hand at XC racing or are \" +\n\t\t\t\"looking for a lively trail bike that's just as inspiring\" +\n\t\t\t\" on the climbs as it is over rougher ground, the Wilder\" +\n\t\t\t\" is one heck of a bike built specifically for short women.\" +\n\t\t\t\" Both the frames and components have been tweaked to \" +\n\t\t\t\"include a women’s saddle, different bars and unique \" +\n\t\t\t\"colourway.\",\n\t\t\"condition\": \"used\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"Breakout\",\n\t\t\"model\": \"XBN 2.1 Alloy\",\n\t\t\"price\": 810,\n\t\t\"description\": \"The XBN 2.1 Alloy is our entry-level road bike – but that’s\" +\n\t\t\t\" not to say that it’s a basic machine. With an internal \" +\n\t\t\t\"weld aluminium frame, a full carbon fork, and the slick-shifting\" +\n\t\t\t\" Claris gears from Shimano’s, this is a bike which doesn’t\" +\n\t\t\t\" break the bank and delivers craved performance.\",\n\t\t\"condition\": \"new\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"ScramBikes\",\n\t\t\"model\": \"WattBike\",\n\t\t\"price\": 2300,\n\t\t\"description\": \"The WattBike is the best e-bike for people who still feel young\" +\n\t\t\t\" at heart. It has a Bafang 1000W mid-drive system and a 48V\" +\n\t\t\t\" 17.5AH Samsung Lithium-Ion battery, allowing you to ride for\" +\n\t\t\t\" more than 60 miles on one charge. It’s great for tackling hilly\" +\n\t\t\t\" terrain or if you just fancy a more leisurely ride. With three\" +\n\t\t\t\" working modes, you can choose between E-bike, assisted bicycle,\" +\n\t\t\t\" and normal bike modes.\",\n\t\t\"condition\": \"new\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"Peaknetic\",\n\t\t\"model\": \"Secto\",\n\t\t\"price\": 430,\n\t\t\"description\": \"If you struggle with stiff fingers or a kinked neck or back after\" +\n\t\t\t\" a few minutes on the road, this lightweight, aluminum bike\" +\n\t\t\t\" alleviates those issues and allows you to enjoy the ride. From\" +\n\t\t\t\" the ergonomic grips to the lumbar-supporting seat position, the\" +\n\t\t\t\" Roll Low-Entry offers incredible comfort. The rear-inclined seat\" +\n\t\t\t\" tube facilitates stability by allowing you to put a foot on the\" +\n\t\t\t\" ground to balance at a stop, and the low step-over frame makes it\" +\n\t\t\t\" accessible for all ability and mobility levels. The saddle is\" +\n\t\t\t\" very soft, with a wide back to support your hip joints and a\" +\n\t\t\t\" cutout in the center to redistribute that pressure. Rim brakes\" +\n\t\t\t\" deliver satisfactory braking control, and the wide tires provide\" +\n\t\t\t\" a smooth, stable ride on paved roads and gravel. Rack and fender\" +\n\t\t\t\" mounts facilitate setting up the Roll Low-Entry as your preferred\" +\n\t\t\t\" commuter, and the BMX-like handlebar offers space for mounting a\" +\n\t\t\t\" flashlight, bell, or phone holder.\",\n\t\t\"condition\": \"new\",\n\t},\n\tmap[string]interface{}{\n\t\t\"brand\": \"nHill\",\n\t\t\"model\": \"Summit\",\n\t\t\"price\": 1200,\n\t\t\"description\": \"This budget mountain bike from nHill performs well both on bike\" +\n\t\t\t\" paths and on the trail. The fork with 100mm of travel absorbs\" +\n\t\t\t\" rough terrain. Fat Kenda Booster tires give you grip in corners\" +\n\t\t\t\" and on wet trails. The Shimano Tourney drivetrain offered enough\" +\n\t\t\t\" gears for finding a comfortable pace to ride uphill, and the\" +\n\t\t\t\" Tektro hydraulic disc brakes break smoothly. Whether you want an\" +\n\t\t\t\" affordable bike that you can take to work, but also take trail in\" +\n\t\t\t\" mountains on the weekends or you’re just after a stable,\" +\n\t\t\t\" comfortable ride for the bike path, the Summit gives a good value\" +\n\t\t\t\" for money.\",\n\t\t\"condition\": \"new\",\n\t},\n\tmap[string]interface{}{\n\t\t\"model\": \"ThrillCycle\",\n\t\t\"brand\": \"BikeShind\",\n\t\t\"price\": 815,\n\t\t\"description\": \"An artsy,  retro-inspired bicycle that’s as functional as it is\" +\n\t\t\t\" pretty: The ThrillCycle steel frame offers a smooth ride. A\" +\n\t\t\t\" 9-speed drivetrain has enough gears for coasting in the city, but\" +\n\t\t\t\" we wouldn’t suggest taking it to the mountains. Fenders protect\" +\n\t\t\t\" you from mud, and a rear basket lets you transport groceries,\" +\n\t\t\t\" flowers and books. The ThrillCycle comes with a limited lifetime\" +\n\t\t\t\" warranty, so this little guy will last you long past graduation.\",\n\t\t\"condition\": \"refurbished\",\n\t},\n}\n\nfunc ExampleClient_search_qs() {\n\t// STEP_START connect\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\t// STEP_END\n\n\t// REMOVE_START\n\trdb.FTDropIndex(ctx, \"idx:bicycle\")\n\t// REMOVE_END\n\n\t// STEP_START create_index\n\tschema := []*redis.FieldSchema{\n\t\t{\n\t\t\tFieldName: \"$.brand\",\n\t\t\tAs:        \"brand\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t{\n\t\t\tFieldName: \"$.model\",\n\t\t\tAs:        \"model\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t\t{\n\t\t\tFieldName: \"$.description\",\n\t\t\tAs:        \"description\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t},\n\t}\n\n\t_, err := rdb.FTCreate(ctx, \"idx:bicycle\",\n\t\t&redis.FTCreateOptions{\n\t\t\tPrefix: []interface{}{\"bicycle:\"},\n\t\t\tOnJSON: true,\n\t\t},\n\t\tschema...,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// STEP_END\n\n\t// STEP_START add_documents\n\tfor i, bicycle := range bicycles {\n\t\t_, err := rdb.JSONSet(\n\t\t\tctx,\n\t\t\tfmt.Sprintf(\"bicycle:%v\", i),\n\t\t\t\"$\",\n\t\t\tbicycle,\n\t\t).Result()\n\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\t// STEP_END\n\n\t// STEP_START wildcard_query\n\twCardResult, err := rdb.FTSearch(ctx, \"idx:bicycle\", \"*\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Documents found: %v\\n\", wCardResult.Total)\n\t// >>> Documents found: 10\n\t// STEP_END\n\n\t// STEP_START query_single_term\n\tstResult, err := rdb.FTSearch(\n\t\tctx,\n\t\t\"idx:bicycle\",\n\t\t\"@model:Jigger\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(stResult)\n\t// >>> {1 [{bicycle:0 <nil> <nil> <nil> map[$:{\"brand\":\"Velorim\", ...\n\t// STEP_END\n\n\t// STEP_START query_exact_matching\n\texactMatchResult, err := rdb.FTSearch(\n\t\tctx,\n\t\t\"idx:bicycle\",\n\t\t\"@brand:\\\"Noka Bikes\\\"\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(exactMatchResult)\n\t// >>> {1 [{bicycle:4 <nil> <nil> <nil> map[$:{\"brand\":\"Noka Bikes\"...\n\t// STEP_END\n\n\t// Output:\n\t// Documents found: 10\n\t// {1 [{bicycle:0 <nil> <nil> <nil> map[$:{\"brand\":\"Velorim\",\"condition\":\"new\",\"description\":\"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.\",\"model\":\"Jigger\",\"price\":270}] <nil>}]}\n\t// {1 [{bicycle:4 <nil> <nil> <nil> map[$:{\"brand\":\"Noka Bikes\",\"condition\":\"used\",\"description\":\"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.\",\"model\":\"Kahuna\",\"price\":3200}] <nil>}]}\n}\n"
  },
  {
    "path": "doctests/set_get_test.go",
    "content": "// EXAMPLE: set_and_get\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc ExampleClient_Set_and_get() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// HIDE_END\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\terrFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test\n\tif errFlush != nil {\n\t\tpanic(errFlush)\n\t}\n\t// REMOVE_END\n\n\terr := rdb.Set(ctx, \"bike:1\", \"Process 134\", 0).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"OK\")\n\n\tvalue, err := rdb.Get(ctx, \"bike:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"The name of the bike is %s\", value)\n\t// HIDE_START\n\n\t// Output: OK\n\t// The name of the bike is Process 134\n}\n\n// HIDE_END\n"
  },
  {
    "path": "doctests/sets_example_test.go",
    "content": "// EXAMPLE: sets_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\nfunc ExampleClient_sadd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\trdb.Del(ctx, \"bikes:racing:usa\")\n\t// REMOVE_END\n\n\t// STEP_START sadd\n\tres1, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> 1\n\n\tres2, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> 0\n\n\tres3, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> 2\n\n\tres4, err := rdb.SAdd(ctx, \"bikes:racing:usa\", \"bike:1\", \"bike:4\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> 2\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 0\n\t// 2\n\t// 2\n}\n\nfunc ExampleClient_sismember() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\trdb.Del(ctx, \"bikes:racing:usa\")\n\t// REMOVE_END\n\n\t_, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.SAdd(ctx, \"bikes:racing:usa\", \"bike:1\", \"bike:4\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START sismember\n\tres5, err := rdb.SIsMember(ctx, \"bikes:racing:usa\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> true\n\n\tres6, err := rdb.SIsMember(ctx, \"bikes:racing:usa\", \"bike:2\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> false\n\t// STEP_END\n\n\t// Output:\n\t// true\n\t// false\n}\n\nfunc ExampleClient_sinter() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\trdb.Del(ctx, \"bikes:racing:usa\")\n\t// REMOVE_END\n\n\t_, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.SAdd(ctx, \"bikes:racing:usa\", \"bike:1\", \"bike:4\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START sinter\n\tres7, err := rdb.SInter(ctx, \"bikes:racing:france\", \"bikes:racing:usa\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> [bike:1]\n\t// STEP_END\n\n\t// Output:\n\t// [bike:1]\n}\n\nfunc ExampleClient_scard() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\t// REMOVE_END\n\n\t_, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START scard\n\tres8, err := rdb.SCard(ctx, \"bikes:racing:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> 3\n\t// STEP_END\n\n\t// Output:\n\t// 3\n}\n\nfunc ExampleClient_saddsmembers() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\t// REMOVE_END\n\n\t// STEP_START sadd_smembers\n\tres9, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> 3\n\n\tres10, err := rdb.SMembers(ctx, \"bikes:racing:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Sort the strings in the slice to make sure the output is lexicographical\n\tsort.Strings(res10)\n\n\tfmt.Println(res10) // >>> [bike:1 bike:2 bike:3]\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// [bike:1 bike:2 bike:3]\n}\n\nfunc ExampleClient_smismember() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\t// REMOVE_END\n\n\t_, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START smismember\n\tres11, err := rdb.SIsMember(ctx, \"bikes:racing:france\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> true\n\n\tres12, err := rdb.SMIsMember(ctx, \"bikes:racing:france\", \"bike:2\", \"bike:3\", \"bike:4\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> [true true false]\n\t// STEP_END\n\n\t// Output:\n\t// true\n\t// [true true false]\n}\n\nfunc ExampleClient_sdiff() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\trdb.Del(ctx, \"bikes:racing:usa\")\n\t// REMOVE_END\n\n\t// STEP_START sdiff\n\t_, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.SAdd(ctx, \"bikes:racing:usa\", \"bike:1\", \"bike:4\").Result()\n\n\tres13, err := rdb.SDiff(ctx, \"bikes:racing:france\", \"bikes:racing:usa\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Sort the strings in the slice to make sure the output is lexicographical\n\tsort.Strings(res13)\n\n\tfmt.Println(res13) // >>> [bike:2 bike:3]\n\t// STEP_END\n\n\t// Output:\n\t// [bike:2 bike:3]\n}\n\nfunc ExampleClient_multisets() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\trdb.Del(ctx, \"bikes:racing:usa\")\n\trdb.Del(ctx, \"bikes:racing:italy\")\n\t// REMOVE_END\n\n\t// STEP_START multisets\n\t_, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.SAdd(ctx, \"bikes:racing:usa\", \"bike:1\", \"bike:4\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.SAdd(ctx, \"bikes:racing:italy\", \"bike:1\", \"bike:2\", \"bike:3\", \"bike:4\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres14, err := rdb.SInter(ctx, \"bikes:racing:france\", \"bikes:racing:usa\", \"bikes:racing:italy\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14) // >>> [bike:1]\n\n\tres15, err := rdb.SUnion(ctx, \"bikes:racing:france\", \"bikes:racing:usa\", \"bikes:racing:italy\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Sort the strings in the slice to make sure the output is lexicographical\n\tsort.Strings(res15)\n\n\tfmt.Println(res15) // >>> [bike:1 bike:2 bike:3 bike:4]\n\n\tres16, err := rdb.SDiff(ctx, \"bikes:racing:france\", \"bikes:racing:usa\", \"bikes:racing:italy\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16) // >>> []\n\n\tres17, err := rdb.SDiff(ctx, \"bikes:racing:usa\", \"bikes:racing:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res17) // >>> [bike:4]\n\n\tres18, err := rdb.SDiff(ctx, \"bikes:racing:france\", \"bikes:racing:usa\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Sort the strings in the slice to make sure the output is lexicographical\n\tsort.Strings(res18)\n\n\tfmt.Println(res18) // >>> [bike:2 bike:3]\n\t// STEP_END\n\n\t// Output:\n\t// [bike:1]\n\t// [bike:1 bike:2 bike:3 bike:4]\n\t// []\n\t// [bike:4]\n\t// [bike:2 bike:3]\n}\n\nfunc ExampleClient_srem() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:racing:france\")\n\t// REMOVE_END\n\n\t// STEP_START srem\n\t_, err := rdb.SAdd(ctx, \"bikes:racing:france\", \"bike:1\", \"bike:2\", \"bike:3\", \"bike:4\", \"bike:5\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres19, err := rdb.SRem(ctx, \"bikes:racing:france\", \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // >>> 1\n\n\tres20, err := rdb.SPop(ctx, \"bikes:racing:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> <random element>\n\n\tres21, err := rdb.SMembers(ctx, \"bikes:racing:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res21) // >>> <remaining elements>\n\n\tres22, err := rdb.SRandMember(ctx, \"bikes:racing:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res22) // >>> <random element>\n\t// STEP_END\n\n\t// Testable examples not available because the test output\n\t// is not deterministic.\n}\n"
  },
  {
    "path": "doctests/ss_tutorial_test.go",
    "content": "// EXAMPLE: ss_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\nfunc ExampleClient_zadd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t// STEP_START zadd\n\tres1, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 10},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> 1\n\n\tres2, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Castilla\", Score: 12},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> 1\n\n\tres3, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 10},\n\t\tredis.Z{Member: \"Sam-Bodden\", Score: 8},\n\t\tredis.Z{Member: \"Royce\", Score: 10},\n\t\tredis.Z{Member: \"Ford\", Score: 6},\n\t\tredis.Z{Member: \"Prickett\", Score: 14},\n\t\tredis.Z{Member: \"Castilla\", Score: 12},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> 4\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 1\n\t// 4\n}\n\nfunc ExampleClient_zrange() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t_, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 10},\n\t\tredis.Z{Member: \"Sam-Bodden\", Score: 8},\n\t\tredis.Z{Member: \"Royce\", Score: 10},\n\t\tredis.Z{Member: \"Ford\", Score: 6},\n\t\tredis.Z{Member: \"Prickett\", Score: 14},\n\t\tredis.Z{Member: \"Castilla\", Score: 12},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START zrange\n\tres4, err := rdb.ZRange(ctx, \"racer_scores\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4)\n\t// >>> [Ford Sam-Bodden Norem Royce Castilla Prickett]\n\n\tres5, err := rdb.ZRevRange(ctx, \"racer_scores\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5)\n\t// >>> [Prickett Castilla Royce Norem Sam-Bodden Ford]\n\t// STEP_END\n\n\t// Output:\n\t// [Ford Sam-Bodden Norem Royce Castilla Prickett]\n\t// [Prickett Castilla Royce Norem Sam-Bodden Ford]\n}\n\nfunc ExampleClient_zrangewithscores() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t_, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 10},\n\t\tredis.Z{Member: \"Sam-Bodden\", Score: 8},\n\t\tredis.Z{Member: \"Royce\", Score: 10},\n\t\tredis.Z{Member: \"Ford\", Score: 6},\n\t\tredis.Z{Member: \"Prickett\", Score: 14},\n\t\tredis.Z{Member: \"Castilla\", Score: 12},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START zrange_withscores\n\tres6, err := rdb.ZRangeWithScores(ctx, \"racer_scores\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6)\n\t// >>> [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}]\n\t// STEP_END\n\n\t// Output:\n\t// [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}]\n}\n\nfunc ExampleClient_zrangebyscore() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t_, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 10},\n\t\tredis.Z{Member: \"Sam-Bodden\", Score: 8},\n\t\tredis.Z{Member: \"Royce\", Score: 10},\n\t\tredis.Z{Member: \"Ford\", Score: 6},\n\t\tredis.Z{Member: \"Prickett\", Score: 14},\n\t\tredis.Z{Member: \"Castilla\", Score: 12},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START zrangebyscore\n\tres7, err := rdb.ZRangeByScore(ctx, \"racer_scores\",\n\t\t&redis.ZRangeBy{Min: \"-inf\", Max: \"10\"},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7)\n\t// >>> [Ford Sam-Bodden Norem Royce]\n\t// STEP_END\n\n\t// Output:\n\t// [Ford Sam-Bodden Norem Royce]\n}\n\nfunc ExampleClient_zremrangebyscore() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t_, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 10},\n\t\tredis.Z{Member: \"Sam-Bodden\", Score: 8},\n\t\tredis.Z{Member: \"Royce\", Score: 10},\n\t\tredis.Z{Member: \"Ford\", Score: 6},\n\t\tredis.Z{Member: \"Prickett\", Score: 14},\n\t\tredis.Z{Member: \"Castilla\", Score: 12},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START zremrangebyscore\n\tres8, err := rdb.ZRem(ctx, \"racer_scores\", \"Castilla\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> 1\n\n\tres9, err := rdb.ZRemRangeByScore(ctx, \"racer_scores\", \"-inf\", \"9\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> 2\n\n\tres10, err := rdb.ZRange(ctx, \"racer_scores\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10)\n\t// >>> [Norem Royce Prickett]\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 2\n\t// [Norem Royce Prickett]\n}\n\nfunc ExampleClient_zrank() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t_, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 10},\n\t\tredis.Z{Member: \"Royce\", Score: 10},\n\t\tredis.Z{Member: \"Prickett\", Score: 14},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START zrank\n\tres11, err := rdb.ZRank(ctx, \"racer_scores\", \"Norem\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> 0\n\n\tres12, err := rdb.ZRevRank(ctx, \"racer_scores\", \"Norem\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> 2\n\t// STEP_END\n\n\t// Output:\n\t// 0\n\t// 2\n}\n\nfunc ExampleClient_zaddlex() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t_, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 0},\n\t\tredis.Z{Member: \"Royce\", Score: 0},\n\t\tredis.Z{Member: \"Prickett\", Score: 0},\n\t).Result()\n\n\t// STEP_START zadd_lex\n\tres13, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Norem\", Score: 0},\n\t\tredis.Z{Member: \"Sam-Bodden\", Score: 0},\n\t\tredis.Z{Member: \"Royce\", Score: 0},\n\t\tredis.Z{Member: \"Ford\", Score: 0},\n\t\tredis.Z{Member: \"Prickett\", Score: 0},\n\t\tredis.Z{Member: \"Castilla\", Score: 0},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13) // >>> 3\n\n\tres14, err := rdb.ZRange(ctx, \"racer_scores\", 0, -1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14)\n\t// >>> [Castilla Ford Norem Prickett Royce Sam-Bodden]\n\n\tres15, err := rdb.ZRangeByLex(ctx, \"racer_scores\", &redis.ZRangeBy{\n\t\tMin: \"[A\", Max: \"[L\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res15) // >>> [Castilla Ford]\n\t// STEP_END\n\n\t// Output:\n\t// 3\n\t// [Castilla Ford Norem Prickett Royce Sam-Bodden]\n\t// [Castilla Ford]\n}\n\nfunc ExampleClient_leaderboard() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_scores\")\n\t// REMOVE_END\n\n\t// STEP_START leaderboard\n\tres16, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Wood\", Score: 100},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16) // >>> 1\n\n\tres17, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Henshaw\", Score: 100},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res17) // >>> 1\n\n\tres18, err := rdb.ZAdd(ctx, \"racer_scores\",\n\t\tredis.Z{Member: \"Henshaw\", Score: 150},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res18) // >>> 0\n\n\tres19, err := rdb.ZIncrBy(ctx, \"racer_scores\", 50, \"Wood\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // >>> 150\n\n\tres20, err := rdb.ZIncrBy(ctx, \"racer_scores\", 50, \"Henshaw\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> 200\n\t// STEP_END\n\n\t// Output:\n\t// 1\n\t// 1\n\t// 0\n\t// 150\n\t// 200\n}\n"
  },
  {
    "path": "doctests/stream_tutorial_test.go",
    "content": "// EXAMPLE: stream_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\n// REMOVE_START\nfunc UNUSED(v ...interface{}) {}\n\n// REMOVE_END\n\nfunc ExampleClient_xadd() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:france\")\n\t// REMOVE_END\n\n\t// STEP_START xadd\n\tres1, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Castilla\",\n\t\t\t\"speed\":       30.2,\n\t\t\t\"position\":    1,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.Println(res1) // >>> 1692632086370-0\n\n\tres2, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Norem\",\n\t\t\t\"speed\":       28.8,\n\t\t\t\"position\":    3,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.PrintLn(res2) // >>> 1692632094485-0\n\n\tres3, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Prickett\",\n\t\t\t\"speed\":       29.7,\n\t\t\t\"position\":    2,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.Println(res3) // >>> 1692632102976-0\n\t// STEP_END\n\n\t// REMOVE_START\n\tUNUSED(res1, res2, res3)\n\t// REMOVE_END\n\n\txlen, err := rdb.XLen(ctx, \"race:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(xlen) // >>> 3\n\n\t// Output:\n\t// 3\n}\n\nfunc ExampleClient_racefrance1() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:france\")\n\t// REMOVE_END\n\n\t_, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Castilla\",\n\t\t\t\"speed\":       30.2,\n\t\t\t\"position\":    1,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t\tID: \"1692632086370-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Norem\",\n\t\t\t\"speed\":       28.8,\n\t\t\t\"position\":    3,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t\tID: \"1692632094485-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Prickett\",\n\t\t\t\"speed\":       29.7,\n\t\t\t\"position\":    2,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t\tID: \"1692632102976-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START xrange\n\tres4, err := rdb.XRangeN(ctx, \"race:france\", \"1691765278160-0\", \"+\", 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4)\n\t// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla...\n\t// STEP_END\n\n\t// STEP_START xread_block\n\tres5, err := rdb.XRead(ctx, &redis.XReadArgs{\n\t\tStreams: []string{\"race:france\", \"0\"},\n\t\tCount:   100,\n\t\tBlock:   300,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5)\n\t// >>> // [{race:france [{1692632086370-0 map[location_id:1 position:1...\n\t// STEP_END\n\n\t// STEP_START xadd_2\n\tres6, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Castilla\",\n\t\t\t\"speed\":       29.9,\n\t\t\t\"position\":    1,\n\t\t\t\"location_id\": 2,\n\t\t},\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t//fmt.Println(res6) // >>> 1692632147973-0\n\t// STEP_END\n\n\t// STEP_START xlen\n\tres7, err := rdb.XLen(ctx, \"race:france\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> 4\n\t// STEP_END\n\n\t// REMOVE_START\n\tUNUSED(res6)\n\t// REMOVE_END\n\n\t// Output:\n\t// [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]\n\t// [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0} {1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7] 0 0}]}]\n\t// 4\n}\n\nfunc ExampleClient_raceusa() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:usa\")\n\t// REMOVE_END\n\n\t// STEP_START xadd_id\n\tres8, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:usa\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"racer\": \"Castilla\",\n\t\t},\n\t\tID: \"0-1\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> 0-1\n\n\tres9, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:usa\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"racer\": \"Norem\",\n\t\t},\n\t\tID: \"0-2\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> 0-2\n\t// STEP_END\n\n\t// STEP_START xadd_bad_id\n\tres10, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tValues: map[string]interface{}{\n\t\t\t\"racer\": \"Prickett\",\n\t\t},\n\t\tID: \"0-1\",\n\t}).Result()\n\n\tif err != nil {\n\t\t// fmt.Println(err)\n\t\t// >>> ERR The ID specified in XADD is equal or smaller than the target stream top item\n\t}\n\t// STEP_END\n\n\t// STEP_START xadd_7\n\tres11, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:usa\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"racer\": \"Prickett\",\n\t\t},\n\t\tID: \"0-*\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> 0-3\n\t// STEP_END\n\n\t// REMOVE_START\n\tUNUSED(res10)\n\t// REMOVE_END\n\n\t// Output:\n\t// 0-1\n\t// 0-2\n\t// 0-3\n}\n\nfunc ExampleClient_racefrance2() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:france\")\n\t// REMOVE_END\n\n\t_, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Castilla\",\n\t\t\t\"speed\":       30.2,\n\t\t\t\"position\":    1,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t\tID: \"1692632086370-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Norem\",\n\t\t\t\"speed\":       28.8,\n\t\t\t\"position\":    3,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t\tID: \"1692632094485-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Prickett\",\n\t\t\t\"speed\":       29.7,\n\t\t\t\"position\":    2,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t\tID: \"1692632102976-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Castilla\",\n\t\t\t\"speed\":       29.9,\n\t\t\t\"position\":    1,\n\t\t\t\"location_id\": 2,\n\t\t},\n\t\tID: \"1692632147973-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// STEP_START xrange_all\n\tres12, err := rdb.XRange(ctx, \"race:france\", \"-\", \"+\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12)\n\t// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla...\n\t// STEP_END\n\n\t// STEP_START xrange_time\n\tres13, err := rdb.XRange(ctx, \"race:france\",\n\t\t\"1692632086369\", \"1692632086371\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13)\n\t// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0}]\n\t// STEP_END\n\n\t// STEP_START xrange_step_1\n\tres14, err := rdb.XRangeN(ctx, \"race:france\", \"-\", \"+\", 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14)\n\t// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]\n\t// STEP_END\n\n\t// STEP_START xrange_step_2\n\tres15, err := rdb.XRangeN(ctx, \"race:france\",\n\t\t\"(1692632094485-0\", \"+\", 2,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res15)\n\t// >>> [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7] 0 0} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]\n\t// STEP_END\n\n\t// STEP_START xrange_empty\n\tres16, err := rdb.XRangeN(ctx, \"race:france\",\n\t\t\"(1692632147973-0\", \"+\", 2,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16)\n\t// >>> []\n\t// STEP_END\n\n\t// STEP_START xrevrange\n\tres17, err := rdb.XRevRangeN(ctx, \"race:france\", \"+\", \"-\", 1).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res17)\n\t// >>> [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]\n\t// STEP_END\n\n\t// STEP_START xread\n\tres18, err := rdb.XRead(ctx, &redis.XReadArgs{\n\t\tStreams: []string{\"race:france\", \"0\"},\n\t\tCount:   2,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res18)\n\t// >>> [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]}]\n\t// STEP_END\n\n\t// Output:\n\t// [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0} {1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7] 0 0} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]\n\t// [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0}]\n\t// [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]\n\t// [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7] 0 0} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]\n\t// []\n\t// [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]\n\t// [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]}]\n}\n\nfunc ExampleClient_xgroupcreate() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:france\")\n\t// REMOVE_END\n\n\t_, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:france\",\n\t\tValues: map[string]interface{}{\n\t\t\t\"rider\":       \"Castilla\",\n\t\t\t\"speed\":       30.2,\n\t\t\t\"position\":    1,\n\t\t\t\"location_id\": 1,\n\t\t},\n\t\tID: \"1692632086370-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START xgroup_create\n\tres19, err := rdb.XGroupCreate(ctx, \"race:france\", \"france_riders\", \"$\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // >>> OK\n\t// STEP_END\n\n\t// Output:\n\t// OK\n}\n\nfunc ExampleClient_xgroupcreatemkstream() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:italy\")\n\t// REMOVE_END\n\n\t// STEP_START xgroup_create_mkstream\n\tres20, err := rdb.XGroupCreateMkStream(ctx,\n\t\t\"race:italy\", \"italy_riders\", \"$\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> OK\n\t// STEP_END\n\n\t// Output:\n\t// OK\n}\n\nfunc ExampleClient_xgroupread() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:italy\")\n\t// REMOVE_END\n\n\t_, err := rdb.XGroupCreateMkStream(ctx,\n\t\t\"race:italy\", \"italy_riders\", \"$\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START xgroup_read\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Castilla\"},\n\t}).Result()\n\t// >>> 1692632639151-0\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Royce\"},\n\t}).Result()\n\t// >>> 1692632647899-0\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Sam-Bodden\"},\n\t}).Result()\n\t// >>> 1692632662819-0\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Prickett\"},\n\t}).Result()\n\t// >>> 1692632670501-0\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Norem\"},\n\t}).Result()\n\t// >>> 1692632678249-0\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.Println(res25)\n\n\tres21, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\tStreams:  []string{\"race:italy\", \">\"},\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Alice\",\n\t\tCount:    1,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.Println(res21)\n\t// >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}]\n\t// STEP_END\n\n\t// REMOVE_START\n\tUNUSED(res21)\n\t// REMOVE_END\n\n\txlen, err := rdb.XLen(ctx, \"race:italy\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(xlen)\n\n\t// Output:\n\t// 5\n}\n\nfunc ExampleClient_raceitaly() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:italy\")\n\trdb.XGroupDestroy(ctx, \"race:italy\", \"italy_riders\")\n\t// REMOVE_END\n\n\t_, err := rdb.XGroupCreateMkStream(ctx,\n\t\t\"race:italy\", \"italy_riders\", \"$\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Castilla\"},\n\t\tID:     \"1692632639151-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Royce\"},\n\t\tID:     \"1692632647899-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Sam-Bodden\"},\n\t\tID:     \"1692632662819-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Prickett\"},\n\t\tID:     \"1692632670501-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tValues: map[string]interface{}{\"rider\": \"Norem\"},\n\t\tID:     \"1692632678249-0\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\tStreams:  []string{\"race:italy\", \">\"},\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Alice\",\n\t\tCount:    1,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// STEP_START xgroup_read_id\n\tres22, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\tStreams:  []string{\"race:italy\", \"0\"},\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Alice\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res22)\n\t// >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}]\n\t// STEP_END\n\n\t// STEP_START xack\n\tres23, err := rdb.XAck(ctx,\n\t\t\"race:italy\", \"italy_riders\", \"1692632639151-0\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res23) // >>> 1\n\n\tres24, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\tStreams:  []string{\"race:italy\", \"0\"},\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Alice\",\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res24)\n\t// >>> [{race:italy []}]\n\t// STEP_END\n\n\t// STEP_START xgroup_read_bob\n\tres25, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{\n\t\tStreams:  []string{\"race:italy\", \">\"},\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Bob\",\n\t\tCount:    2,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res25)\n\t// >>> [{race:italy [{1692632647899-0 map[rider:Royce] 0 0} {1692632662819-0 map[rider:Sam-Bodden] 0 0}]}]\n\n\t// STEP_END\n\n\t// STEP_START xpending\n\tres26, err := rdb.XPending(ctx, \"race:italy\", \"italy_riders\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res26)\n\t// >>> &{2 1692632647899-0 1692632662819-0 map[Bob:2]}\n\t// STEP_END\n\n\t// STEP_START xpending_plus_minus\n\tres27, err := rdb.XPendingExt(ctx, &redis.XPendingExtArgs{\n\t\tStream: \"race:italy\",\n\t\tGroup:  \"italy_riders\",\n\t\tStart:  \"-\",\n\t\tEnd:    \"+\",\n\t\tCount:  10,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.Println(res27)\n\t// >>> [{1692632647899-0 Bob 0s 1} {1692632662819-0 Bob 0s 1}]\n\t// STEP_END\n\n\t// STEP_START xrange_pending\n\tres28, err := rdb.XRange(ctx, \"race:italy\",\n\t\t\"1692632647899-0\", \"1692632647899-0\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res28) // >>> [{1692632647899-0 map[rider:Royce] 0 0}]\n\t// STEP_END\n\n\t// STEP_START xclaim\n\tres29, err := rdb.XClaim(ctx, &redis.XClaimArgs{\n\t\tStream:   \"race:italy\",\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Alice\",\n\t\tMinIdle:  0,\n\t\tMessages: []string{\"1692632647899-0\"},\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res29)\n\t// STEP_END\n\n\t// STEP_START xautoclaim\n\tres30, res30a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{\n\t\tStream:   \"race:italy\",\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Alice\",\n\t\tStart:    \"0-0\",\n\t\tCount:    1,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res30)  // >>> [{1692632647899-0 map[rider:Royce] 0 0}]\n\tfmt.Println(res30a) // >>> 1692632662819-0\n\t// STEP_END\n\n\t// STEP_START xautoclaim_cursor\n\tres31, res31a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{\n\t\tStream:   \"race:italy\",\n\t\tGroup:    \"italy_riders\",\n\t\tConsumer: \"Lora\",\n\t\tStart:    \"(1692632662819-0\",\n\t\tCount:    1,\n\t}).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res31)  // >>> []\n\tfmt.Println(res31a) // >>> 0-0\n\t// STEP_END\n\n\t// STEP_START xinfo\n\tres32, err := rdb.XInfoStream(ctx, \"race:italy\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res32.Length)\n\t// >>> 5\n\tfmt.Println(res32.FirstEntry)\n\t// >>> {1692632639151-0 map[rider:Castilla] 0 0}\n\t// STEP_END\n\n\t// STEP_START xinfo_groups\n\tres33, err := rdb.XInfoGroups(ctx, \"race:italy\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res33)\n\t// >>> [{italy_riders 3 2 1692632662819-0 3 2}]\n\t// STEP_END\n\n\t// STEP_START xinfo_consumers\n\tres34, err := rdb.XInfoConsumers(ctx, \"race:italy\", \"italy_riders\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.Println(res34)\n\t// >>> [{Alice 1 1ms 1ms} {Bob 1 2ms 2ms} {Lora 0 1ms -1ms}]\n\t// STEP_END\n\n\t// STEP_START maxlen\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tMaxLen: 2,\n\t\tValues: map[string]interface{}{\"rider\": \"Jones\"},\n\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tMaxLen: 2,\n\t\tValues: map[string]interface{}{\"rider\": \"Wood\"},\n\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tMaxLen: 2,\n\t\tValues: map[string]interface{}{\"rider\": \"Henshaw\"},\n\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres35, err := rdb.XLen(ctx, \"race:italy\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res35) // >>> 2\n\n\tres36, err := rdb.XRange(ctx, \"race:italy\", \"-\", \"+\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// fmt.Println(res36)\n\t// >>> [{1726649529170-1 map[rider:Wood] 0 0} {1726649529171-0 map[rider:Henshaw] 0 0}]\n\t// STEP_END\n\n\t// STEP_START xtrim\n\tres37, err := rdb.XTrimMaxLen(ctx, \"race:italy\", 10).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res37) // >>> 0\n\t// STEP_END\n\n\t// STEP_START xtrim2\n\tres38, err := rdb.XTrimMaxLenApprox(ctx, \"race:italy\", 10, 20).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res38) // >>> 0\n\t// STEP_END\n\n\t// REMOVE_START\n\tUNUSED(res27, res34, res36)\n\t// REMOVE_END\n\n\t// Output:\n\t// [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}]\n\t// 1\n\t// [{race:italy []}]\n\t// [{race:italy [{1692632647899-0 map[rider:Royce] 0 0} {1692632662819-0 map[rider:Sam-Bodden] 0 0}]}]\n\t// &{2 1692632647899-0 1692632662819-0 map[Bob:2]}\n\t// [{1692632647899-0 map[rider:Royce] 0 0}]\n\t// [{1692632647899-0 map[rider:Royce] 0 0}]\n\t// [{1692632647899-0 map[rider:Royce] 0 0}]\n\t// 1692632662819-0\n\t// []\n\t// 0-0\n\t// 5\n\t// {1692632639151-0 map[rider:Castilla] 0 0}\n\t// [{italy_riders 3 2 1692632662819-0 3 2}]\n\t// 2\n\t// 0\n\t// 0\n}\n\nfunc ExampleClient_xdel() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"race:italy\")\n\t// REMOVE_END\n\n\t_, err := rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tMaxLen: 2,\n\t\tValues: map[string]interface{}{\"rider\": \"Wood\"},\n\t\tID:     \"1692633198206-0\",\n\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.XAdd(ctx, &redis.XAddArgs{\n\t\tStream: \"race:italy\",\n\t\tMaxLen: 2,\n\t\tValues: map[string]interface{}{\"rider\": \"Henshaw\"},\n\t\tID:     \"1692633208557-0\",\n\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START xdel\n\tres39, err := rdb.XRangeN(ctx, \"race:italy\", \"-\", \"+\", 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res39)\n\t// >>> [{1692633198206-0 map[rider:Wood] 0 0} {1692633208557-0 map[rider:Henshaw] 0 0}]\n\n\tres40, err := rdb.XDel(ctx, \"race:italy\", \"1692633208557-0\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res40) // 1\n\n\tres41, err := rdb.XRangeN(ctx, \"race:italy\", \"-\", \"+\", 2).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res41)\n\t// >>> [{1692633198206-0 map[rider:Wood] 0 0}]\n\t// STEP_END\n\n\t// Output:\n\t// [{1692633198206-0 map[rider:Wood] 0 0} {1692633208557-0 map[rider:Henshaw] 0 0}]\n\t// 1\n\t// [{1692633198206-0 map[rider:Wood] 0 0}]\n}\n"
  },
  {
    "path": "doctests/string_example_test.go",
    "content": "// EXAMPLE: set_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\nfunc ExampleClient_set_get() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike:1\")\n\t// REMOVE_END\n\n\t// STEP_START set_get\n\tres1, err := rdb.Set(ctx, \"bike:1\", \"Deimos\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.Get(ctx, \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> Deimos\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// Deimos\n}\n\nfunc ExampleClient_setnx_xx() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Set(ctx, \"bike:1\", \"Deimos\", 0)\n\t// REMOVE_END\n\n\t// STEP_START setnx_xx\n\tres3, err := rdb.SetNX(ctx, \"bike:1\", \"bike\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> false\n\n\tres4, err := rdb.Get(ctx, \"bike:1\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> Deimos\n\n\tres5, err := rdb.SetXX(ctx, \"bike:1\", \"bike\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> OK\n\t// STEP_END\n\n\t// Output:\n\t// false\n\t// Deimos\n\t// true\n}\n\nfunc ExampleClient_mset() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bike:1\", \"bike:2\", \"bike:3\")\n\t// REMOVE_END\n\n\t// STEP_START mset\n\tres6, err := rdb.MSet(ctx, \"bike:1\", \"Deimos\", \"bike:2\", \"Ares\", \"bike:3\", \"Vanth\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> OK\n\n\tres7, err := rdb.MGet(ctx, \"bike:1\", \"bike:2\", \"bike:3\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> [Deimos Ares Vanth]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [Deimos Ares Vanth]\n}\n\nfunc ExampleClient_incr() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"total_crashes\")\n\t// REMOVE_END\n\n\t// STEP_START incr\n\tres8, err := rdb.Set(ctx, \"total_crashes\", \"0\", 0).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> OK\n\n\tres9, err := rdb.Incr(ctx, \"total_crashes\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> 1\n\n\tres10, err := rdb.IncrBy(ctx, \"total_crashes\", 10).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> 11\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// 1\n\t// 11\n}\n"
  },
  {
    "path": "doctests/tdigest_tutorial_test.go",
    "content": "// EXAMPLE: tdigest_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_tdigstart() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_ages\", \"bikes:sales\")\n\t// REMOVE_END\n\n\t// STEP_START tdig_start\n\tres1, err := rdb.TDigestCreate(ctx, \"bikes:sales\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.TDigestAdd(ctx, \"bikes:sales\", 21).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> OK\n\n\tres3, err := rdb.TDigestAdd(ctx, \"bikes:sales\",\n\t\t150, 95, 75, 34,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> OK\n\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// OK\n}\n\nfunc ExampleClient_tdigcdf() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_ages\", \"bikes:sales\")\n\t// REMOVE_END\n\n\t// STEP_START tdig_cdf\n\tres4, err := rdb.TDigestCreate(ctx, \"racer_ages\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> OK\n\n\tres5, err := rdb.TDigestAdd(ctx, \"racer_ages\",\n\t\t45.88, 44.2, 58.03, 19.76, 39.84, 69.28,\n\t\t50.97, 25.41, 19.27, 85.71, 42.63,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> OK\n\n\tres6, err := rdb.TDigestRank(ctx, \"racer_ages\", 50).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> [7]\n\n\tres7, err := rdb.TDigestRank(ctx, \"racer_ages\", 50, 40).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> [7 4]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// [7]\n\t// [7 4]\n}\n\nfunc ExampleClient_tdigquant() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_ages\")\n\t// REMOVE_END\n\n\t_, err := rdb.TDigestCreate(ctx, \"racer_ages\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.TDigestAdd(ctx, \"racer_ages\",\n\t\t45.88, 44.2, 58.03, 19.76, 39.84, 69.28,\n\t\t50.97, 25.41, 19.27, 85.71, 42.63,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START tdig_quant\n\tres8, err := rdb.TDigestQuantile(ctx, \"racer_ages\", 0.5).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> [44.2]\n\n\tres9, err := rdb.TDigestByRank(ctx, \"racer_ages\", 4).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> [42.63]\n\t// STEP_END\n\n\t// Output:\n\t// [44.2]\n\t// [42.63]\n}\n\nfunc ExampleClient_tdigmin() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_ages\")\n\t// REMOVE_END\n\n\t_, err := rdb.TDigestCreate(ctx, \"racer_ages\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.TDigestAdd(ctx, \"racer_ages\",\n\t\t45.88, 44.2, 58.03, 19.76, 39.84, 69.28,\n\t\t50.97, 25.41, 19.27, 85.71, 42.63,\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START tdig_min\n\tres10, err := rdb.TDigestMin(ctx, \"racer_ages\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> 19.27\n\n\tres11, err := rdb.TDigestMax(ctx, \"racer_ages\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> 85.71\n\t// STEP_END\n\n\t// Output:\n\t// 19.27\n\t// 85.71\n}\n\nfunc ExampleClient_tdigreset() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"racer_ages\")\n\t// REMOVE_END\n\t_, err := rdb.TDigestCreate(ctx, \"racer_ages\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START tdig_reset\n\tres12, err := rdb.TDigestReset(ctx, \"racer_ages\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> OK\n\t// STEP_END\n\n\t// Output:\n\t// OK\n}\n"
  },
  {
    "path": "doctests/timeseries_tut_test.go",
    "content": "// EXAMPLE: time_series_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\n// mapKeys returns a slice of all keys from the map (Go 1.21 compatible)\n// TODO: Once minimum Go version is upgraded to 1.23+, replace with slices.Collect(maps.Keys(m))\nfunc mapKeys[K comparable, V any](m map[K]V) []K {\n\tkeys := make([]K, 0, len(m))\n\tfor k := range m {\n\t\tkeys = append(keys, k)\n\t}\n\treturn keys\n}\n\nfunc ExampleClient_timeseries_create() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"thermometer:1\", \"thermometer:2\", \"thermometer:3\")\n\t// REMOVE_END\n\n\t// STEP_START create\n\tres1, err := rdb.TSCreate(ctx, \"thermometer:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.Type(ctx, \"thermometer:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> TSDB-TYPE\n\n\tres3, err := rdb.TSInfo(ctx, \"thermometer:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3[\"totalSamples\"]) // >>> 0\n\t// STEP_END\n\n\t// STEP_START create_retention\n\tres4, err := rdb.TSAddWithArgs(\n\t\tctx,\n\t\t\"thermometer:2\",\n\t\t1,\n\t\t10.8,\n\t\t&redis.TSOptions{\n\t\t\tRetention: 100,\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> 1\n\n\tres5, err := rdb.TSInfo(ctx, \"thermometer:2\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5[\"retentionTime\"]) // >>> 100\n\t// STEP_END\n\n\t// STEP_START create_labels\n\tres6, err := rdb.TSAddWithArgs(\n\t\tctx,\n\t\t\"thermometer:3\",\n\t\t1,\n\t\t10.4,\n\t\t&redis.TSOptions{\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"location\": \"UK\",\n\t\t\t\t\"type\":     \"Mercury\",\n\t\t\t},\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> 1\n\n\tres7, err := rdb.TSInfo(ctx, \"thermometer:3\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7[\"labels\"])\n\t// >>> map[location:UK type:Mercury]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// TSDB-TYPE\n\t// 0\n\t// 1\n\t// 100\n\t// 1\n\t// map[location:UK type:Mercury]\n}\n\nfunc ExampleClient_timeseries_add() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"thermometer:1\", \"thermometer:2\")\n\trdb.TSCreate(ctx, \"thermometer:1\")\n\trdb.TSCreate(ctx, \"thermometer:2\")\n\t// REMOVE_END\n\n\t// STEP_START madd\n\tres1, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"thermometer:1\", 1, 9.2},\n\t\t{\"thermometer:1\", 2, 9.9},\n\t\t{\"thermometer:2\", 2, 10.3},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> [1 2 2]\n\t// STEP_END\n\n\t// STEP_START get\n\t// The last recorded temperature for thermometer:2\n\t// was 10.3 at time 2.\n\tres2, err := rdb.TSGet(ctx, \"thermometer:2\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2)\n\t// >>> {2 10.3}\n\t// STEP_END\n\n\t// Output:\n\t// [1 2 2]\n\t// {2 10.3}\n}\n\nfunc ExampleClient_timeseries_range() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"rg:1\")\n\t// REMOVE_END\n\n\t// STEP_START range\n\t// Add 5 data points to a time series named \"rg:1\".\n\tres1, err := rdb.TSCreate(ctx, \"rg:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"rg:1\", 0, 18},\n\t\t{\"rg:1\", 1, 14},\n\t\t{\"rg:1\", 2, 22},\n\t\t{\"rg:1\", 3, 18},\n\t\t{\"rg:1\", 4, 24},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> [0 1 2 3 4]\n\n\t// Retrieve all the data points in ascending order.\n\t// Note: use 0 and `math.MaxInt64` instead of - and +\n\t// to denote the minimum and maximum possible timestamps.\n\tres3, err := rdb.TSRange(ctx, \"rg:1\", 0, math.MaxInt64).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3)\n\t// >>> [{0 18} {1 14} {2 22} {3 18} {4 24}]\n\n\t// Retrieve data points up to time 1 (inclusive).\n\tres4, err := rdb.TSRange(ctx, \"rg:1\", 0, 1).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4)\n\t// >>> [{0 18} {1 14}]\n\n\t// Retrieve data points from time 3 onwards.\n\tres5, err := rdb.TSRange(ctx, \"rg:1\", 3, math.MaxInt64).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5)\n\t// >>> [{3 18} {4 24}]\n\n\t// Retrieve all the data points in descending order.\n\tres6, err := rdb.TSRevRange(ctx, \"rg:1\", 0, math.MaxInt64).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6)\n\t// >>> [{4 24} {3 18} {2 22} {1 14} {0 18}]\n\n\t// Retrieve data points up to time 1 (inclusive), but return them\n\t// in descending order.\n\tres7, err := rdb.TSRevRange(ctx, \"rg:1\", 0, 1).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7)\n\t// >>> [{1 14} {0 18}]\n\t// STEP_END\n\n\t// STEP_START range_filter\n\tres8, err := rdb.TSRangeWithArgs(\n\t\tctx,\n\t\t\"rg:1\",\n\t\t0,\n\t\tmath.MaxInt64,\n\t\t&redis.TSRangeOptions{\n\t\t\tFilterByTS: []int{0, 2, 4},\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> [{0 18} {2 22} {4 24}]\n\n\tres9, err := rdb.TSRevRangeWithArgs(\n\t\tctx,\n\t\t\"rg:1\",\n\t\t0,\n\t\tmath.MaxInt64,\n\t\t&redis.TSRevRangeOptions{\n\t\t\tFilterByTS:    []int{0, 2, 4},\n\t\t\tFilterByValue: []int{20, 25},\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> [{4 24} {2 22}]\n\n\tres10, err := rdb.TSRevRangeWithArgs(\n\t\tctx,\n\t\t\"rg:1\",\n\t\t0,\n\t\tmath.MaxInt64,\n\t\t&redis.TSRevRangeOptions{\n\t\t\tFilterByTS:    []int{0, 2, 4},\n\t\t\tFilterByValue: []int{22, 22},\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> [{2 22}]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [0 1 2 3 4]\n\t// [{0 18} {1 14} {2 22} {3 18} {4 24}]\n\t// [{0 18} {1 14}]\n\t// [{3 18} {4 24}]\n\t// [{4 24} {3 18} {2 22} {1 14} {0 18}]\n\t// [{1 14} {0 18}]\n\t// [{0 18} {2 22} {4 24}]\n\t// [{4 24} {2 22}]\n\t// [{2 22}]\n}\n\nfunc ExampleClient_timeseries_query_multi() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"rg:2\", \"rg:3\", \"rg:4\")\n\t// REMOVE_END\n\n\t// STEP_START query_multi\n\t// Create three new \"rg:\" time series (two in the US\n\t// and one in the UK, with different units) and add some\n\t// data points.\n\tres20, err := rdb.TSCreateWithArgs(ctx, \"rg:2\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"location\": \"us\", \"unit\": \"cm\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> OK\n\n\tres21, err := rdb.TSCreateWithArgs(ctx, \"rg:3\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"location\": \"us\", \"unit\": \"in\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res21) // >>> OK\n\n\tres22, err := rdb.TSCreateWithArgs(ctx, \"rg:4\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"location\": \"uk\", \"unit\": \"mm\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res22) // >>> OK\n\n\tres23, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"rg:2\", 0, 1.8},\n\t\t{\"rg:3\", 0, 0.9},\n\t\t{\"rg:4\", 0, 25},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res23) // >>> [0 0 0]\n\n\tres24, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"rg:2\", 1, 2.1},\n\t\t{\"rg:3\", 1, 0.77},\n\t\t{\"rg:4\", 1, 18},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res24) // >>> [1 1 1]\n\n\tres25, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"rg:2\", 2, 2.3},\n\t\t{\"rg:3\", 2, 1.1},\n\t\t{\"rg:4\", 2, 21},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res25) // >>> [2 2 2]\n\n\tres26, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"rg:2\", 3, 1.9},\n\t\t{\"rg:3\", 3, 0.81},\n\t\t{\"rg:4\", 3, 19},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res26) // >>> [3 3 3]\n\n\tres27, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"rg:2\", 4, 1.78},\n\t\t{\"rg:3\", 4, 0.74},\n\t\t{\"rg:4\", 4, 23},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res27) // >>> [4 4 4]\n\n\t// Retrieve the last data point from each US time series.\n\tres28, err := rdb.TSMGet(ctx, []string{\"location=us\"}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres28Keys := mapKeys(res28)\n\tsort.Strings(res28Keys)\n\n\tfor _, k := range res28Keys {\n\t\tlabels := res28[k][0].(map[interface{}]interface{})\n\n\t\tlabelKeys := make([]string, 0, len(labels))\n\n\t\tfor lk := range labels {\n\t\t\tlabelKeys = append(labelKeys, lk.(string))\n\t\t}\n\n\t\tsort.Strings(labelKeys)\n\n\t\tfmt.Printf(\"%v:\\n\", k)\n\n\t\tfor _, lk := range labelKeys {\n\t\t\tfmt.Printf(\"  %v: %v\\n\", lk, labels[lk])\n\t\t}\n\n\t\tfmt.Printf(\"  %v\\n\", res28[k][1])\n\t}\n\t// >>> rg:2:\n\t// >>>   {4 1.78}\n\t// >>> rg:3:\n\t// >>>   {4 0.74}\n\n\t// Retrieve the same data points, but include the `unit`\n\t// label in the results.\n\tres29, err := rdb.TSMGetWithArgs(\n\t\tctx,\n\t\t[]string{\"location=us\"},\n\t\t&redis.TSMGetOptions{\n\t\t\tSelectedLabels: []interface{}{\"unit\"},\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres29Keys := mapKeys(res29)\n\tsort.Strings(res29Keys)\n\n\tfor _, k := range res29Keys {\n\t\tlabels := res29[k][0].(map[interface{}]interface{})\n\n\t\tlabelKeys := make([]string, 0, len(labels))\n\n\t\tfor lk := range labels {\n\t\t\tlabelKeys = append(labelKeys, lk.(string))\n\t\t}\n\n\t\tsort.Strings(labelKeys)\n\n\t\tfmt.Printf(\"%v:\\n\", k)\n\n\t\tfor _, lk := range labelKeys {\n\t\t\tfmt.Printf(\"  %v: %v\\n\", lk, labels[lk])\n\t\t}\n\n\t\tfmt.Printf(\"  %v\\n\", res29[k][1])\n\t}\n\n\t// >>> rg:2:\n\t// >>>   unit: cm\n\t// >>>   [4 1.78]\n\t// >>> rg:3:\n\t// >>>   unit: in\n\t// >>>   [4 0.74]\n\n\t// Retrieve data points up to time 2 (inclusive) from all\n\t// time series that use millimeters as the unit. Include all\n\t// labels in the results.\n\t// Note that the `aggregators` field is empty if you don't\n\t// specify any aggregators.\n\tres30, err := rdb.TSMRangeWithArgs(\n\t\tctx,\n\t\t0,\n\t\t2,\n\t\t[]string{\"unit=mm\"},\n\t\t&redis.TSMRangeOptions{\n\t\t\tWithLabels: true,\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres30Keys := mapKeys(res30)\n\tsort.Strings(res30Keys)\n\n\tfor _, k := range res30Keys {\n\t\tlabels := res30[k][0].(map[interface{}]interface{})\n\t\tlabelKeys := make([]string, 0, len(labels))\n\n\t\tfor lk := range labels {\n\t\t\tlabelKeys = append(labelKeys, lk.(string))\n\t\t}\n\n\t\tsort.Strings(labelKeys)\n\n\t\tfmt.Printf(\"%v:\\n\", k)\n\n\t\tfor _, lk := range labelKeys {\n\t\t\tfmt.Printf(\"  %v: %v\\n\", lk, labels[lk])\n\t\t}\n\n\t\tfmt.Printf(\"  Aggregators: %v\\n\", res30[k][1])\n\t\tfmt.Printf(\"  %v\\n\", res30[k][2])\n\t}\n\t// >>> rg:4:\n\t// >>>   location: uk\n\t// >>>   unit: mm\n\t// >>>   Aggregators: map[aggregators:[]]\n\t// >>>   [{0 25} {1 18} {2 21}]\n\n\t// Retrieve data points from time 1 to time 3 (inclusive) from\n\t// all time series that use centimeters or millimeters as the unit,\n\t// but only return the `location` label. Return the results\n\t// in descending order of timestamp.\n\tres31, err := rdb.TSMRevRangeWithArgs(\n\t\tctx,\n\t\t1,\n\t\t3,\n\t\t[]string{\"unit=(cm,mm)\"},\n\t\t&redis.TSMRevRangeOptions{\n\t\t\tSelectedLabels: []interface{}{\"location\"},\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres31Keys := mapKeys(res31)\n\tsort.Strings(res31Keys)\n\n\tfor _, k := range res31Keys {\n\t\tlabels := res31[k][0].(map[interface{}]interface{})\n\t\tlabelKeys := make([]string, 0, len(labels))\n\n\t\tfor lk := range labels {\n\t\t\tlabelKeys = append(labelKeys, lk.(string))\n\t\t}\n\n\t\tsort.Strings(labelKeys)\n\n\t\tfmt.Printf(\"%v:\\n\", k)\n\n\t\tfor _, lk := range labelKeys {\n\t\t\tfmt.Printf(\"  %v: %v\\n\", lk, labels[lk])\n\t\t}\n\n\t\tfmt.Printf(\"  Aggregators: %v\\n\", res31[k][1])\n\t\tfmt.Printf(\"  %v\\n\", res31[k][2])\n\t}\n\t// >>> rg:2:\n\t// >>>   location: us\n\t// >>>   Aggregators: map[aggregators:[]]\n\t// >>>   [{3 1.9} {2 2.3} {1 2.1}]\n\t// >>> rg:4:\n\t// >>>   location: uk\n\t// >>>   Aggregators: map[aggregators:[]]\n\t// >>>   [{3 19} {2 21} {1 18}]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// OK\n\t// [0 0 0]\n\t// [1 1 1]\n\t// [2 2 2]\n\t// [3 3 3]\n\t// [4 4 4]\n\t// rg:2:\n\t//   [4 1.78]\n\t// rg:3:\n\t//   [4 0.74]\n\t// rg:2:\n\t//   unit: cm\n\t//   [4 1.78]\n\t// rg:3:\n\t//   unit: in\n\t//   [4 0.74]\n\t// rg:4:\n\t//   location: uk\n\t//   unit: mm\n\t//   Aggregators: map[aggregators:[]]\n\t//   [[0 25] [1 18] [2 21]]\n\t// rg:2:\n\t//   location: us\n\t//   Aggregators: map[aggregators:[]]\n\t//   [[3 1.9] [2 2.3] [1 2.1]]\n\t// rg:4:\n\t//   location: uk\n\t//   Aggregators: map[aggregators:[]]\n\t//   [[3 19] [2 21] [1 18]]\n}\n\nfunc ExampleClient_timeseries_aggregation() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"rg:2\")\n\t// REMOVE_END\n\n\t// Setup data for aggregation example\n\t_, err := rdb.TSCreateWithArgs(ctx, \"rg:2\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"location\": \"us\", \"unit\": \"cm\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"rg:2\", 0, 1.8},\n\t\t{\"rg:2\", 1, 2.1},\n\t\t{\"rg:2\", 2, 2.3},\n\t\t{\"rg:2\", 3, 1.9},\n\t\t{\"rg:2\", 4, 1.78},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// STEP_START agg\n\tres32, err := rdb.TSRangeWithArgs(\n\t\tctx,\n\t\t\"rg:2\",\n\t\t0,\n\t\tmath.MaxInt64,\n\t\t&redis.TSRangeOptions{\n\t\t\tAggregator:     redis.Avg,\n\t\t\tBucketDuration: 2,\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res32)\n\t// >>> [{0 1.9500000000000002} {2 2.0999999999999996} {4 1.78}]\n\t// STEP_END\n\n\t// Output:\n\t// [{0 1.9500000000000002} {2 2.0999999999999996} {4 1.78}]\n}\nfunc ExampleClient_timeseries_agg_bucket() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"sensor3\")\n\t// REMOVE_END\n\n\t// STEP_START agg_bucket\n\tres1, err := rdb.TSCreate(ctx, \"sensor3\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"sensor3\", 10, 1000},\n\t\t{\"sensor3\", 20, 2000},\n\t\t{\"sensor3\", 30, 3000},\n\t\t{\"sensor3\", 40, 4000},\n\t\t{\"sensor3\", 50, 5000},\n\t\t{\"sensor3\", 60, 6000},\n\t\t{\"sensor3\", 70, 7000},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> [10 20 30 40 50 60 70]\n\n\tres3, err := rdb.TSRangeWithArgs(\n\t\tctx,\n\t\t\"sensor3\",\n\t\t10,\n\t\t70,\n\t\t&redis.TSRangeOptions{\n\t\t\tAggregator:     redis.Min,\n\t\t\tBucketDuration: 25,\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> [{0 1000} {25 3000} {50 5000}]\n\t// STEP_END\n\n\t// STEP_START agg_align\n\tres4, err := rdb.TSRangeWithArgs(\n\t\tctx,\n\t\t\"sensor3\",\n\t\t10,\n\t\t70,\n\t\t&redis.TSRangeOptions{\n\t\t\tAggregator:     redis.Min,\n\t\t\tBucketDuration: 25,\n\t\t\tAlign:          \"START\",\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> [{10 1000} {35 4000} {60 6000}]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [10 20 30 40 50 60 70]\n\t// [{0 1000} {25 3000} {50 5000}]\n\t// [{10 1000} {35 4000} {60 6000}]\n}\n\nfunc ExampleClient_timeseries_aggmulti() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"wind:1\", \"wind:2\", \"wind:3\", \"wind:4\")\n\t// REMOVE_END\n\n\t// STEP_START agg_multi\n\tres37, err := rdb.TSCreateWithArgs(ctx, \"wind:1\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"country\": \"uk\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res37) // >>> OK\n\n\tres38, err := rdb.TSCreateWithArgs(ctx, \"wind:2\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"country\": \"uk\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res38) // >>> OK\n\n\tres39, err := rdb.TSCreateWithArgs(ctx, \"wind:3\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"country\": \"us\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res39) // >>> OK\n\n\tres40, err := rdb.TSCreateWithArgs(ctx, \"wind:4\", &redis.TSOptions{\n\t\tLabels: map[string]string{\"country\": \"us\"},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res40) // >>> OK\n\n\tres41, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"wind:1\", 1, 12},\n\t\t{\"wind:2\", 1, 18},\n\t\t{\"wind:3\", 1, 5},\n\t\t{\"wind:4\", 1, 20},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res41) // >>> [1 1 1 1]\n\n\tres42, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"wind:1\", 2, 14},\n\t\t{\"wind:2\", 2, 21},\n\t\t{\"wind:3\", 2, 4},\n\t\t{\"wind:4\", 2, 25},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res42) // >>> [2 2 2 2]\n\n\tres43, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"wind:1\", 3, 10},\n\t\t{\"wind:2\", 3, 24},\n\t\t{\"wind:3\", 3, 8},\n\t\t{\"wind:4\", 3, 18},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res43) // >>> [3 3 3 3]\n\n\t// The result pairs contain the timestamp and the maximum sample value\n\t// for the country at that timestamp.\n\tres44, err := rdb.TSMRangeWithArgs(\n\t\tctx,\n\t\t0,\n\t\tmath.MaxInt64,\n\t\t[]string{\"country=(us,uk)\"},\n\t\t&redis.TSMRangeOptions{\n\t\t\tGroupByLabel: \"country\",\n\t\t\tReducer:      \"max\",\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres44Keys := mapKeys(res44)\n\tsort.Strings(res44Keys)\n\n\tfor _, k := range res44Keys {\n\t\tlabels := res44[k][0].(map[interface{}]interface{})\n\t\tlabelKeys := make([]string, 0, len(labels))\n\n\t\tfor lk := range labels {\n\t\t\tlabelKeys = append(labelKeys, lk.(string))\n\t\t}\n\n\t\tsort.Strings(labelKeys)\n\n\t\tfmt.Printf(\"%v:\\n\", k)\n\n\t\tfor _, lk := range labelKeys {\n\t\t\tfmt.Printf(\"  %v: %v\\n\", lk, labels[lk])\n\t\t}\n\n\t\tfmt.Printf(\"  %v\\n\", res44[k][1])\n\t\tfmt.Printf(\"  %v\\n\", res44[k][2])\n\t\tfmt.Printf(\"  %v\\n\", res44[k][3])\n\t}\n\t// >>> country=uk:\n\t// >>>   map[reducers:[max]]\n\t// >>>   map[sources:[wind:1 wind:2]]\n\t// >>>   [[1 18] [2 21] [3 24]]\n\t// >>> country=us:\n\t// >>>   map[reducers:[max]]\n\t// >>>   map[sources:[wind:3 wind:4]]\n\t// >>>   [[1 20] [2 25] [3 18]]\n\n\t// The result pairs contain the timestamp and the average sample value\n\t// for the country at that timestamp.\n\tres45, err := rdb.TSMRangeWithArgs(\n\t\tctx,\n\t\t0,\n\t\tmath.MaxInt64,\n\t\t[]string{\"country=(us,uk)\"},\n\t\t&redis.TSMRangeOptions{\n\t\t\tGroupByLabel: \"country\",\n\t\t\tReducer:      \"avg\",\n\t\t},\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres45Keys := mapKeys(res45)\n\tsort.Strings(res45Keys)\n\n\tfor _, k := range res45Keys {\n\t\tlabels := res45[k][0].(map[interface{}]interface{})\n\t\tlabelKeys := make([]string, 0, len(labels))\n\n\t\tfor lk := range labels {\n\t\t\tlabelKeys = append(labelKeys, lk.(string))\n\t\t}\n\n\t\tsort.Strings(labelKeys)\n\n\t\tfmt.Printf(\"%v:\\n\", k)\n\n\t\tfor _, lk := range labelKeys {\n\t\t\tfmt.Printf(\"  %v: %v\\n\", lk, labels[lk])\n\t\t}\n\n\t\tfmt.Printf(\"  %v\\n\", res45[k][1])\n\t\tfmt.Printf(\"  %v\\n\", res45[k][2])\n\t\tfmt.Printf(\"  %v\\n\", res45[k][3])\n\t}\n\t// >>> country=uk:\n\t// >>>   map[reducers:[avg]]\n\t// >>>   map[sources:[wind:1 wind:2]]\n\t// >>>   [[1 15] [2 17.5] [3 17]]\n\t// >>> country=us:\n\t// >>>   map[reducers:[avg]]\n\t// >>>   map[sources:[wind:3 wind:4]]\n\t// >>>   [[1 12.5] [2 14.5] [3 13]]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// OK\n\t// OK\n\t// [1 1 1 1]\n\t// [2 2 2 2]\n\t// [3 3 3 3]\n\t// country=uk:\n\t//   map[reducers:[max]]\n\t//   map[sources:[wind:1 wind:2]]\n\t//   [[1 18] [2 21] [3 24]]\n\t// country=us:\n\t//   map[reducers:[max]]\n\t//   map[sources:[wind:3 wind:4]]\n\t//   [[1 20] [2 25] [3 18]]\n\t// country=uk:\n\t//   map[reducers:[avg]]\n\t//   map[sources:[wind:1 wind:2]]\n\t//   [[1 15] [2 17.5] [3 17]]\n\t// country=us:\n\t//   map[reducers:[avg]]\n\t//   map[sources:[wind:3 wind:4]]\n\t//   [[1 12.5] [2 14.5] [3 13]]\n}\n\nfunc ExampleClient_timeseries_compaction() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"hyg:1\", \"hyg:compacted\")\n\t// REMOVE_END\n\n\t// STEP_START create_compaction\n\tres45, err := rdb.TSCreate(ctx, \"hyg:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res45) // >>> OK\n\n\tres46, err := rdb.TSCreate(ctx, \"hyg:compacted\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res46) // >>> OK\n\n\tres47, err := rdb.TSCreateRule(\n\t\tctx, \"hyg:1\", \"hyg:compacted\", redis.Min, 3,\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res47) // >>> OK\n\n\tres48, err := rdb.TSInfo(ctx, \"hyg:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res48[\"rules\"]) // >>> [[hyg:compacted 3 MIN 0]]\n\n\tres49, err := rdb.TSInfo(ctx, \"hyg:compacted\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res49[\"sourceKey\"]) // >>> hyg:1\n\t// STEP_END\n\n\t// STEP_START comp_add\n\tres50, err := rdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"hyg:1\", 0, 75},\n\t\t{\"hyg:1\", 1, 77},\n\t\t{\"hyg:1\", 2, 78},\n\t}).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res50) // >>> [0 1 2]\n\n\tres51, err := rdb.TSRange(\n\t\tctx, \"hyg:compacted\", 0, math.MaxInt64,\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res51) // >>> []\n\n\tres52, err := rdb.TSAdd(ctx, \"hyg:1\", 3, 79).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res52) // >>> 3\n\n\tres53, err := rdb.TSRange(\n\t\tctx, \"hyg:compacted\", 0, math.MaxInt64,\n\t).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res53) // >>> [{0 75}]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// OK\n\t// OK\n\t// map[hyg:compacted:[3 MIN 0]]\n\t// hyg:1\n\t// [0 1 2]\n\t// []\n\t// 3\n\t// [{0 75}]\n}\n\nfunc ExampleClient_timeseries_delete() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// make sure we are working with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"thermometer:1\")\n\t// Setup initial data\n\trdb.TSCreate(ctx, \"thermometer:1\")\n\trdb.TSMAdd(ctx, [][]interface{}{\n\t\t{\"thermometer:1\", 1, 9.2},\n\t\t{\"thermometer:1\", 2, 9.9},\n\t})\n\t// REMOVE_END\n\n\t// STEP_START del\n\tres54, err := rdb.TSInfo(ctx, \"thermometer:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res54[\"totalSamples\"])   // >>> 2\n\tfmt.Println(res54[\"firstTimestamp\"]) // >>> 1\n\tfmt.Println(res54[\"lastTimestamp\"])  // >>> 2\n\n\tres55, err := rdb.TSAdd(ctx, \"thermometer:1\", 3, 9.7).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res55) // >>> 3\n\n\tres56, err := rdb.TSInfo(ctx, \"thermometer:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res56[\"totalSamples\"])   // >>> 3\n\tfmt.Println(res56[\"firstTimestamp\"]) // >>> 1\n\tfmt.Println(res56[\"lastTimestamp\"])  // >>> 3\n\n\tres57, err := rdb.TSDel(ctx, \"thermometer:1\", 1, 2).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res57) // >>> 2\n\n\tres58, err := rdb.TSInfo(ctx, \"thermometer:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res58[\"totalSamples\"])   // >>> 1\n\tfmt.Println(res58[\"firstTimestamp\"]) // >>> 3\n\tfmt.Println(res58[\"lastTimestamp\"])  // >>> 3\n\n\tres59, err := rdb.TSDel(ctx, \"thermometer:1\", 3, 3).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res59) // >>> 1\n\n\tres60, err := rdb.TSInfo(ctx, \"thermometer:1\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res60[\"totalSamples\"]) // >>> 0\n\t// STEP_END\n\n\t// Output:\n\t// 2\n\t// 1\n\t// 2\n\t// 3\n\t// 3\n\t// 1\n\t// 3\n\t// 2\n\t// 1\n\t// 3\n\t// 3\n\t// 1\n\t// 0\n}\n"
  },
  {
    "path": "doctests/topk_tutorial_test.go",
    "content": "// EXAMPLE: topk_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_topk() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t})\n\n\t// REMOVE_START\n\t// start with fresh database\n\trdb.FlushDB(ctx)\n\trdb.Del(ctx, \"bikes:keywords\")\n\t// REMOVE_END\n\n\t// STEP_START topk\n\tres1, err := rdb.TopKReserve(ctx, \"bikes:keywords\", 5).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> OK\n\n\tres2, err := rdb.TopKAdd(ctx, \"bikes:keywords\",\n\t\t\"store\",\n\t\t\"seat\",\n\t\t\"handlebars\",\n\t\t\"handles\",\n\t\t\"pedals\",\n\t\t\"tires\",\n\t\t\"store\",\n\t\t\"seat\",\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> [     handlebars  ]\n\n\tres3, err := rdb.TopKList(ctx, \"bikes:keywords\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // [store seat pedals tires handles]\n\n\tres4, err := rdb.TopKQuery(ctx, \"bikes:keywords\", \"store\", \"handlebars\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // [true false]\n\t// STEP_END\n\n\t// Output:\n\t// OK\n\t// [     handlebars  ]\n\t// [store seat pedals tires handles]\n\t// [true false]\n}\n"
  },
  {
    "path": "doctests/vec_set_test.go",
    "content": "// EXAMPLE: vecset_tutorial\n// HIDE_START\npackage example_commands_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HIDE_END\n\nfunc ExampleClient_vectorset() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\tdefer rdb.Close()\n\t// REMOVE_START\n\trdb.Del(ctx, \"points\", \"quantSetQ8\", \"quantSetNoQ\", \"quantSetBin\", \"setNotReduced\", \"setReduced\")\n\t// REMOVE_END\n\n\t// STEP_START vadd\n\tres1, err := rdb.VAdd(ctx, \"points\", \"pt:A\",\n\t\t&redis.VectorValues{Val: []float64{1.0, 1.0}},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> true\n\n\tres2, err := rdb.VAdd(ctx, \"points\", \"pt:B\",\n\t\t&redis.VectorValues{Val: []float64{-1.0, -1.0}},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> true\n\n\tres3, err := rdb.VAdd(ctx, \"points\", \"pt:C\",\n\t\t&redis.VectorValues{Val: []float64{-1.0, 1.0}},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> true\n\n\tres4, err := rdb.VAdd(ctx, \"points\", \"pt:D\",\n\t\t&redis.VectorValues{Val: []float64{1.0, -1.0}},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res4) // >>> true\n\n\tres5, err := rdb.VAdd(ctx, \"points\", \"pt:E\",\n\t\t&redis.VectorValues{Val: []float64{1.0, 0.0}},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res5) // >>> true\n\n\tres6, err := rdb.Type(ctx, \"points\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res6) // >>> vectorset\n\t// STEP_END\n\n\t// STEP_START vcardvdim\n\tres7, err := rdb.VCard(ctx, \"points\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res7) // >>> 5\n\n\tres8, err := rdb.VDim(ctx, \"points\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res8) // >>> 2\n\t// STEP_END\n\n\t// STEP_START vemb\n\tres9, err := rdb.VEmb(ctx, \"points\", \"pt:A\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res9) // >>> [0.9999999403953552 0.9999999403953552]\n\n\tres10, err := rdb.VEmb(ctx, \"points\", \"pt:B\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res10) // >>> [-0.9999999403953552 -0.9999999403953552]\n\n\tres11, err := rdb.VEmb(ctx, \"points\", \"pt:C\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res11) // >>> [-0.9999999403953552 0.9999999403953552]\n\n\tres12, err := rdb.VEmb(ctx, \"points\", \"pt:D\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res12) // >>> [0.9999999403953552 -0.9999999403953552]\n\n\tres13, err := rdb.VEmb(ctx, \"points\", \"pt:E\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res13) // >>> [1 0]\n\t// STEP_END\n\n\t// STEP_START attr\n\tattrs := map[string]interface{}{\n\t\t\"name\":        \"Point A\",\n\t\t\"description\": \"First point added\",\n\t}\n\n\tres14, err := rdb.VSetAttr(ctx, \"points\", \"pt:A\", attrs).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res14) // >>> true\n\n\tres15, err := rdb.VGetAttr(ctx, \"points\", \"pt:A\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res15)\n\t// >>> {\"description\":\"First point added\",\"name\":\"Point A\"}\n\n\tres16, err := rdb.VClearAttributes(ctx, \"points\", \"pt:A\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res16) // >>> true\n\n\t// `VGetAttr()` returns an error if the attribute doesn't exist.\n\t_, err = rdb.VGetAttr(ctx, \"points\", \"pt:A\").Result()\n\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\t// STEP_END\n\n\t// STEP_START vrem\n\tres18, err := rdb.VAdd(ctx, \"points\", \"pt:F\",\n\t\t&redis.VectorValues{Val: []float64{0.0, 0.0}},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res18) // >>> true\n\n\tres19, err := rdb.VCard(ctx, \"points\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res19) // >>> 6\n\n\tres20, err := rdb.VRem(ctx, \"points\", \"pt:F\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res20) // >>> true\n\n\tres21, err := rdb.VCard(ctx, \"points\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res21) // >>> 5\n\t// STEP_END\n\n\t// STEP_START vsim_basic\n\tres22, err := rdb.VSim(ctx, \"points\",\n\t\t&redis.VectorValues{Val: []float64{0.9, 0.1}},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res22) // >>> [pt:E pt:A pt:D pt:C pt:B]\n\t// STEP_END\n\n\t// STEP_START vsim_options\n\tres23, err := rdb.VSimWithArgsWithScores(\n\t\tctx,\n\t\t\"points\",\n\t\t&redis.VectorRef{Name: \"pt:A\"},\n\t\t&redis.VSimArgs{Count: 4},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsort.Slice(res23, func(i, j int) bool {\n\t\treturn res23[i].Name < res23[j].Name\n\t})\n\n\tfmt.Println(res23)\n\t// >>> [{pt:A 1} {pt:C 0.5} {pt:D 0.5} {pt:E 0.8535534143447876}]\n\t// STEP_END\n\n\t// STEP_START vsim_filter\n\t// Set attributes for filtering\n\tres24, err := rdb.VSetAttr(ctx, \"points\", \"pt:A\",\n\t\tmap[string]interface{}{\n\t\t\t\"size\":  \"large\",\n\t\t\t\"price\": 18.99,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res24) // >>> true\n\n\tres25, err := rdb.VSetAttr(ctx, \"points\", \"pt:B\",\n\t\tmap[string]interface{}{\n\t\t\t\"size\":  \"large\",\n\t\t\t\"price\": 35.99,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res25) // >>> true\n\n\tres26, err := rdb.VSetAttr(ctx, \"points\", \"pt:C\",\n\t\tmap[string]interface{}{\n\t\t\t\"size\":  \"large\",\n\t\t\t\"price\": 25.99,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res26) // >>> true\n\n\tres27, err := rdb.VSetAttr(ctx, \"points\", \"pt:D\",\n\t\tmap[string]interface{}{\n\t\t\t\"size\":  \"small\",\n\t\t\t\"price\": 21.00,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res27) // >>> true\n\n\tres28, err := rdb.VSetAttr(ctx, \"points\", \"pt:E\",\n\t\tmap[string]interface{}{\n\t\t\t\"size\":  \"small\",\n\t\t\t\"price\": 17.75,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res28) // >>> true\n\n\t// Return elements in order of distance from point A whose\n\t// `size` attribute is `large`.\n\tres29, err := rdb.VSimWithArgs(ctx, \"points\",\n\t\t&redis.VectorRef{Name: \"pt:A\"},\n\t\t&redis.VSimArgs{Filter: `.size == \"large\"`},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res29) // >>> [pt:A pt:C pt:B]\n\n\t// Return elements in order of distance from point A whose size is\n\t// `large` and whose price is greater than 20.00.\n\tres30, err := rdb.VSimWithArgs(ctx, \"points\",\n\t\t&redis.VectorRef{Name: \"pt:A\"},\n\t\t&redis.VSimArgs{Filter: `.size == \"large\" && .price > 20.00`},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res30) // >>> [pt:C pt:B]\n\t// STEP_END\n\n\t// Output:\n\t// true\n\t// true\n\t// true\n\t// true\n\t// true\n\t// vectorset\n\t// 5\n\t// 2\n\t// [0.9999999403953552 0.9999999403953552]\n\t// [-0.9999999403953552 -0.9999999403953552]\n\t// [-0.9999999403953552 0.9999999403953552]\n\t// [0.9999999403953552 -0.9999999403953552]\n\t// [1 0]\n\t// true\n\t// {\"description\":\"First point added\",\"name\":\"Point A\"}\n\t// true\n\t// redis: nil\n\t// true\n\t// 6\n\t// true\n\t// 5\n\t// [pt:E pt:A pt:D pt:C pt:B]\n\t// [{pt:A 1} {pt:C 0.5} {pt:D 0.5} {pt:E 0.8535534143447876}]\n\t// true\n\t// true\n\t// true\n\t// true\n\t// true\n\t// [pt:A pt:C pt:B]\n\t// [pt:C pt:B]\n}\n\nfunc ExampleClient_vectorset_quantization() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\tdefer rdb.Close()\n\t// REMOVE_START\n\trdb.Del(ctx, \"quantSetQ8\", \"quantSetNoQ\", \"quantSetBin\")\n\t// REMOVE_END\n\n\t// STEP_START add_quant\n\t// Add with Q8 quantization\n\tvecQ := &redis.VectorValues{Val: []float64{1.262185, 1.958231}}\n\n\tres1, err := rdb.VAddWithArgs(ctx, \"quantSetQ8\", \"quantElement\", vecQ,\n\t\t&redis.VAddArgs{\n\t\t\tQ8: true,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> true\n\n\tembQ8, err := rdb.VEmb(ctx, \"quantSetQ8\", \"quantElement\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Q8 embedding: %v\\n\", embQ8)\n\t// >>> Q8 embedding: [1.2621850967407227 1.9582309722900391]\n\n\t// Add with NOQUANT option\n\tres2, err := rdb.VAddWithArgs(ctx, \"quantSetNoQ\", \"quantElement\", vecQ,\n\t\t&redis.VAddArgs{\n\t\t\tNoQuant: true,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> true\n\n\tembNoQ, err := rdb.VEmb(ctx, \"quantSetNoQ\", \"quantElement\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"NOQUANT embedding: %v\\n\", embNoQ)\n\t// >>> NOQUANT embedding: [1.262185 1.958231]\n\n\t// Add with BIN quantization\n\tres3, err := rdb.VAddWithArgs(ctx, \"quantSetBin\", \"quantElement\", vecQ,\n\t\t&redis.VAddArgs{\n\t\t\tBin: true,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res3) // >>> true\n\n\tembBin, err := rdb.VEmb(ctx, \"quantSetBin\", \"quantElement\", false).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"BIN embedding: %v\\n\", embBin)\n\t// >>> BIN embedding: [1 1]\n\t// STEP_END\n\n\t// Output:\n\t// true\n\t// Q8 embedding: [1.2643694877624512 1.958230972290039]\n\t// true\n\t// NOQUANT embedding: [1.262184977531433 1.958230972290039]\n\t// true\n\t// BIN embedding: [1 1]\n}\n\nfunc ExampleClient_vectorset_dimension_reduction() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password set\n\t\tDB:       0,  // use default DB\n\t})\n\n\tdefer rdb.Close()\n\t// REMOVE_START\n\trdb.Del(ctx, \"setNotReduced\", \"setReduced\")\n\t// REMOVE_END\n\n\t// STEP_START add_reduce\n\t// Create a vector with 300 dimensions\n\tvalues := make([]float64, 300)\n\n\tfor i := 0; i < 300; i++ {\n\t\tvalues[i] = float64(i) / 299\n\t}\n\n\tvecLarge := &redis.VectorValues{Val: values}\n\n\t// Add without reduction\n\tres1, err := rdb.VAdd(ctx, \"setNotReduced\", \"element\", vecLarge).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res1) // >>> true\n\n\tdim1, err := rdb.VDim(ctx, \"setNotReduced\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Dimension without reduction: %d\\n\", dim1)\n\t// >>> Dimension without reduction: 300\n\n\t// Add with reduction to 100 dimensions\n\tres2, err := rdb.VAddWithArgs(ctx, \"setReduced\", \"element\", vecLarge,\n\t\t&redis.VAddArgs{\n\t\t\tReduce: 100,\n\t\t},\n\t).Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(res2) // >>> true\n\n\tdim2, err := rdb.VDim(ctx, \"setReduced\").Result()\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Dimension after reduction: %d\\n\", dim2)\n\t// >>> Dimension after reduction: 100\n\t// STEP_END\n\n\t// Output:\n\t// true\n\t// Dimension without reduction: 300\n\t// true\n\t// Dimension after reduction: 100\n}\n"
  },
  {
    "path": "error.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\n// ErrClosed performs any operation on the closed client will return this error.\nvar ErrClosed = pool.ErrClosed\n\n// ErrPoolExhausted is returned from a pool connection method\n// when the maximum number of database connections in the pool has been reached.\nvar ErrPoolExhausted = pool.ErrPoolExhausted\n\n// ErrPoolTimeout timed out waiting to get a connection from the connection pool.\nvar ErrPoolTimeout = pool.ErrPoolTimeout\n\n// ErrCrossSlot is returned when keys are used in the same Redis command and\n// the keys are not in the same hash slot. This error is returned by Redis\n// Cluster and will be returned by the client when TxPipeline or TxPipelined\n// is used on a ClusterClient with keys in different slots.\nvar ErrCrossSlot = proto.RedisError(\"CROSSSLOT Keys in request don't hash to the same slot\")\n\n// ErrNoScript is returned when EVALSHA is requested for a script digest that\n// is not available in the script cache. Note that this error text is reproduced\n// literally from that used by Redis.\nvar ErrNoScript = proto.RedisError(\"NOSCRIPT No matching script. Please use EVAL.\")\n\n// HasErrorPrefix checks if the err is a Redis error and the message contains a prefix.\nfunc HasErrorPrefix(err error, prefix string) bool {\n\tvar rErr Error\n\tif !errors.As(err, &rErr) {\n\t\treturn false\n\t}\n\tmsg := rErr.Error()\n\tmsg = strings.TrimPrefix(msg, \"ERR \") // KVRocks adds such prefix\n\treturn strings.HasPrefix(msg, prefix)\n}\n\ntype Error interface {\n\terror\n\n\t// RedisError is a no-op function but\n\t// serves to distinguish types that are Redis\n\t// errors from ordinary errors: a type is a\n\t// Redis error if it has a RedisError method.\n\tRedisError()\n}\n\nvar _ Error = proto.RedisError(\"\")\n\nfunc isContextError(err error) bool {\n\t// Check for wrapped context errors using errors.Is\n\treturn errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)\n}\n\n// isTimeoutError checks if an error is a timeout error, even if wrapped.\n// Returns (isTimeout, shouldRetryOnTimeout) where:\n// - isTimeout: true if the error is any kind of timeout error\n// - shouldRetryOnTimeout: true if Timeout() method returns true\nfunc isTimeoutError(err error) (isTimeout bool, hasTimeoutFlag bool) {\n\t// Check for timeoutError interface (works with wrapped errors)\n\tvar te timeoutError\n\tif errors.As(err, &te) {\n\t\treturn true, te.Timeout()\n\t}\n\n\t// Check for net.Error specifically (common case for network timeouts)\n\tvar netErr net.Error\n\tif errors.As(err, &netErr) {\n\t\treturn true, netErr.Timeout()\n\t}\n\n\treturn false, false\n}\n\nfunc shouldRetry(err error, retryTimeout bool) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\t// Check for EOF errors (works with wrapped errors)\n\tif errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {\n\t\treturn true\n\t}\n\n\t// Check for context errors (works with wrapped errors)\n\tif errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n\t\treturn false\n\t}\n\n\t// Check for pool timeout (works with wrapped errors)\n\tif errors.Is(err, pool.ErrPoolTimeout) {\n\t\t// connection pool timeout, increase retries. #3289\n\t\treturn true\n\t}\n\n\t// Check for timeout errors (works with wrapped errors)\n\tif isTimeout, hasTimeoutFlag := isTimeoutError(err); isTimeout {\n\t\tif hasTimeoutFlag {\n\t\t\treturn retryTimeout\n\t\t}\n\t\treturn true\n\t}\n\n\t// Check for typed Redis errors using errors.As (works with wrapped errors)\n\tif proto.IsMaxClientsError(err) {\n\t\treturn true\n\t}\n\tif proto.IsLoadingError(err) {\n\t\treturn true\n\t}\n\tif proto.IsReadOnlyError(err) {\n\t\treturn true\n\t}\n\tif proto.IsMasterDownError(err) {\n\t\treturn true\n\t}\n\tif proto.IsClusterDownError(err) {\n\t\treturn true\n\t}\n\tif proto.IsTryAgainError(err) {\n\t\treturn true\n\t}\n\tif proto.IsNoReplicasError(err) {\n\t\treturn true\n\t}\n\n\t// Fallback to string checking for backward compatibility with plain errors\n\ts := err.Error()\n\tif strings.HasPrefix(s, \"ERR max number of clients reached\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"LOADING \") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"READONLY \") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"CLUSTERDOWN \") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"TRYAGAIN \") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"MASTERDOWN \") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"NOREPLICAS \") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc isRedisError(err error) bool {\n\t// Check if error implements the Error interface (works with wrapped errors)\n\tvar redisErr Error\n\tif errors.As(err, &redisErr) {\n\t\treturn true\n\t}\n\t// Also check for proto.RedisError specifically\n\tvar protoRedisErr proto.RedisError\n\treturn errors.As(err, &protoRedisErr)\n}\n\nfunc isBadConn(err error, allowTimeout bool, addr string) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\t// Check for context errors (works with wrapped errors)\n\tif errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n\t\treturn true\n\t}\n\n\t// Check for pool timeout errors (works with wrapped errors)\n\tif errors.Is(err, pool.ErrConnUnusableTimeout) {\n\t\treturn true\n\t}\n\n\tif isRedisError(err) {\n\t\tswitch {\n\t\tcase isReadOnlyError(err):\n\t\t\t// Close connections in read only state in case domain addr is used\n\t\t\t// and domain resolves to a different Redis Server. See #790.\n\t\t\treturn true\n\t\tcase isMovedSameConnAddr(err, addr):\n\t\t\t// Close connections when we are asked to move to the same addr\n\t\t\t// of the connection. Force a DNS resolution when all connections\n\t\t\t// of the pool are recycled\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\tif allowTimeout {\n\t\t// Check for network timeout errors (works with wrapped errors)\n\t\tvar netErr net.Error\n\t\tif errors.As(err, &netErr) && netErr.Timeout() {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc isMovedError(err error) (moved bool, ask bool, addr string) {\n\t// Check for typed MovedError\n\tif movedErr, ok := proto.IsMovedError(err); ok {\n\t\taddr = movedErr.Addr()\n\t\taddr = internal.GetAddr(addr)\n\t\treturn true, false, addr\n\t}\n\n\t// Check for typed AskError\n\tif askErr, ok := proto.IsAskError(err); ok {\n\t\taddr = askErr.Addr()\n\t\taddr = internal.GetAddr(addr)\n\t\treturn false, true, addr\n\t}\n\n\t// Fallback to string checking for backward compatibility\n\ts := err.Error()\n\tif strings.HasPrefix(s, \"MOVED \") {\n\t\t// Parse: MOVED 3999 127.0.0.1:6381\n\t\tparts := strings.Split(s, \" \")\n\t\tif len(parts) == 3 {\n\t\t\taddr = internal.GetAddr(parts[2])\n\t\t\treturn true, false, addr\n\t\t}\n\t}\n\tif strings.HasPrefix(s, \"ASK \") {\n\t\t// Parse: ASK 3999 127.0.0.1:6381\n\t\tparts := strings.Split(s, \" \")\n\t\tif len(parts) == 3 {\n\t\t\taddr = internal.GetAddr(parts[2])\n\t\t\treturn false, true, addr\n\t\t}\n\t}\n\n\treturn false, false, \"\"\n}\n\nfunc isLoadingError(err error) bool {\n\treturn proto.IsLoadingError(err)\n}\n\nfunc isReadOnlyError(err error) bool {\n\treturn proto.IsReadOnlyError(err)\n}\n\nfunc isMovedSameConnAddr(err error, addr string) bool {\n\tif movedErr, ok := proto.IsMovedError(err); ok {\n\t\treturn strings.HasSuffix(movedErr.Addr(), addr)\n\t}\n\treturn false\n}\n\n//------------------------------------------------------------------------------\n\n// Typed error checking functions for public use.\n// These functions work correctly even when errors are wrapped in hooks.\n\n// IsLoadingError checks if an error is a Redis LOADING error, even if wrapped.\n// LOADING errors occur when Redis is loading the dataset in memory.\nfunc IsLoadingError(err error) bool {\n\treturn proto.IsLoadingError(err)\n}\n\n// IsReadOnlyError checks if an error is a Redis READONLY error, even if wrapped.\n// READONLY errors occur when trying to write to a read-only replica.\nfunc IsReadOnlyError(err error) bool {\n\treturn proto.IsReadOnlyError(err)\n}\n\n// IsClusterDownError checks if an error is a Redis CLUSTERDOWN error, even if wrapped.\n// CLUSTERDOWN errors occur when the cluster is down.\nfunc IsClusterDownError(err error) bool {\n\treturn proto.IsClusterDownError(err)\n}\n\n// IsTryAgainError checks if an error is a Redis TRYAGAIN error, even if wrapped.\n// TRYAGAIN errors occur when a command cannot be processed and should be retried.\nfunc IsTryAgainError(err error) bool {\n\treturn proto.IsTryAgainError(err)\n}\n\n// IsMasterDownError checks if an error is a Redis MASTERDOWN error, even if wrapped.\n// MASTERDOWN errors occur when the master is down.\nfunc IsMasterDownError(err error) bool {\n\treturn proto.IsMasterDownError(err)\n}\n\n// IsMaxClientsError checks if an error is a Redis max clients error, even if wrapped.\n// This error occurs when the maximum number of clients has been reached.\nfunc IsMaxClientsError(err error) bool {\n\treturn proto.IsMaxClientsError(err)\n}\n\n// IsMovedError checks if an error is a Redis MOVED error, even if wrapped.\n// MOVED errors occur in cluster mode when a key has been moved to a different node.\n// Returns the address of the node where the key has been moved and a boolean indicating if it's a MOVED error.\nfunc IsMovedError(err error) (addr string, ok bool) {\n\tif movedErr, isMovedErr := proto.IsMovedError(err); isMovedErr {\n\t\treturn movedErr.Addr(), true\n\t}\n\treturn \"\", false\n}\n\n// IsAskError checks if an error is a Redis ASK error, even if wrapped.\n// ASK errors occur in cluster mode when a key is being migrated and the client should ask another node.\n// Returns the address of the node to ask and a boolean indicating if it's an ASK error.\nfunc IsAskError(err error) (addr string, ok bool) {\n\tif askErr, isAskErr := proto.IsAskError(err); isAskErr {\n\t\treturn askErr.Addr(), true\n\t}\n\treturn \"\", false\n}\n\n// IsAuthError checks if an error is a Redis authentication error, even if wrapped.\n// Authentication errors occur when:\n// - NOAUTH: Redis requires authentication but none was provided\n// - WRONGPASS: Redis authentication failed due to incorrect password\n// - unauthenticated: Error returned when password changed\nfunc IsAuthError(err error) bool {\n\treturn proto.IsAuthError(err)\n}\n\n// IsPermissionError checks if an error is a Redis permission error, even if wrapped.\n// Permission errors (NOPERM) occur when a user does not have permission to execute a command.\nfunc IsPermissionError(err error) bool {\n\treturn proto.IsPermissionError(err)\n}\n\n// IsExecAbortError checks if an error is a Redis EXECABORT error, even if wrapped.\n// EXECABORT errors occur when a transaction is aborted.\nfunc IsExecAbortError(err error) bool {\n\treturn proto.IsExecAbortError(err)\n}\n\n// IsOOMError checks if an error is a Redis OOM (Out Of Memory) error, even if wrapped.\n// OOM errors occur when Redis is out of memory.\nfunc IsOOMError(err error) bool {\n\treturn proto.IsOOMError(err)\n}\n\n// IsNoReplicasError checks if an error is a Redis NOREPLICAS error, even if wrapped.\n// NOREPLICAS errors occur when not enough replicas acknowledge a write operation.\n// This typically happens with WAIT/WAITAOF commands or CLUSTER SETSLOT with synchronous\n// replication when the required number of replicas cannot confirm the write within the timeout.\nfunc IsNoReplicasError(err error) bool {\n\treturn proto.IsNoReplicasError(err)\n}\n\n//------------------------------------------------------------------------------\n\ntype timeoutError interface {\n\tTimeout() bool\n}\n"
  },
  {
    "path": "error_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\ntype testTimeout struct {\n\ttimeout bool\n}\n\nfunc (t testTimeout) Timeout() bool {\n\treturn t.timeout\n}\n\nfunc (t testTimeout) Error() string {\n\treturn \"test timeout\"\n}\n\nvar _ = Describe(\"error\", func() {\n\tBeforeEach(func() {\n\n\t})\n\n\tAfterEach(func() {\n\n\t})\n\n\tIt(\"should retry\", func() {\n\t\tdata := map[error]bool{\n\t\t\tio.EOF:                   true,\n\t\t\tio.ErrUnexpectedEOF:      true,\n\t\t\tnil:                      false,\n\t\t\tcontext.Canceled:         false,\n\t\t\tcontext.DeadlineExceeded: false,\n\t\t\tredis.ErrPoolTimeout:     true,\n\t\t\t// Use typed errors instead of plain errors.New()\n\t\t\tproto.ParseErrorReply([]byte(\"-ERR max number of clients reached\")):                      true,\n\t\t\tproto.ParseErrorReply([]byte(\"-LOADING Redis is loading the dataset in memory\")):         true,\n\t\t\tproto.ParseErrorReply([]byte(\"-READONLY You can't write against a read only replica\")):   true,\n\t\t\tproto.ParseErrorReply([]byte(\"-CLUSTERDOWN The cluster is down\")):                        true,\n\t\t\tproto.ParseErrorReply([]byte(\"-TRYAGAIN Command cannot be processed, please try again\")): true,\n\t\t\tproto.ParseErrorReply([]byte(\"-NOREPLICAS Not enough good replicas to write\")):           true,\n\t\t\tproto.ParseErrorReply([]byte(\"-ERR other\")):                                              false,\n\t\t}\n\n\t\tfor err, expected := range data {\n\t\t\tExpect(redis.ShouldRetry(err, false)).To(Equal(expected))\n\t\t\tExpect(redis.ShouldRetry(err, true)).To(Equal(expected))\n\t\t}\n\t})\n\n\tIt(\"should retry timeout\", func() {\n\t\tt1 := testTimeout{timeout: true}\n\t\tExpect(redis.ShouldRetry(t1, true)).To(Equal(true))\n\t\tExpect(redis.ShouldRetry(t1, false)).To(Equal(false))\n\n\t\tt2 := testTimeout{timeout: false}\n\t\tExpect(redis.ShouldRetry(t2, true)).To(Equal(true))\n\t\tExpect(redis.ShouldRetry(t2, false)).To(Equal(true))\n\t})\n})\n"
  },
  {
    "path": "error_wrapping_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\n// TestTypedErrorsWithHookWrapping demonstrates that typed errors work correctly\n// even when wrapped by hooks, which is the main improvement of this change.\nfunc TestTypedErrorsWithHookWrapping(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\terrorMsg  string\n\t\tcheckFunc func(error) bool\n\t\ttestName  string\n\t}{\n\t\t{\n\t\t\tname:      \"LOADING error wrapped in hook\",\n\t\t\terrorMsg:  \"LOADING Redis is loading the dataset in memory\",\n\t\t\tcheckFunc: redis.IsLoadingError,\n\t\t\ttestName:  \"IsLoadingError\",\n\t\t},\n\t\t{\n\t\t\tname:      \"READONLY error wrapped in hook\",\n\t\t\terrorMsg:  \"READONLY You can't write against a read only replica\",\n\t\t\tcheckFunc: redis.IsReadOnlyError,\n\t\t\ttestName:  \"IsReadOnlyError\",\n\t\t},\n\t\t{\n\t\t\tname:      \"CLUSTERDOWN error wrapped in hook\",\n\t\t\terrorMsg:  \"CLUSTERDOWN The cluster is down\",\n\t\t\tcheckFunc: redis.IsClusterDownError,\n\t\t\ttestName:  \"IsClusterDownError\",\n\t\t},\n\t\t{\n\t\t\tname:      \"TRYAGAIN error wrapped in hook\",\n\t\t\terrorMsg:  \"TRYAGAIN Multiple keys request during rehashing of slot\",\n\t\t\tcheckFunc: redis.IsTryAgainError,\n\t\t\ttestName:  \"IsTryAgainError\",\n\t\t},\n\t\t{\n\t\t\tname:      \"MASTERDOWN error wrapped in hook\",\n\t\t\terrorMsg:  \"MASTERDOWN Link with MASTER is down\",\n\t\t\tcheckFunc: redis.IsMasterDownError,\n\t\t\ttestName:  \"IsMasterDownError\",\n\t\t},\n\t\t{\n\t\t\tname:      \"Max clients error wrapped in hook\",\n\t\t\terrorMsg:  \"ERR max number of clients reached\",\n\t\t\tcheckFunc: redis.IsMaxClientsError,\n\t\t\ttestName:  \"IsMaxClientsError\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Simulate a Redis error being created\n\t\t\tparsedErr := proto.ParseErrorReply([]byte(\"-\" + tt.errorMsg))\n\n\t\t\t// Simulate hook wrapping the error\n\t\t\twrappedErr := fmt.Errorf(\"hook wrapper: %w\", parsedErr)\n\t\t\tdoubleWrappedErr := fmt.Errorf(\"another hook: %w\", wrappedErr)\n\n\t\t\t// Test that the typed error check works with wrapped errors\n\t\t\tif !tt.checkFunc(doubleWrappedErr) {\n\t\t\t\tt.Errorf(\"%s failed to detect wrapped error: %v\", tt.testName, doubleWrappedErr)\n\t\t\t}\n\n\t\t\t// Test that the error message is still accessible\n\t\t\tif !errors.Is(doubleWrappedErr, parsedErr) {\n\t\t\t\tt.Errorf(\"errors.Is failed to match wrapped error\")\n\t\t\t}\n\n\t\t\t// Test that the original error message is preserved in the chain\n\t\t\texpectedMsg := tt.errorMsg\n\t\t\tif parsedErr.Error() != expectedMsg {\n\t\t\t\tt.Errorf(\"Error message changed: got %q, want %q\", parsedErr.Error(), expectedMsg)\n\t\t\t}\n\n\t\t\t// Verify the generic RedisError interface still works\n\t\t\tvar redisError redis.Error\n\t\t\tif !errors.As(doubleWrappedErr, &redisError) {\n\t\t\t\tt.Errorf(\"Failed to extract redis.Error from wrapped error\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestMovedAndAskErrorsWithHookWrapping tests MOVED and ASK errors with wrapping\nfunc TestMovedAndAskErrorsWithHookWrapping(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\terrorMsg     string\n\t\texpectedAddr string\n\t\tisMoved      bool\n\t}{\n\t\t{\n\t\t\tname:         \"MOVED error\",\n\t\t\terrorMsg:     \"MOVED 3999 127.0.0.1:6381\",\n\t\t\texpectedAddr: \"127.0.0.1:6381\",\n\t\t\tisMoved:      true,\n\t\t},\n\t\t{\n\t\t\tname:         \"ASK error\",\n\t\t\terrorMsg:     \"ASK 3999 192.168.1.100:6380\",\n\t\t\texpectedAddr: \"192.168.1.100:6380\",\n\t\t\tisMoved:      false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create the error\n\t\t\tparsedErr := proto.ParseErrorReply([]byte(\"-\" + tt.errorMsg))\n\n\t\t\t// Wrap it in hooks\n\t\t\twrappedErr := fmt.Errorf(\"hook wrapper: %w\", parsedErr)\n\t\t\tdoubleWrappedErr := fmt.Errorf(\"another hook: %w\", wrappedErr)\n\n\t\t\t// Test address extraction from wrapped error\n\t\t\tif tt.isMoved {\n\t\t\t\taddr, ok := redis.IsMovedError(doubleWrappedErr)\n\t\t\t\tif !ok {\n\t\t\t\t\tt.Errorf(\"IsMovedError failed to detect wrapped MOVED error\")\n\t\t\t\t}\n\t\t\t\tif addr != tt.expectedAddr {\n\t\t\t\t\tt.Errorf(\"Address mismatch: got %q, want %q\", addr, tt.expectedAddr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\taddr, ok := redis.IsAskError(doubleWrappedErr)\n\t\t\t\tif !ok {\n\t\t\t\t\tt.Errorf(\"IsAskError failed to detect wrapped ASK error\")\n\t\t\t\t}\n\t\t\t\tif addr != tt.expectedAddr {\n\t\t\t\t\tt.Errorf(\"Address mismatch: got %q, want %q\", addr, tt.expectedAddr)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestBackwardCompatibilityWithStringChecks verifies that old string-based\n// error checking still works for backward compatibility\nfunc TestBackwardCompatibilityWithStringChecks(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\terrorMsg     string\n\t\tstringPrefix string\n\t}{\n\t\t{\n\t\t\tname:         \"LOADING error\",\n\t\t\terrorMsg:     \"LOADING Redis is loading the dataset in memory\",\n\t\t\tstringPrefix: \"LOADING \",\n\t\t},\n\t\t{\n\t\t\tname:         \"READONLY error\",\n\t\t\terrorMsg:     \"READONLY You can't write against a read only replica\",\n\t\t\tstringPrefix: \"READONLY \",\n\t\t},\n\t\t{\n\t\t\tname:         \"CLUSTERDOWN error\",\n\t\t\terrorMsg:     \"CLUSTERDOWN The cluster is down\",\n\t\t\tstringPrefix: \"CLUSTERDOWN \",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tparsedErr := proto.ParseErrorReply([]byte(\"-\" + tt.errorMsg))\n\n\t\t\t// Old-style string checking should still work\n\t\t\terrMsg := parsedErr.Error()\n\t\t\tif errMsg != tt.errorMsg {\n\t\t\t\tt.Errorf(\"Error message mismatch: got %q, want %q\", errMsg, tt.errorMsg)\n\t\t\t}\n\n\t\t\t// String prefix checking should still work\n\t\t\tif len(errMsg) < len(tt.stringPrefix) || errMsg[:len(tt.stringPrefix)] != tt.stringPrefix {\n\t\t\t\tt.Errorf(\"String prefix check failed: error %q doesn't start with %q\", errMsg, tt.stringPrefix)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestErrorWrappingInHookScenario simulates a real-world scenario where\n// a hook wraps errors for logging or instrumentation\nfunc TestErrorWrappingInHookScenario(t *testing.T) {\n\t// Simulate a hook that wraps errors for logging\n\twrapErrorForLogging := func(err error) error {\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"logged error at %s: %w\", \"2024-01-01T00:00:00Z\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Simulate a hook that adds context\n\taddContextToError := func(err error, cmd string) error {\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"command %s failed: %w\", cmd, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Create a LOADING error\n\tloadingErr := proto.ParseErrorReply([]byte(\"-LOADING Redis is loading the dataset in memory\"))\n\n\t// Wrap it through multiple hooks\n\terr := loadingErr\n\terr = wrapErrorForLogging(err)\n\terr = addContextToError(err, \"GET mykey\")\n\n\t// The typed error check should still work\n\tif !redis.IsLoadingError(err) {\n\t\tt.Errorf(\"IsLoadingError failed to detect error through multiple hook wrappers\")\n\t}\n\n\t// The error message should contain all the context\n\terrMsg := err.Error()\n\texpectedSubstrings := []string{\n\t\t\"command GET mykey failed\",\n\t\t\"logged error at\",\n\t\t\"LOADING Redis is loading the dataset in memory\",\n\t}\n\n\tfor _, substr := range expectedSubstrings {\n\t\tif !contains(errMsg, substr) {\n\t\t\tt.Errorf(\"Error message missing expected substring %q: %s\", substr, errMsg)\n\t\t}\n\t}\n}\n\n// TestShouldRetryWithTypedErrors tests that shouldRetry works with typed errors\nfunc TestShouldRetryWithTypedErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\terrorMsg     string\n\t\tshouldRetry  bool\n\t\tretryTimeout bool\n\t}{\n\t\t{\n\t\t\tname:         \"LOADING error should retry\",\n\t\t\terrorMsg:     \"LOADING Redis is loading the dataset in memory\",\n\t\t\tshouldRetry:  true,\n\t\t\tretryTimeout: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"READONLY error should retry\",\n\t\t\terrorMsg:     \"READONLY You can't write against a read only replica\",\n\t\t\tshouldRetry:  true,\n\t\t\tretryTimeout: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"CLUSTERDOWN error should retry\",\n\t\t\terrorMsg:     \"CLUSTERDOWN The cluster is down\",\n\t\t\tshouldRetry:  true,\n\t\t\tretryTimeout: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"TRYAGAIN error should retry\",\n\t\t\terrorMsg:     \"TRYAGAIN Multiple keys request during rehashing of slot\",\n\t\t\tshouldRetry:  true,\n\t\t\tretryTimeout: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"MASTERDOWN error should retry\",\n\t\t\terrorMsg:     \"MASTERDOWN Link with MASTER is down\",\n\t\t\tshouldRetry:  true,\n\t\t\tretryTimeout: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"Max clients error should retry\",\n\t\t\terrorMsg:     \"ERR max number of clients reached\",\n\t\t\tshouldRetry:  true,\n\t\t\tretryTimeout: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"NOREPLICAS error should retry\",\n\t\t\terrorMsg:     \"NOREPLICAS Not enough good replicas to write\",\n\t\t\tshouldRetry:  true,\n\t\t\tretryTimeout: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := proto.ParseErrorReply([]byte(\"-\" + tt.errorMsg))\n\n\t\t\t// Wrap the error\n\t\t\twrappedErr := fmt.Errorf(\"hook wrapper: %w\", err)\n\n\t\t\t// Test shouldRetry (using the exported ShouldRetry for testing)\n\t\t\tresult := redis.ShouldRetry(wrappedErr, tt.retryTimeout)\n\t\t\tif result != tt.shouldRetry {\n\t\t\t\tt.Errorf(\"ShouldRetry returned %v, want %v for error: %v\", result, tt.shouldRetry, wrappedErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestSetErrWithWrappedError tests that when a hook wraps an error and sets it\n// via cmd.SetErr(), the underlying typed error can still be detected\nfunc TestSetErrWithWrappedError(t *testing.T) {\n\ttestCtx := context.Background()\n\n\t// Test with a simulated LOADING error\n\t// We test the mechanism directly without needing a real Redis server\n\tcmd := redis.NewStatusCmd(testCtx, \"GET\", \"key\")\n\tloadingErr := proto.ParseErrorReply([]byte(\"-LOADING Redis is loading the dataset in memory\"))\n\twrappedLoadingErr := fmt.Errorf(\"hook wrapper: %w\", loadingErr)\n\tcmd.SetErr(wrappedLoadingErr)\n\n\t// Verify we can still detect the LOADING error through the wrapper\n\tif !redis.IsLoadingError(cmd.Err()) {\n\t\tt.Errorf(\"IsLoadingError failed to detect wrapped error set via SetErr: %v\", cmd.Err())\n\t}\n\n\t// Test with MOVED error\n\tcmd2 := redis.NewStatusCmd(testCtx, \"GET\", \"key\")\n\tmovedErr := proto.ParseErrorReply([]byte(\"-MOVED 3999 127.0.0.1:6381\"))\n\twrappedMovedErr := fmt.Errorf(\"hook wrapper: %w\", movedErr)\n\tcmd2.SetErr(wrappedMovedErr)\n\n\t// Verify we can still detect and extract address from MOVED error\n\taddr, ok := redis.IsMovedError(cmd2.Err())\n\tif !ok {\n\t\tt.Errorf(\"IsMovedError failed to detect wrapped error set via SetErr: %v\", cmd2.Err())\n\t}\n\tif addr != \"127.0.0.1:6381\" {\n\t\tt.Errorf(\"Address extraction failed: got %q, want %q\", addr, \"127.0.0.1:6381\")\n\t}\n\n\t// Test with READONLY error\n\tcmd3 := redis.NewStatusCmd(testCtx, \"SET\", \"key\", \"value\")\n\treadonlyErr := proto.ParseErrorReply([]byte(\"-READONLY You can't write against a read only replica\"))\n\twrappedReadonlyErr := fmt.Errorf(\"custom error wrapper: %w\", readonlyErr)\n\tcmd3.SetErr(wrappedReadonlyErr)\n\n\t// Verify we can still detect the READONLY error through the wrapper\n\tif !redis.IsReadOnlyError(cmd3.Err()) {\n\t\tt.Errorf(\"IsReadOnlyError failed to detect wrapped error set via SetErr: %v\", cmd3.Err())\n\t}\n\n\t// Verify the error message contains both the wrapper and original error\n\terrMsg := cmd3.Err().Error()\n\tif !contains(errMsg, \"custom error wrapper\") {\n\t\tt.Errorf(\"Error message missing wrapper context: %v\", errMsg)\n\t}\n\tif !contains(errMsg, \"READONLY\") {\n\t\tt.Errorf(\"Error message missing original error: %v\", errMsg)\n\t}\n}\n\n// AppError is a custom error type for testing\ntype AppError struct {\n\tCode      string\n\tMessage   string\n\tRequestID string\n\tErr       error\n}\n\n// Error implements the error interface\nfunc (e *AppError) Error() string {\n\treturn fmt.Sprintf(\"[%s] %s (request_id=%s): %v\", e.Code, e.Message, e.RequestID, e.Err)\n}\n\n// Unwrap implements the error unwrapping interface - this is critical for errors.As() to work\nfunc (e *AppError) Unwrap() error {\n\treturn e.Err\n}\n\n// TestCustomErrorTypeWrapping tests that users can wrap Redis errors in their own custom error types\n// and still have typed error detection work correctly\nfunc TestCustomErrorTypeWrapping(t *testing.T) {\n\ttestCtx := context.Background()\n\n\t// Test 1: Wrap LOADING error in custom type\n\tcmd1 := redis.NewStatusCmd(testCtx, \"GET\", \"key\")\n\tloadingErr := proto.ParseErrorReply([]byte(\"-LOADING Redis is loading the dataset in memory\"))\n\tcustomErr1 := &AppError{\n\t\tCode:      \"REDIS_ERROR\",\n\t\tMessage:   \"Database operation failed\",\n\t\tRequestID: \"req-12345\",\n\t\tErr:       loadingErr,\n\t}\n\tcmd1.SetErr(customErr1)\n\n\t// Verify typed error detection works through custom error type\n\tif !redis.IsLoadingError(cmd1.Err()) {\n\t\tt.Errorf(\"IsLoadingError failed to detect error wrapped in custom type: %v\", cmd1.Err())\n\t}\n\n\t// Verify error message contains custom context\n\terrMsg := cmd1.Err().Error()\n\tif !contains(errMsg, \"REDIS_ERROR\") || !contains(errMsg, \"req-12345\") {\n\t\tt.Errorf(\"Error message missing custom error context: %v\", errMsg)\n\t}\n\n\t// Test 2: Wrap MOVED error in custom type\n\tcmd2 := redis.NewStatusCmd(testCtx, \"GET\", \"key\")\n\tmovedErr := proto.ParseErrorReply([]byte(\"-MOVED 3999 127.0.0.1:6381\"))\n\tcustomErr2 := &AppError{\n\t\tCode:      \"CLUSTER_REDIRECT\",\n\t\tMessage:   \"Key moved to different node\",\n\t\tRequestID: \"req-67890\",\n\t\tErr:       movedErr,\n\t}\n\tcmd2.SetErr(customErr2)\n\n\t// Verify address extraction works through custom error type\n\taddr, ok := redis.IsMovedError(cmd2.Err())\n\tif !ok {\n\t\tt.Errorf(\"IsMovedError failed to detect error wrapped in custom type: %v\", cmd2.Err())\n\t}\n\tif addr != \"127.0.0.1:6381\" {\n\t\tt.Errorf(\"Address extraction failed: got %q, want %q\", addr, \"127.0.0.1:6381\")\n\t}\n\n\t// Test 3: Multiple levels of wrapping (custom type + fmt.Errorf)\n\tcmd3 := redis.NewStatusCmd(testCtx, \"SET\", \"key\", \"value\")\n\treadonlyErr := proto.ParseErrorReply([]byte(\"-READONLY You can't write against a read only replica\"))\n\tcustomErr3 := &AppError{\n\t\tCode:      \"WRITE_ERROR\",\n\t\tMessage:   \"Write operation failed\",\n\t\tRequestID: \"req-11111\",\n\t\tErr:       readonlyErr,\n\t}\n\t// Wrap the custom error again with fmt.Errorf\n\tdoubleWrapped := fmt.Errorf(\"hook context: %w\", customErr3)\n\tcmd3.SetErr(doubleWrapped)\n\n\t// Verify typed error detection works through multiple levels of wrapping\n\tif !redis.IsReadOnlyError(cmd3.Err()) {\n\t\tt.Errorf(\"IsReadOnlyError failed to detect error wrapped in custom type + fmt.Errorf: %v\", cmd3.Err())\n\t}\n\n\t// Verify we can unwrap to get the custom error\n\tvar appErr *AppError\n\tif !errors.As(cmd3.Err(), &appErr) {\n\t\tt.Errorf(\"errors.As failed to extract custom error type from wrapped error\")\n\t} else {\n\t\tif appErr.Code != \"WRITE_ERROR\" || appErr.RequestID != \"req-11111\" {\n\t\t\tt.Errorf(\"Custom error fields incorrect: Code=%s, RequestID=%s\", appErr.Code, appErr.RequestID)\n\t\t}\n\t}\n}\n\n// TestTimeoutErrorWrapping tests that timeout errors work correctly when wrapped\nfunc TestTimeoutErrorWrapping(t *testing.T) {\n\t// Test 1: Wrapped timeoutError interface\n\tt.Run(\"Wrapped timeoutError with Timeout()=true\", func(t *testing.T) {\n\t\ttimeoutErr := &testTimeoutError{timeout: true, msg: \"i/o timeout\"}\n\t\twrappedErr := fmt.Errorf(\"hook wrapper: %w\", timeoutErr)\n\t\tdoubleWrappedErr := fmt.Errorf(\"another wrapper: %w\", wrappedErr)\n\n\t\t// Should NOT retry when retryTimeout=false\n\t\tif redis.ShouldRetry(doubleWrappedErr, false) {\n\t\t\tt.Errorf(\"Should not retry timeout error when retryTimeout=false\")\n\t\t}\n\n\t\t// Should retry when retryTimeout=true\n\t\tif !redis.ShouldRetry(doubleWrappedErr, true) {\n\t\t\tt.Errorf(\"Should retry timeout error when retryTimeout=true\")\n\t\t}\n\t})\n\n\t// Test 2: Wrapped timeoutError with Timeout()=false\n\tt.Run(\"Wrapped timeoutError with Timeout()=false\", func(t *testing.T) {\n\t\ttimeoutErr := &testTimeoutError{timeout: false, msg: \"connection error\"}\n\t\twrappedErr := fmt.Errorf(\"hook wrapper: %w\", timeoutErr)\n\n\t\t// Should always retry when Timeout()=false\n\t\tif !redis.ShouldRetry(wrappedErr, false) {\n\t\t\tt.Errorf(\"Should retry non-timeout error even when retryTimeout=false\")\n\t\t}\n\t\tif !redis.ShouldRetry(wrappedErr, true) {\n\t\t\tt.Errorf(\"Should retry non-timeout error when retryTimeout=true\")\n\t\t}\n\t})\n\n\t// Test 3: Wrapped net.Error with Timeout()=true\n\tt.Run(\"Wrapped net.Error\", func(t *testing.T) {\n\t\tnetErr := &testNetError{timeout: true, temporary: true, msg: \"network timeout\"}\n\t\twrappedErr := fmt.Errorf(\"hook context: %w\", netErr)\n\n\t\t// Should respect retryTimeout parameter\n\t\tif redis.ShouldRetry(wrappedErr, false) {\n\t\t\tt.Errorf(\"Should not retry network timeout when retryTimeout=false\")\n\t\t}\n\t\tif !redis.ShouldRetry(wrappedErr, true) {\n\t\t\tt.Errorf(\"Should retry network timeout when retryTimeout=true\")\n\t\t}\n\t})\n\n\t// Test 4: Multiple levels of wrapping\n\tt.Run(\"Multiple levels of wrapping\", func(t *testing.T) {\n\t\ttimeoutErr := &testTimeoutError{timeout: true, msg: \"timeout\"}\n\t\tcustomErr := &AppError{\n\t\t\tCode:      \"TIMEOUT_ERROR\",\n\t\t\tMessage:   \"Operation timed out\",\n\t\t\tRequestID: \"req-timeout-123\",\n\t\t\tErr:       timeoutErr,\n\t\t}\n\t\twrappedErr := fmt.Errorf(\"hook wrapper: %w\", customErr)\n\n\t\t// Should still detect timeout through multiple wrappers\n\t\tif redis.ShouldRetry(wrappedErr, false) {\n\t\t\tt.Errorf(\"Should not retry timeout through custom error when retryTimeout=false\")\n\t\t}\n\t\tif !redis.ShouldRetry(wrappedErr, true) {\n\t\t\tt.Errorf(\"Should retry timeout through custom error when retryTimeout=true\")\n\t\t}\n\n\t\t// Should be able to extract custom error\n\t\tvar appErr *AppError\n\t\tif !errors.As(wrappedErr, &appErr) {\n\t\t\tt.Errorf(\"Should be able to extract AppError from wrapped error\")\n\t\t}\n\t})\n}\n\n// testTimeoutError implements the timeoutError interface for testing\ntype testTimeoutError struct {\n\ttimeout bool\n\tmsg     string\n}\n\nfunc (e *testTimeoutError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *testTimeoutError) Timeout() bool {\n\treturn e.timeout\n}\n\n// testNetError implements net.Error for testing\ntype testNetError struct {\n\ttimeout   bool\n\ttemporary bool\n\tmsg       string\n}\n\nfunc (e *testNetError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *testNetError) Timeout() bool {\n\treturn e.timeout\n}\n\nfunc (e *testNetError) Temporary() bool {\n\treturn e.temporary\n}\n\n// TestContextErrorWrapping tests that context errors work correctly when wrapped\nfunc TestContextErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped context.Canceled\", func(t *testing.T) {\n\t\twrappedErr := fmt.Errorf(\"operation failed: %w\", context.Canceled)\n\t\tdoubleWrappedErr := fmt.Errorf(\"hook wrapper: %w\", wrappedErr)\n\n\t\t// Should NOT retry\n\t\tif redis.ShouldRetry(doubleWrappedErr, false) {\n\t\t\tt.Errorf(\"Should not retry wrapped context.Canceled\")\n\t\t}\n\t\tif redis.ShouldRetry(doubleWrappedErr, true) {\n\t\t\tt.Errorf(\"Should not retry wrapped context.Canceled even with retryTimeout=true\")\n\t\t}\n\t})\n\n\tt.Run(\"Wrapped context.DeadlineExceeded\", func(t *testing.T) {\n\t\twrappedErr := fmt.Errorf(\"timeout: %w\", context.DeadlineExceeded)\n\t\tdoubleWrappedErr := fmt.Errorf(\"hook wrapper: %w\", wrappedErr)\n\n\t\t// Should NOT retry\n\t\tif redis.ShouldRetry(doubleWrappedErr, false) {\n\t\t\tt.Errorf(\"Should not retry wrapped context.DeadlineExceeded\")\n\t\t}\n\t\tif redis.ShouldRetry(doubleWrappedErr, true) {\n\t\t\tt.Errorf(\"Should not retry wrapped context.DeadlineExceeded even with retryTimeout=true\")\n\t\t}\n\t})\n}\n\n// TestIOErrorWrapping tests that io errors work correctly when wrapped\nfunc TestIOErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped io.EOF\", func(t *testing.T) {\n\t\twrappedErr := fmt.Errorf(\"read failed: %w\", io.EOF)\n\t\tdoubleWrappedErr := fmt.Errorf(\"hook wrapper: %w\", wrappedErr)\n\n\t\t// Should retry\n\t\tif !redis.ShouldRetry(doubleWrappedErr, false) {\n\t\t\tt.Errorf(\"Should retry wrapped io.EOF\")\n\t\t}\n\t})\n\n\tt.Run(\"Wrapped io.ErrUnexpectedEOF\", func(t *testing.T) {\n\t\twrappedErr := fmt.Errorf(\"read failed: %w\", io.ErrUnexpectedEOF)\n\n\t\t// Should retry\n\t\tif !redis.ShouldRetry(wrappedErr, false) {\n\t\t\tt.Errorf(\"Should retry wrapped io.ErrUnexpectedEOF\")\n\t\t}\n\t})\n}\n\n// TestPoolErrorWrapping tests that pool errors work correctly when wrapped\nfunc TestPoolErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped pool.ErrPoolTimeout\", func(t *testing.T) {\n\t\twrappedErr := fmt.Errorf(\"connection failed: %w\", redis.ErrPoolTimeout)\n\t\tdoubleWrappedErr := fmt.Errorf(\"hook wrapper: %w\", wrappedErr)\n\n\t\t// Should retry\n\t\tif !redis.ShouldRetry(doubleWrappedErr, false) {\n\t\t\tt.Errorf(\"Should retry wrapped pool.ErrPoolTimeout\")\n\t\t}\n\t})\n}\n\n// TestRedisErrorWrapping tests that RedisError detection works with wrapped errors\nfunc TestRedisErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped proto.RedisError\", func(t *testing.T) {\n\t\tredisErr := proto.RedisError(\"ERR something went wrong\")\n\t\twrappedErr := fmt.Errorf(\"command failed: %w\", redisErr)\n\t\tdoubleWrappedErr := fmt.Errorf(\"hook wrapper: %w\", wrappedErr)\n\n\t\t// Create a command and set the wrapped error\n\t\tcmd := redis.NewStatusCmd(context.Background(), \"GET\", \"key\")\n\t\tcmd.SetErr(doubleWrappedErr)\n\n\t\t// The error should still be recognized as a Redis error\n\t\t// This is tested indirectly through the typed error system\n\t\tif !strings.Contains(cmd.Err().Error(), \"ERR something went wrong\") {\n\t\t\tt.Errorf(\"Error message not preserved through wrapping\")\n\t\t}\n\t})\n}\n\n// Helper function to check if a string contains a substring\nfunc contains(s, substr string) bool {\n\treturn strings.Contains(s, substr)\n}\n\nfunc TestAuthErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped NOAUTH error\", func(t *testing.T) {\n\t\t// Create an auth error\n\t\tauthErr := proto.NewAuthError(\"NOAUTH Authentication required\")\n\n\t\t// Wrap it\n\t\twrappedErr := fmt.Errorf(\"hook: %w\", authErr)\n\n\t\t// Should still be detected\n\t\tif !redis.IsAuthError(wrappedErr) {\n\t\t\tt.Errorf(\"IsAuthError should detect wrapped NOAUTH error\")\n\t\t}\n\t})\n\n\tt.Run(\"Wrapped WRONGPASS error\", func(t *testing.T) {\n\t\t// Create an auth error\n\t\tauthErr := proto.NewAuthError(\"WRONGPASS invalid username-password pair\")\n\n\t\t// Wrap it multiple times\n\t\twrappedErr := fmt.Errorf(\"connection error: %w\", authErr)\n\t\tdoubleWrappedErr := fmt.Errorf(\"client error: %w\", wrappedErr)\n\n\t\t// Should still be detected\n\t\tif !redis.IsAuthError(doubleWrappedErr) {\n\t\t\tt.Errorf(\"IsAuthError should detect double-wrapped WRONGPASS error\")\n\t\t}\n\t})\n\n\tt.Run(\"Wrapped unauthenticated error\", func(t *testing.T) {\n\t\t// Create an auth error\n\t\tauthErr := proto.NewAuthError(\"ERR unauthenticated\")\n\n\t\t// Wrap it\n\t\twrappedErr := fmt.Errorf(\"hook: %w\", authErr)\n\n\t\t// Should still be detected\n\t\tif !redis.IsAuthError(wrappedErr) {\n\t\t\tt.Errorf(\"IsAuthError should detect wrapped unauthenticated error\")\n\t\t}\n\t})\n}\n\nfunc TestPermissionErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped NOPERM error\", func(t *testing.T) {\n\t\t// Create a permission error\n\t\tpermErr := proto.NewPermissionError(\"NOPERM this user has no permissions to run the 'flushdb' command\")\n\n\t\t// Wrap it\n\t\twrappedErr := fmt.Errorf(\"hook: %w\", permErr)\n\n\t\t// Should still be detected\n\t\tif !redis.IsPermissionError(wrappedErr) {\n\t\t\tt.Errorf(\"IsPermissionError should detect wrapped NOPERM error\")\n\t\t}\n\t})\n}\n\nfunc TestExecAbortErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped EXECABORT error\", func(t *testing.T) {\n\t\t// Create an EXECABORT error\n\t\texecAbortErr := proto.NewExecAbortError(\"EXECABORT Transaction discarded because of previous errors\")\n\n\t\t// Wrap it\n\t\twrappedErr := fmt.Errorf(\"hook: %w\", execAbortErr)\n\n\t\t// Should still be detected\n\t\tif !redis.IsExecAbortError(wrappedErr) {\n\t\t\tt.Errorf(\"IsExecAbortError should detect wrapped EXECABORT error\")\n\t\t}\n\t})\n}\n\nfunc TestOOMErrorWrapping(t *testing.T) {\n\tt.Run(\"Wrapped OOM error\", func(t *testing.T) {\n\t\t// Create an OOM error\n\t\toomErr := proto.NewOOMError(\"OOM command not allowed when used memory > 'maxmemory'\")\n\n\t\t// Wrap it\n\t\twrappedErr := fmt.Errorf(\"hook: %w\", oomErr)\n\n\t\t// Should still be detected\n\t\tif !redis.IsOOMError(wrappedErr) {\n\t\t\tt.Errorf(\"IsOOMError should detect wrapped OOM error\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "example/cluster-mget/README.md",
    "content": "# Redis Cluster MGET Example\n\nThis example demonstrates how to use the Redis Cluster client with the `MGET` command to retrieve multiple keys efficiently.\n\n## Overview\n\nThe example shows:\n- Creating a Redis Cluster client\n- Setting 10 keys with individual `SET` commands\n- Retrieving all 10 keys in a single operation using `MGET`\n- Validating that the retrieved values match the expected values\n- Cleaning up by deleting the test keys\n\n## Prerequisites\n\nYou need a running Redis Cluster. The example expects cluster nodes at:\n- `localhost:7000`\n- `localhost:7001`\n- `localhost:7002`\n\n### Setting up a Redis Cluster (using Docker)\n\nIf you don't have a Redis Cluster running, you can use the docker-compose setup from the repository root:\n\n```bash\n# From the go-redis repository root\ndocker compose --profile cluster up -d\n```\n\nThis will start a Redis Cluster with nodes on ports 16600-16605.\n\nIf using the docker-compose cluster, update the `Addrs` in `main.go` to:\n```go\nAddrs: []string{\n    \"localhost:16600\",\n    \"localhost:16601\",\n    \"localhost:16602\",\n},\n```\n\n## Running the Example\n\n```bash\ngo run main.go\n```\n\n## Expected Output\n\n```\n✓ Connected to Redis cluster\n\n=== Setting 10 keys ===\n✓ SET key0 = value0\n✓ SET key1 = value1\n✓ SET key2 = value2\n✓ SET key3 = value3\n✓ SET key4 = value4\n✓ SET key5 = value5\n✓ SET key6 = value6\n✓ SET key7 = value7\n✓ SET key8 = value8\n✓ SET key9 = value9\n\n=== Retrieving keys with MGET ===\n\n=== Validating MGET results ===\n✓ key0: value0\n✓ key1: value1\n✓ key2: value2\n✓ key3: value3\n✓ key4: value4\n✓ key5: value5\n✓ key6: value6\n✓ key7: value7\n✓ key8: value8\n✓ key9: value9\n\n=== Summary ===\n✓ All values retrieved successfully and match expected values!\n\n=== Cleaning up ===\n✓ Cleanup complete\n```\n\n## Key Concepts\n\n### MGET Command\n\n`MGET` (Multiple GET) is a Redis command that retrieves the values of multiple keys in a single operation. This is more efficient than executing multiple individual `GET` commands.\n\n**Syntax:**\n```go\nresult, err := rdb.MGet(ctx, key1, key2, key3, ...).Result()\n```\n\n**Returns:**\n- A slice of `interface{}` values\n- Each value corresponds to a key in the same order\n- `nil` is returned for keys that don't exist\n\n### Cluster Client\n\nThe `ClusterClient` automatically handles:\n- Distributing keys across cluster nodes based on hash slots\n- Following cluster redirects\n- Maintaining connections to all cluster nodes\n- Retrying operations on cluster topology changes\n\nFor `MGET` operations in a cluster, the client may need to split the request across multiple nodes if the keys map to different hash slots.\n\n## Code Highlights\n\n```go\n// Create cluster client\nrdb := redis.NewClusterClient(&redis.ClusterOptions{\n    Addrs: []string{\n        \"localhost:7000\",\n        \"localhost:7001\",\n        \"localhost:7002\",\n    },\n})\n\n// Set individual keys\nfor i := 0; i < 10; i++ {\n    err := rdb.Set(ctx, fmt.Sprintf(\"key%d\", i), fmt.Sprintf(\"value%d\", i), 0).Err()\n    // handle error\n}\n\n// Retrieve all keys with MGET\nresult, err := rdb.MGet(ctx, keys...).Result()\n\n// Validate results\nfor i, val := range result {\n    actualValue, ok := val.(string)\n    // validate actualValue matches expected\n}\n```\n\n## Learn More\n\n- [Redis MGET Documentation](https://redis.io/commands/mget/)\n- [Redis Cluster Specification](https://redis.io/topics/cluster-spec)\n- [go-redis Documentation](https://redis.uptrace.dev/)\n\n"
  },
  {
    "path": "example/cluster-mget/go.mod",
    "content": "module github.com/redis/go-redis/example/cluster-mget\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.16.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/cluster-mget/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/cluster-mget/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\t// Create a cluster client\n\trdb := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs: []string{\n\t\t\t\"localhost:16600\",\n\t\t\t\"localhost:16601\",\n\t\t\t\"localhost:16602\",\n\t\t\t\"localhost:16603\",\n\t\t\t\"localhost:16604\",\n\t\t\t\"localhost:16605\",\n\t\t},\n\t})\n\tdefer rdb.Close()\n\n\t// Test connection\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\tpanic(fmt.Sprintf(\"Failed to connect to Redis cluster: %v\", err))\n\t}\n\n\tfmt.Println(\"✓ Connected to Redis cluster\")\n\n\t// Define 10 keys and values\n\tkeys := make([]string, 10)\n\tvalues := make([]string, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tkeys[i] = fmt.Sprintf(\"key%d\", i)\n\t\tvalues[i] = fmt.Sprintf(\"value%d\", i)\n\t}\n\n\t// Set all 10 keys\n\tfmt.Println(\"\\n=== Setting 10 keys ===\")\n\tfor i := 0; i < 10; i++ {\n\t\terr := rdb.Set(ctx, keys[i], values[i], 0).Err()\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"Failed to set %s: %v\", keys[i], err))\n\t\t}\n\t\tfmt.Printf(\"✓ SET %s = %s\\n\", keys[i], values[i])\n\t}\n\n\t/*\n\t\t// Retrieve all keys using MGET\n\t\tfmt.Println(\"\\n=== Retrieving keys with MGET ===\")\n\t\tresult, err := rdb.MGet(ctx, keys...).Result()\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"Failed to execute MGET: %v\", err))\n\t\t}\n\t*/\n\n\t/*\n\t\t// Validate the results\n\t\tfmt.Println(\"\\n=== Validating MGET results ===\")\n\t\tallValid := true\n\t\tfor i, val := range result {\n\t\t\texpectedValue := values[i]\n\t\t\tactualValue, ok := val.(string)\n\n\t\t\tif !ok {\n\t\t\t\tfmt.Printf(\"✗ %s: expected string, got %T\\n\", keys[i], val)\n\t\t\t\tallValid = false\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif actualValue != expectedValue {\n\t\t\t\tfmt.Printf(\"✗ %s: expected '%s', got '%s'\\n\", keys[i], expectedValue, actualValue)\n\t\t\t\tallValid = false\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"✓ %s: %s\\n\", keys[i], actualValue)\n\t\t\t}\n\t\t}\n\n\t\t// Print summary\n\t\tfmt.Println(\"\\n=== Summary ===\")\n\t\tif allValid {\n\t\t\tfmt.Println(\"✓ All values retrieved successfully and match expected values!\")\n\t\t} else {\n\t\t\tfmt.Println(\"✗ Some values did not match expected values\")\n\t\t}\n\t*/\n\n\t// Clean up - delete the keys\n\tfmt.Println(\"\\n=== Cleaning up ===\")\n\tfor _, key := range keys {\n\t\tif err := rdb.Del(ctx, key).Err(); err != nil {\n\t\t\tfmt.Printf(\"Warning: Failed to delete %s: %v\\n\", key, err)\n\t\t}\n\t}\n\tfmt.Println(\"✓ Cleanup complete\")\n\n\terr := rdb.Set(ctx, \"{tag}exists\", \"asdf\", 0).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tval, err := rdb.Get(ctx, \"{tag}nilkeykey1\").Result()\n\tfmt.Printf(\"\\nval: %+v err: %+v\\n\", val, err)\n\tvalm, err := rdb.MGet(ctx, \"{tag}nilkeykey1\", \"{tag}exists\").Result()\n\tfmt.Printf(\"\\nval: %+v err: %+v\\n\", valm, err)\n}\n"
  },
  {
    "path": "example/del-keys-without-ttl/README.md",
    "content": "# Delete keys without a ttl\n\nThis example demonstrates how to use `SCAN` and pipelines to efficiently delete keys without a TTL.\n\nTo run this example:\n\n```shell\ngo run .\n```\n\nSee [documentation](https://redis.uptrace.dev/guide/get-all-keys.html) for more details.\n"
  },
  {
    "path": "example/del-keys-without-ttl/go.mod",
    "content": "module github.com/redis/go-redis/example/del-keys-without-ttl\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire (\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgo.uber.org/zap v1.24.0\n)\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgo.uber.org/multierr v1.9.0 // indirect\n)\n"
  },
  {
    "path": "example/del-keys-without-ttl/go.sum",
    "content": "github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=\ngo.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=\ngo.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=\ngo.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=\ngo.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "example/del-keys-without-ttl/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\n\t_ = rdb.Set(ctx, \"key_with_ttl\", \"bar\", time.Minute).Err()\n\t_ = rdb.Set(ctx, \"key_without_ttl_1\", \"\", 0).Err()\n\t_ = rdb.Set(ctx, \"key_without_ttl_2\", \"\", 0).Err()\n\n\tchecker := NewKeyChecker(rdb, 100)\n\n\tstart := time.Now()\n\tchecker.Start(ctx)\n\n\titer := rdb.Scan(ctx, 0, \"\", 0).Iterator()\n\tfor iter.Next(ctx) {\n\t\tchecker.Add(iter.Val())\n\t}\n\tif err := iter.Err(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tdeleted := checker.Stop()\n\tfmt.Println(\"deleted\", deleted, \"keys\", \"in\", time.Since(start))\n}\n\ntype KeyChecker struct {\n\trdb       *redis.Client\n\tbatchSize int\n\tch        chan string\n\tdelCh     chan string\n\twg        sync.WaitGroup\n\tdeleted   int\n\tlogger    *zap.Logger\n}\n\nfunc NewKeyChecker(rdb *redis.Client, batchSize int) *KeyChecker {\n\treturn &KeyChecker{\n\t\trdb:       rdb,\n\t\tbatchSize: batchSize,\n\t\tch:        make(chan string, batchSize),\n\t\tdelCh:     make(chan string, batchSize),\n\t\tlogger:    zap.L(),\n\t}\n}\n\nfunc (c *KeyChecker) Add(key string) {\n\tc.ch <- key\n}\n\nfunc (c *KeyChecker) Start(ctx context.Context) {\n\tc.wg.Add(1)\n\tgo func() {\n\t\tdefer c.wg.Done()\n\t\tif err := c.del(ctx); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tc.wg.Add(1)\n\tgo func() {\n\t\tdefer c.wg.Done()\n\t\tdefer close(c.delCh)\n\n\t\tkeys := make([]string, 0, c.batchSize)\n\n\t\tfor key := range c.ch {\n\t\t\tkeys = append(keys, key)\n\t\t\tif len(keys) < cap(keys) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := c.checkKeys(ctx, keys); err != nil {\n\t\t\t\tc.logger.Error(\"checkKeys failed\", zap.Error(err))\n\t\t\t}\n\t\t\tkeys = keys[:0]\n\t\t}\n\n\t\tif len(keys) > 0 {\n\t\t\tif err := c.checkKeys(ctx, keys); err != nil {\n\t\t\t\tc.logger.Error(\"checkKeys failed\", zap.Error(err))\n\t\t\t}\n\t\t\tkeys = nil\n\t\t}\n\t}()\n}\n\nfunc (c *KeyChecker) Stop() int {\n\tclose(c.ch)\n\tc.wg.Wait()\n\treturn c.deleted\n}\n\nfunc (c *KeyChecker) checkKeys(ctx context.Context, keys []string) error {\n\tcmds, err := c.rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\tfor _, key := range keys {\n\t\t\tpipe.TTL(ctx, key)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i, cmd := range cmds {\n\t\td, err := cmd.(*redis.DurationCmd).Result()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif d == -1 {\n\t\t\tc.delCh <- keys[i]\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *KeyChecker) del(ctx context.Context) error {\n\tpipe := c.rdb.Pipeline()\n\n\tfor key := range c.delCh {\n\t\tfmt.Printf(\"deleting %s...\\n\", key)\n\t\tpipe.Del(ctx, key)\n\t\tc.deleted++\n\n\t\tif pipe.Len() < c.batchSize {\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := pipe.Exec(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif _, err := pipe.Exec(ctx); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "example/digest-optimistic-locking/README.md",
    "content": "# Redis Digest & Optimistic Locking Example\n\nThis example demonstrates how to use Redis DIGEST command and digest-based optimistic locking with go-redis.\n\n## What is Redis DIGEST?\n\nThe DIGEST command (Redis 8.4+) returns a 64-bit xxh3 hash of a key's value. This hash can be used for:\n\n- **Optimistic locking**: Update values only if they haven't changed\n- **Change detection**: Detect if a value was modified\n- **Conditional operations**: Delete or update based on expected content\n\n## Features Demonstrated\n\n1. **Basic Digest Usage**: Get digest from Redis and verify with client-side calculation\n2. **Optimistic Locking with SetIFDEQ**: Update only if digest matches (value unchanged)\n3. **Change Detection with SetIFDNE**: Update only if digest differs (value changed)\n4. **Conditional Delete**: Delete only if digest matches expected value\n5. **Client-Side Digest Generation**: Calculate digests without fetching from Redis\n\n## Requirements\n\n- Redis 8.4+ (for DIGEST command support)\n- Go 1.18+\n\n## Installation\n\n```bash\ncd example/digest-optimistic-locking\ngo mod tidy\n```\n\n## Running the Example\n\n```bash\n# Make sure Redis 8.4+ is running on localhost:6379\nredis-server\n\n# In another terminal, run the example\ngo run .\n```\n\n## Expected Output\n\n```\n=== Redis Digest & Optimistic Locking Example ===\n\n1. Basic Digest Usage\n---------------------\nKey: user:1000:name\nValue: Alice\nDigest: 7234567890123456789 (0x6478a1b2c3d4e5f6)\nClient-calculated digest: 7234567890123456789 (0x6478a1b2c3d4e5f6)\n✓ Digests match!\n\n2. Optimistic Locking with SetIFDEQ\n------------------------------------\nInitial value: 100\nCurrent digest: 0x1234567890abcdef\n✓ Update successful! New value: 150\n✓ Correctly rejected update with wrong digest\n\n3. Detecting Changes with SetIFDNE\n-----------------------------------\nInitial value: v1.0.0\nOld digest: 0xabcdef1234567890\n✓ Value changed! Updated to: v2.0.0\n✓ Correctly rejected: current value matches the digest\n\n4. Conditional Delete with DelExArgs\n-------------------------------------\nCreated session: session:abc123\nExpected digest: 0x9876543210fedcba\n✓ Correctly refused to delete (wrong digest)\n✓ Successfully deleted with correct digest\n✓ Session deleted\n\n5. Client-Side Digest Generation\n---------------------------------\nCurrent price: $29.99\nExpected digest (calculated client-side): 0xfedcba0987654321\n✓ Price updated successfully to $24.99\n\nBinary data example:\nBinary data digest: 0x1122334455667788\n✓ Binary digest matches!\n\n=== All examples completed successfully! ===\n```\n\n## How It Works\n\n### Digest Calculation\n\nRedis uses the **xxh3** hashing algorithm. The go-redis library provides built-in helper functions to calculate digests client-side:\n\n```go\nimport \"github.com/redis/go-redis/v9/helper\"\n\n// For strings\ndigest := helper.DigestString(\"myvalue\")\n\n// For binary data\ndigest := helper.DigestBytes([]byte{0x01, 0x02, 0x03})\n```\n\n### Optimistic Locking Pattern\n\n```go\n// 1. Read current value and get its digest\ncurrentValue := rdb.Get(ctx, \"key\").Val()\ncurrentDigest := rdb.Digest(ctx, \"key\").Val()\n\n// 2. Perform business logic\nnewValue := processValue(currentValue)\n\n// 3. Update only if value hasn't changed\nresult := rdb.SetIFDEQ(ctx, \"key\", newValue, currentDigest, 0)\nif result.Err() == redis.Nil {\n    // Value was modified by another client - retry or handle conflict\n}\n```\n\n### Client-Side Digest (No Extra Round Trip)\n\n```go\nimport \"github.com/redis/go-redis/v9/helper\"\n\n// If you know the expected current value, calculate digest client-side\nexpectedValue := \"100\"\nexpectedDigest := helper.DigestString(expectedValue)\n\n// Update without fetching digest from Redis first\nresult := rdb.SetIFDEQ(ctx, \"counter\", \"150\", expectedDigest, 0)\n```\n\n## Use Cases\n\n### 1. Distributed Counter with Conflict Detection\n\n```go\n// Multiple clients can safely update a counter\ncurrentValue := rdb.Get(ctx, \"counter\").Val()\ncurrentDigest := rdb.Digest(ctx, \"counter\").Val()\n\nnewValue := incrementCounter(currentValue)\n\n// Only succeeds if no other client modified it\nif rdb.SetIFDEQ(ctx, \"counter\", newValue, currentDigest, 0).Err() == redis.Nil {\n    // Retry with new value\n}\n```\n\n### 2. Session Management\n\n```go\nimport \"github.com/redis/go-redis/v9/helper\"\n\n// Delete session only if it contains expected data\nsessionData := \"user:1234:active\"\nexpectedDigest := helper.DigestString(sessionData)\n\ndeleted := rdb.DelExArgs(ctx, \"session:xyz\", redis.DelExArgs{\n    Mode:        \"IFDEQ\",\n    MatchDigest: expectedDigest,\n}).Val()\n```\n\n### 3. Configuration Updates\n\n```go\nimport \"github.com/redis/go-redis/v9/helper\"\n\n// Update config only if it changed\noldConfig := loadOldConfig()\noldDigest := helper.DigestString(oldConfig)\n\nnewConfig := loadNewConfig()\n\n// Only update if config actually changed\nresult := rdb.SetIFDNE(ctx, \"config\", newConfig, oldDigest, 0)\nif result.Err() != redis.Nil {\n    fmt.Println(\"Config updated!\")\n}\n```\n\n## Advantages Over WATCH/MULTI/EXEC\n\n- **Simpler**: Single command instead of transaction\n- **Faster**: No transaction overhead\n- **Client-side digest**: Can calculate expected digest without fetching from Redis\n- **Works with any command**: Not limited to transactions\n\n## Learn More\n\n- [Redis DIGEST command](https://redis.io/commands/digest/)\n- [Redis SET command with IFDEQ/IFDNE](https://redis.io/commands/set/)\n- [xxh3 hashing algorithm](https://github.com/Cyan4973/xxHash)\n\n## Helper Functions Reference\n\nThe `github.com/redis/go-redis/v9/helper` package provides:\n\n| Function | Description |\n|----------|-------------|\n| `DigestString(s string) uint64` | Computes xxh3 hash of a string |\n| `DigestBytes(data []byte) uint64` | Computes xxh3 hash of a byte slice |\n\nBoth functions produce the **exact same hash** as the Redis DIGEST command.\n"
  },
  {
    "path": "example/digest-optimistic-locking/go.mod",
    "content": "module github.com/redis/go-redis/example/digest-optimistic-locking\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.18.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.10 // indirect\n\tgithub.com/zeebo/xxh3 v1.1.0 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n)\n"
  },
  {
    "path": "example/digest-optimistic-locking/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=\ngithub.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/digest-optimistic-locking/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/helper\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\t// Connect to Redis\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer rdb.Close()\n\n\t// Ping to verify connection\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to connect to Redis: %v\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Println(\"=== Redis Digest & Optimistic Locking Example ===\")\n\tfmt.Println()\n\n\t// Example 1: Basic Digest Usage\n\tfmt.Println(\"1. Basic Digest Usage\")\n\tfmt.Println(\"---------------------\")\n\tbasicDigestExample(ctx, rdb)\n\tfmt.Println()\n\n\t// Example 2: Optimistic Locking with SetIFDEQ\n\tfmt.Println(\"2. Optimistic Locking with SetIFDEQ\")\n\tfmt.Println(\"------------------------------------\")\n\toptimisticLockingExample(ctx, rdb)\n\tfmt.Println()\n\n\t// Example 3: Detecting Changes with SetIFDNE\n\tfmt.Println(\"3. Detecting Changes with SetIFDNE\")\n\tfmt.Println(\"-----------------------------------\")\n\tdetectChangesExample(ctx, rdb)\n\tfmt.Println()\n\n\t// Example 4: Conditional Delete with DelExArgs\n\tfmt.Println(\"4. Conditional Delete with DelExArgs\")\n\tfmt.Println(\"-------------------------------------\")\n\tconditionalDeleteExample(ctx, rdb)\n\tfmt.Println()\n\n\t// Example 5: Client-Side Digest Generation\n\tfmt.Println(\"5. Client-Side Digest Generation\")\n\tfmt.Println(\"---------------------------------\")\n\tclientSideDigestExample(ctx, rdb)\n\tfmt.Println()\n\n\tfmt.Println(\"=== All examples completed successfully! ===\")\n}\n\n// basicDigestExample demonstrates getting a digest from Redis\nfunc basicDigestExample(ctx context.Context, rdb *redis.Client) {\n\t// Set a value\n\tkey := \"user:1000:name\"\n\tvalue := \"Alice\"\n\trdb.Set(ctx, key, value, 0)\n\n\t// Get the digest\n\tdigest := rdb.Digest(ctx, key).Val()\n\n\tfmt.Printf(\"Key: %s\\n\", key)\n\tfmt.Printf(\"Value: %s\\n\", value)\n\tfmt.Printf(\"Digest: %d (0x%016x)\\n\", digest, digest)\n\n\t// Verify with client-side calculation\n\tclientDigest := helper.DigestString(value)\n\tfmt.Printf(\"Client-calculated digest: %d (0x%016x)\\n\", clientDigest, clientDigest)\n\n\tif digest == clientDigest {\n\t\tfmt.Println(\"✓ Digests match!\")\n\t}\n}\n\n// optimisticLockingExample demonstrates using SetIFDEQ for optimistic locking\nfunc optimisticLockingExample(ctx context.Context, rdb *redis.Client) {\n\tkey := \"counter\"\n\n\t// Initial value\n\trdb.Set(ctx, key, \"100\", 0)\n\tfmt.Printf(\"Initial value: %s\\n\", rdb.Get(ctx, key).Val())\n\n\t// Get current digest\n\tcurrentDigest := rdb.Digest(ctx, key).Val()\n\tfmt.Printf(\"Current digest: 0x%016x\\n\", currentDigest)\n\n\t// Simulate some processing time\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Try to update only if value hasn't changed (digest matches)\n\tnewValue := \"150\"\n\tresult := rdb.SetIFDEQ(ctx, key, newValue, currentDigest, 0)\n\n\tif result.Err() == redis.Nil {\n\t\tfmt.Println(\"✗ Update failed: value was modified by another client\")\n\t} else if result.Err() != nil {\n\t\tfmt.Printf(\"✗ Error: %v\\n\", result.Err())\n\t} else {\n\t\tfmt.Printf(\"✓ Update successful! New value: %s\\n\", rdb.Get(ctx, key).Val())\n\t}\n\n\t// Try again with wrong digest (simulating concurrent modification)\n\twrongDigest := uint64(12345)\n\tresult = rdb.SetIFDEQ(ctx, key, \"200\", wrongDigest, 0)\n\n\tif result.Err() == redis.Nil {\n\t\tfmt.Println(\"✓ Correctly rejected update with wrong digest\")\n\t}\n}\n\n// detectChangesExample demonstrates using SetIFDNE to detect if a value changed\nfunc detectChangesExample(ctx context.Context, rdb *redis.Client) {\n\tkey := \"config:version\"\n\n\t// Set initial value\n\toldValue := \"v1.0.0\"\n\trdb.Set(ctx, key, oldValue, 0)\n\tfmt.Printf(\"Initial value: %s\\n\", oldValue)\n\n\t// Calculate digest of a DIFFERENT value (what we expect it NOT to be)\n\tunwantedValue := \"v0.9.0\"\n\tunwantedDigest := helper.DigestString(unwantedValue)\n\tfmt.Printf(\"Unwanted value digest: 0x%016x\\n\", unwantedDigest)\n\n\t// Update to new value only if current value is NOT the unwanted value\n\t// (i.e., only if digest does NOT match unwantedDigest)\n\tnewValue := \"v2.0.0\"\n\tresult := rdb.SetIFDNE(ctx, key, newValue, unwantedDigest, 0)\n\n\tif result.Err() == redis.Nil {\n\t\tfmt.Println(\"✗ Current value matches unwanted value (digest matches)\")\n\t} else if result.Err() != nil {\n\t\tfmt.Printf(\"✗ Error: %v\\n\", result.Err())\n\t} else {\n\t\tfmt.Printf(\"✓ Current value is different from unwanted value! Updated to: %s\\n\", rdb.Get(ctx, key).Val())\n\t}\n\n\t// Try to update again, but this time the digest matches current value (should fail)\n\tcurrentDigest := rdb.Digest(ctx, key).Val()\n\tresult = rdb.SetIFDNE(ctx, key, \"v3.0.0\", currentDigest, 0)\n\n\tif result.Err() == redis.Nil {\n\t\tfmt.Println(\"✓ Correctly rejected: current value matches the digest (IFDNE failed)\")\n\t}\n}\n\n// conditionalDeleteExample demonstrates using DelExArgs with digest\nfunc conditionalDeleteExample(ctx context.Context, rdb *redis.Client) {\n\tkey := \"session:abc123\"\n\tvalue := \"user_data_here\"\n\n\t// Set a value\n\trdb.Set(ctx, key, value, 0)\n\tfmt.Printf(\"Created session: %s\\n\", key)\n\n\t// Calculate expected digest\n\texpectedDigest := helper.DigestString(value)\n\tfmt.Printf(\"Expected digest: 0x%016x\\n\", expectedDigest)\n\n\t// Try to delete with wrong digest (should fail)\n\twrongDigest := uint64(99999)\n\tdeleted := rdb.DelExArgs(ctx, key, redis.DelExArgs{\n\t\tMode:        \"IFDEQ\",\n\t\tMatchDigest: wrongDigest,\n\t}).Val()\n\n\tif deleted == 0 {\n\t\tfmt.Println(\"✓ Correctly refused to delete (wrong digest)\")\n\t}\n\n\t// Delete with correct digest (should succeed)\n\tdeleted = rdb.DelExArgs(ctx, key, redis.DelExArgs{\n\t\tMode:        \"IFDEQ\",\n\t\tMatchDigest: expectedDigest,\n\t}).Val()\n\n\tif deleted == 1 {\n\t\tfmt.Println(\"✓ Successfully deleted with correct digest\")\n\t}\n\n\t// Verify deletion\n\texists := rdb.Exists(ctx, key).Val()\n\tif exists == 0 {\n\t\tfmt.Println(\"✓ Session deleted\")\n\t}\n}\n\n// clientSideDigestExample demonstrates calculating digests without fetching from Redis\nfunc clientSideDigestExample(ctx context.Context, rdb *redis.Client) {\n\tkey := \"product:1001:price\"\n\n\t// Scenario: We know the expected current value\n\texpectedCurrentValue := \"29.99\"\n\tnewValue := \"24.99\"\n\n\t// Set initial value\n\trdb.Set(ctx, key, expectedCurrentValue, 0)\n\tfmt.Printf(\"Current price: $%s\\n\", expectedCurrentValue)\n\n\t// Calculate digest client-side (no need to fetch from Redis!)\n\texpectedDigest := helper.DigestString(expectedCurrentValue)\n\tfmt.Printf(\"Expected digest (calculated client-side): 0x%016x\\n\", expectedDigest)\n\n\t// Update price only if it matches our expectation\n\tresult := rdb.SetIFDEQ(ctx, key, newValue, expectedDigest, 0)\n\n\tif result.Err() == redis.Nil {\n\t\tfmt.Println(\"✗ Price was already changed by someone else\")\n\t\tactualValue := rdb.Get(ctx, key).Val()\n\t\tfmt.Printf(\"  Actual current price: $%s\\n\", actualValue)\n\t} else if result.Err() != nil {\n\t\tfmt.Printf(\"✗ Error: %v\\n\", result.Err())\n\t} else {\n\t\tfmt.Printf(\"✓ Price updated successfully to $%s\\n\", newValue)\n\t}\n\n\t// Demonstrate with binary data\n\tfmt.Println(\"\\nBinary data example:\")\n\tbinaryKey := \"image:thumbnail\"\n\tbinaryData := []byte{0xFF, 0xD8, 0xFF, 0xE0} // JPEG header\n\n\trdb.Set(ctx, binaryKey, binaryData, 0)\n\n\t// Calculate digest for binary data\n\tbinaryDigest := helper.DigestBytes(binaryData)\n\tfmt.Printf(\"Binary data digest: 0x%016x\\n\", binaryDigest)\n\n\t// Verify it matches Redis\n\tredisDigest := rdb.Digest(ctx, binaryKey).Val()\n\tif binaryDigest == redisDigest {\n\t\tfmt.Println(\"✓ Binary digest matches!\")\n\t}\n}\n"
  },
  {
    "path": "example/disable-maintnotifications/README.md",
    "content": "# Disable Maintenance Notifications Example\n\nThis example demonstrates how to use the go-redis client with maintenance notifications **disabled**.\n\n## What are Maintenance Notifications?\n\nMaintenance notifications are a Redis Cloud feature that allows the server to notify clients about:\n- Planned maintenance events\n- Failover operations\n- Node migrations\n- Cluster topology changes\n\nThe go-redis client supports three modes:\n- **`ModeDisabled`**: Client doesn't send `CLIENT MAINT_NOTIFICATIONS ON` command\n- **`ModeEnabled`**: Client forcefully sends the command, interrupts connection on error\n- **`ModeAuto`** (default): Client tries to send the command, disables feature on error\n\n## When to Disable Maintenance Notifications\n\nYou should disable maintenance notifications when:\n\n1. **Connecting to non-Redis Cloud / Redis Enterprise instances** - Standard Redis servers don't support this feature\n2. **You want to handle failovers manually** - Your application has custom failover logic\n3. **Minimizing client-side overhead** - You want the simplest possible client behavior\n4. **The Redis server doesn't support the feature** - Older Redis versions or forks\n\n## Usage\n\n### Basic Example\n\n```go\nimport (\n    \"github.com/redis/go-redis/v9\"\n    \"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\nrdb := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6379\",\n\n    // Explicitly disable maintenance notifications\n    MaintNotificationsConfig: &maintnotifications.Config{\n        Mode: maintnotifications.ModeDisabled,\n    },\n})\ndefer rdb.Close()\n```\n\n### Cluster Client Example\n\n```go\nrdbCluster := redis.NewClusterClient(&redis.ClusterOptions{\n    Addrs: []string{\"localhost:7000\", \"localhost:7001\", \"localhost:7002\"},\n\n    // Disable maintenance notifications for cluster\n    MaintNotificationsConfig: &maintnotifications.Config{\n        Mode: maintnotifications.ModeDisabled,\n    },\n})\ndefer rdbCluster.Close()\n```\n\n### Default Behavior (ModeAuto)\n\nIf you don't specify `MaintNotifications`, the client defaults to `ModeAuto`:\n\n```go\n// This uses ModeAuto by default\nrdb := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6379\",\n    // MaintNotificationsConfig: nil means ModeAuto\n})\n```\n\nWith `ModeAuto`, the client will:\n1. Try to enable maintenance notifications\n2. If the server doesn't support it, silently disable the feature\n3. Continue normal operation\n\n## Running the Example\n\n1. Start a Redis server:\n   ```bash\n   redis-server --port 6379\n   ```\n\n2. Run the example:\n   ```bash\n   go run main.go\n   ```\n\n## Expected Output\n\n```\n=== Example 1: Explicitly Disabled ===\n✓ Connected successfully (maintenance notifications disabled)\n✓ SET operation successful\n✓ GET operation successful: value1\n\n=== Example 2: Default Behavior (ModeAuto) ===\n✓ Connected successfully (maintenance notifications auto-enabled)\n\n=== Example 3: Cluster Client with Disabled Notifications ===\nCluster not available (expected): ...\n\n=== Example 4: Performance Comparison ===\n✓ 1000 SET operations (disabled): 45ms\n✓ 1000 SET operations (auto): 46ms\n\n=== Cleanup ===\n✓ Database flushed\n\n=== Summary ===\nMaintenance notifications can be disabled by setting:\n  MaintNotificationsConfig: &maintnotifications.Config{\n    Mode: maintnotifications.ModeDisabled,\n  }\n\nThis is useful when:\n  - Connecting to non-Redis Cloud instances\n  - You want to handle failovers manually\n  - You want to minimize client-side overhead\n  - The Redis server doesn't support CLIENT MAINT_NOTIFICATIONS\n```\n\n## Performance Impact\n\nDisabling maintenance notifications has minimal performance impact. The main differences are:\n\n1. **Connection Setup**: One less command (`CLIENT MAINT_NOTIFICATIONS ON`) during connection initialization\n2. **Runtime Overhead**: No background processing of maintenance notifications\n3. **Memory Usage**: Slightly lower memory footprint (no notification handlers)\n\nIn most cases, the performance difference is negligible (< 1%)."
  },
  {
    "path": "example/disable-maintnotifications/go.mod",
    "content": "module github.com/redis/go-redis/example/disable-maintnotifications\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.7.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/disable-maintnotifications/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/disable-maintnotifications/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\t// Example 0: Explicitly disable maintenance notifications\n\tfmt.Println(\"=== Example 0: Explicitly Enabled ===\")\n\trdb0 := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\n\t\t// Explicitly disable maintenance notifications\n\t\t// This prevents the client from sending CLIENT MAINT_NOTIFICATIONS ON\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeEnabled,\n\t\t},\n\t})\n\tdefer rdb0.Close()\n\n\t// Test the connection\n\tif err := rdb0.Ping(ctx).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to connect: %v\\n\\n\", err)\n\t}\n\tfmt.Println(\"When ModeEnabled, the client will return an error if the server doesn't support maintenance notifications.\")\n\tfmt.Printf(\"ModeAuto will silently disable the feature.\\n\\n\")\n\n\t// Example 1: Explicitly disable maintenance notifications\n\tfmt.Println(\"=== Example 1: Explicitly Disabled ===\")\n\trdb1 := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\n\t\t// Explicitly disable maintenance notifications\n\t\t// This prevents the client from sending CLIENT MAINT_NOTIFICATIONS ON\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeDisabled,\n\t\t},\n\t})\n\tdefer rdb1.Close()\n\n\t// Test the connection\n\tif err := rdb1.Ping(ctx).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to connect: %v\\n\\n\", err)\n\t\treturn\n\t}\n\tfmt.Println(\"✓ Connected successfully (maintenance notifications disabled)\")\n\n\t// Perform some operations\n\tif err := rdb1.Set(ctx, \"example:key1\", \"value1\", 0).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to set key: %v\\n\\n\", err)\n\t\treturn\n\t}\n\tfmt.Println(\"✓ SET operation successful\")\n\n\tval, err := rdb1.Get(ctx, \"example:key1\").Result()\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to get key: %v\\n\\n\", err)\n\t\treturn\n\t}\n\tfmt.Printf(\"✓ GET operation successful: %s\\n\\n\", val)\n\n\t// Example 2: Using nil config (defaults to ModeAuto)\n\tfmt.Printf(\"\\n=== Example 2: Default Behavior (ModeAuto) ===\\n\")\n\trdb2 := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t\t// MaintNotifications: nil means ModeAuto (enabled for Redis Cloud)\n\t})\n\tdefer rdb2.Close()\n\n\tif err := rdb2.Ping(ctx).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to connect: %v\\n\\n\", err)\n\t\treturn\n\t}\n\tfmt.Println(\"✓ Connected successfully (maintenance notifications auto-enabled)\")\n\n\t// Example 4: Comparing behavior with and without maintenance notifications\n\tfmt.Printf(\"\\n=== Example 4: Performance Comparison ===\\n\")\n\n\t// Client with auto-enabled notifications\n\tstartauto := time.Now()\n\tfor i := 0; i < 1000; i++ {\n\t\tkey := fmt.Sprintf(\"test:auto:%d\", i)\n\t\tif err := rdb2.Set(ctx, key, i, time.Minute).Err(); err != nil {\n\t\t\tfmt.Printf(\"Failed to set key: %v\\n\", err)\n\t\t\treturn\n\t\t}\n\t}\n\tautoDuration := time.Since(startauto)\n\tfmt.Printf(\"✓ 1000 SET operations (auto): %v\\n\", autoDuration)\n\n\t// print pool stats\n\tfmt.Printf(\"Pool stats (auto): %+v\\n\", rdb2.PoolStats())\n\n\t// give the server a moment to take chill\n\tfmt.Println(\"---\")\n\ttime.Sleep(time.Second)\n\n\t// Client with disabled notifications\n\tstart := time.Now()\n\tfor i := 0; i < 1000; i++ {\n\t\tkey := fmt.Sprintf(\"test:disabled:%d\", i)\n\t\tif err := rdb1.Set(ctx, key, i, time.Minute).Err(); err != nil {\n\t\t\tfmt.Printf(\"Failed to set key: %v\\n\", err)\n\t\t\treturn\n\t\t}\n\t}\n\tdisabledDuration := time.Since(start)\n\tfmt.Printf(\"✓ 1000 SET operations (disabled): %v\\n\", disabledDuration)\n\tfmt.Printf(\"Pool stats (disabled): %+v\\n\", rdb1.PoolStats())\n\n\t// performance comparison note\n\tfmt.Printf(\"\\nNote: The pool stats and performance are identical because there is no background processing overhead.\\n\")\n\tfmt.Println(\"Since the server doesn't support maintenance notifications, there is no difference in behavior.\")\n\tfmt.Printf(\"The only difference is that the \\\"ModeDisabled\\\" client doesn't send the CLIENT MAINT_NOTIFICATIONS ON command.\\n\\n\")\n\tfmt.Println(\"p.s. reordering the execution here makes it look like there is a small performance difference, but it's just noise.\")\n\n\t// Cleanup\n\tfmt.Printf(\"\\n=== Cleanup ===\\n\")\n\tif err := rdb1.FlushDB(ctx).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to flush DB: %v\\n\", err)\n\t\treturn\n\t}\n\tfmt.Println(\"✓ Database flushed\")\n\n\tfmt.Printf(\"\\n=== Summary ===\\n\")\n\tfmt.Println(\"Maintenance notifications can be disabled by setting:\")\n\tfmt.Println(\"  MaintNotifications: &maintnotifications.Config{\")\n\tfmt.Println(\"    Mode: maintnotifications.ModeDisabled,\")\n\tfmt.Println(\"  }\")\n\tfmt.Printf(\"\\nThis is useful when:\\n\")\n\tfmt.Println(\"  - Connecting to non-Redis Cloud instances\")\n\tfmt.Println(\"  - You want to handle failovers manually\")\n\tfmt.Println(\"  - You want to minimize client-side overhead\")\n\tfmt.Println(\"  - The Redis server doesn't support CLIENT MAINT_NOTIFICATIONS\")\n\tfmt.Printf(\"\\nFor more information, see:\\n\")\n\tfmt.Println(\"  https://github.com/redis/go-redis/tree/master/maintnotifications\")\n}\n"
  },
  {
    "path": "example/hll/README.md",
    "content": "# Redis HyperLogLog example\n\nTo run this example:\n\n```shell\ngo run .\n```\n\nSee [Using HyperLogLog command with go-redis](https://redis.uptrace.dev/guide/go-redis-hll.html) for\ndetails.\n"
  },
  {
    "path": "example/hll/go.mod",
    "content": "module github.com/redis/go-redis/example/hll\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.18.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/hll/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/hll/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\t_ = rdb.FlushDB(ctx).Err()\n\n\tfor i := 0; i < 10; i++ {\n\t\tif err := rdb.PFAdd(ctx, \"myset\", fmt.Sprint(i)).Err(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tcard, err := rdb.PFCount(ctx, \"myset\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"set cardinality\", card)\n}\n"
  },
  {
    "path": "example/hset-struct/README.md",
    "content": "# Example for setting struct fields as hash fields\n\nTo run this example:\n\n```shell\ngo run .\n```\n"
  },
  {
    "path": "example/hset-struct/go.mod",
    "content": "module github.com/redis/go-redis/example/scan-struct\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1\n\tgithub.com/redis/go-redis/v9 v9.18.0\n)\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/hset-struct/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/hset-struct/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype Model struct {\n\tStr1    string     `redis:\"str1\"`\n\tStr2    string     `redis:\"str2\"`\n\tStr3    *string    `redis:\"str3\"`\n\tStr4    *string    `redis:\"str4\"`\n\tBytes   []byte     `redis:\"bytes\"`\n\tInt     int        `redis:\"int\"`\n\tInt2    *int       `redis:\"int2\"`\n\tInt3    *int       `redis:\"int3\"`\n\tBool    bool       `redis:\"bool\"`\n\tBool2   *bool      `redis:\"bool2\"`\n\tBool3   *bool      `redis:\"bool3\"`\n\tBool4   *bool      `redis:\"bool4,omitempty\"`\n\tTime    time.Time  `redis:\"time\"`\n\tTime2   *time.Time `redis:\"time2\"`\n\tTime3   *time.Time `redis:\"time3\"`\n\tIgnored struct{}   `redis:\"-\"`\n}\n\nfunc main() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\n\t_ = rdb.FlushDB(ctx).Err()\n\n\tt := time.Date(2025, 02, 8, 0, 0, 0, 0, time.UTC)\n\n\tdata := Model{\n\t\tStr1:    \"hello\",\n\t\tStr2:    \"world\",\n\t\tStr3:    ToPtr(\"hello\"),\n\t\tStr4:    nil,\n\t\tBytes:   []byte(\"this is bytes !\"),\n\t\tInt:     123,\n\t\tInt2:    ToPtr(0),\n\t\tInt3:    nil,\n\t\tBool:    true,\n\t\tBool2:   ToPtr(false),\n\t\tBool3:   nil,\n\t\tTime:    t,\n\t\tTime2:   ToPtr(t),\n\t\tTime3:   nil,\n\t\tIgnored: struct{}{},\n\t}\n\n\t// Set some fields.\n\tif _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {\n\t\trdb.HMSet(ctx, \"key\", data)\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar model1, model2 Model\n\n\t// Scan all fields into the model.\n\tif err := rdb.HGetAll(ctx, \"key\").Scan(&model1); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Or scan a subset of the fields.\n\tif err := rdb.HMGet(ctx, \"key\", \"str1\", \"int\").Scan(&model2); err != nil {\n\t\tpanic(err)\n\t}\n\n\tspew.Dump(model1)\n\t// Output:\n\t// (main.Model) {\n\t//  Str1: (string) (len=5) \"hello\",\n\t//  Str2: (string) (len=5) \"world\",\n\t//  Str3: (*string)(0xc000016970)((len=5) \"hello\"),\n\t//  Str4: (*string)(0xc000016980)(\"\"),\n\t//  Bytes: ([]uint8) (len=15 cap=16) {\n\t//   00000000  74 68 69 73 20 69 73 20  62 79 74 65 73 20 21     |this is bytes !|\n\t//  },\n\t//  Int: (int) 123,\n\t//  Int2: (*int)(0xc000014568)(0),\n\t//  Int3: (*int)(0xc000014560)(0),\n\t//  Bool: (bool) true,\n\t//  Bool2: (*bool)(0xc000014570)(false),\n\t//  Bool3: (*bool)(0xc000014548)(false),\n\t//  Bool4: (*bool)(<nil>),\n\t//  Time: (time.Time) 2025-02-08 00:00:00 +0000 UTC,\n\t//  Time2: (*time.Time)(0xc0000122a0)(2025-02-08 00:00:00 +0000 UTC),\n\t//  Time3: (*time.Time)(0xc000012288)(0001-01-01 00:00:00 +0000 UTC),\n\t//  Ignored: (struct {}) {\n\t//  }\n\t// }\n\n\tspew.Dump(model2)\n\t// Output:\n\t// (main.Model) {\n\t//  Str1: (string) (len=5) \"hello\",\n\t//  Str2: (string) \"\",\n\t//  Str3: (*string)(<nil>),\n\t//  Str4: (*string)(<nil>),\n\t//  Bytes: ([]uint8) <nil>,\n\t//  Int: (int) 123,\n\t//  Int2: (*int)(<nil>),\n\t//  Int3: (*int)(<nil>),\n\t//  Bool: (bool) false,\n\t//  Bool2: (*bool)(<nil>),\n\t//  Bool3: (*bool)(<nil>),\n\t//  Bool4: (*bool)(<nil>),\n\t//  Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC,\n\t//  Time2: (*time.Time)(<nil>),\n\t//  Time3: (*time.Time)(<nil>),\n\t//  Ignored: (struct {}) {\n\t//  }\n\t// }\n}\n\nfunc ToPtr[T any](v T) *T {\n\treturn &v\n}\n"
  },
  {
    "path": "example/lua-scripting/README.md",
    "content": "# Redis Lua scripting example\n\nThis is an example for [Redis Lua scripting](https://redis.uptrace.dev/guide/lua-scripting.html)\narticle. To run it:\n\n```shell\ngo run .\n```\n"
  },
  {
    "path": "example/lua-scripting/go.mod",
    "content": "module github.com/redis/go-redis/example/lua-scripting\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.18.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/lua-scripting/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/lua-scripting/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\t_ = rdb.FlushDB(ctx).Err()\n\n\tfmt.Printf(\"# INCR BY\\n\")\n\tfor _, change := range []int{+1, +5, 0} {\n\t\tnum, err := incrBy.Run(ctx, rdb, []string{\"my_counter\"}, change).Int()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfmt.Printf(\"incr by %d: %d\\n\", change, num)\n\t}\n\n\tfmt.Printf(\"\\n# SUM\\n\")\n\tsum, err := sum.Run(ctx, rdb, []string{\"my_sum\"}, 1, 2, 3).Int()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"sum is: %d\\n\", sum)\n}\n\nvar incrBy = redis.NewScript(`\nlocal key = KEYS[1]\nlocal change = ARGV[1]\n\nlocal value = redis.call(\"GET\", key)\nif not value then\n  value = 0\nend\n\nvalue = value + change\nredis.call(\"SET\", key, value)\n\nreturn value\n`)\n\nvar sum = redis.NewScript(`\nlocal key = KEYS[1]\n\nlocal sum = redis.call(\"GET\", key)\nif not sum then\n  sum = 0\nend\n\nlocal num_arg = #ARGV\nfor i = 1, num_arg do\n  sum = sum + ARGV[i]\nend\n\nredis.call(\"SET\", key, sum)\n\nreturn sum\n`)\n"
  },
  {
    "path": "example/maintnotifiations-pubsub/go.mod",
    "content": "module github.com/redis/go-redis/example/pubsub\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.11.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/maintnotifiations-pubsub/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/maintnotifiations-pubsub/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\nvar ctx = context.Background()\nvar cntErrors atomic.Int64\nvar cntSuccess atomic.Int64\nvar startTime = time.Now()\n\n// This example is not supposed to be run as is. It is just a test to see how pubsub behaves in relation to pool management.\n// It was used to find regressions in pool management in maintnotifications mode.\n// Please don't use it as a reference for how to use pubsub.\nfunc main() {\n\tstartTime = time.Now()\n\twg := &sync.WaitGroup{}\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\tEndpointType:               maintnotifications.EndpointTypeExternalIP,\n\t\t\tHandoffTimeout:             10 * time.Second,\n\t\t\tRelaxedTimeout:             10 * time.Second,\n\t\t\tPostHandoffRelaxedDuration: 10 * time.Second,\n\t\t},\n\t})\n\t_ = rdb.FlushDB(ctx).Err()\n\tmaintnotificationsManager := rdb.GetMaintNotificationsManager()\n\tif maintnotificationsManager == nil {\n\t\tpanic(\"maintnotifications manager is nil\")\n\t}\n\tloggingHook := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tmaintnotificationsManager.AddNotificationHook(loggingHook)\n\n\tgo func() {\n\t\tfor {\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t\tfmt.Printf(\"pool stats: %+v\\n\", rdb.PoolStats())\n\t\t}\n\t}()\n\terr := rdb.Ping(ctx).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := rdb.Set(ctx, \"publishers\", \"0\", 0).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := rdb.Set(ctx, \"subscribers\", \"0\", 0).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := rdb.Set(ctx, \"published\", \"0\", 0).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := rdb.Set(ctx, \"received\", \"0\", 0).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"published\", rdb.Get(ctx, \"published\").Val())\n\tfmt.Println(\"received\", rdb.Get(ctx, \"received\").Val())\n\tsubCtx, cancelSubCtx := context.WithCancel(ctx)\n\tpubCtx, cancelPublishers := context.WithCancel(ctx)\n\tfor i := 0; i < 10; i++ {\n\t\twg.Add(1)\n\t\tgo subscribe(subCtx, rdb, \"test\", i, wg)\n\t}\n\ttime.Sleep(time.Second)\n\tcancelSubCtx()\n\ttime.Sleep(time.Second)\n\tsubCtx, cancelSubCtx = context.WithCancel(ctx)\n\tfor i := 0; i < 10; i++ {\n\t\tif err := rdb.Incr(ctx, \"publishers\").Err(); err != nil {\n\t\t\tfmt.Println(\"incr error:\", err)\n\t\t\tcntErrors.Add(1)\n\t\t}\n\t\twg.Add(1)\n\t\tgo floodThePool(pubCtx, rdb, wg)\n\t}\n\n\tfor i := 0; i < 500; i++ {\n\t\tif err := rdb.Incr(ctx, \"subscribers\").Err(); err != nil {\n\t\t\tfmt.Println(\"incr error:\", err)\n\t\t\tcntErrors.Add(1)\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo subscribe(subCtx, rdb, \"test2\", i, wg)\n\t}\n\ttime.Sleep(120 * time.Second)\n\tfmt.Println(\"canceling publishers\")\n\tcancelPublishers()\n\ttime.Sleep(10 * time.Second)\n\tfmt.Println(\"canceling subscribers\")\n\tcancelSubCtx()\n\twg.Wait()\n\tpublished, err := rdb.Get(ctx, \"published\").Result()\n\treceived, err := rdb.Get(ctx, \"received\").Result()\n\tpublishers, err := rdb.Get(ctx, \"publishers\").Result()\n\tsubscribers, err := rdb.Get(ctx, \"subscribers\").Result()\n\tfmt.Printf(\"publishers: %s\\n\", publishers)\n\tfmt.Printf(\"published: %s\\n\", published)\n\tfmt.Printf(\"subscribers: %s\\n\", subscribers)\n\tfmt.Printf(\"received: %s\\n\", received)\n\tpublishedInt, err := rdb.Get(ctx, \"published\").Int()\n\tsubscribersInt, err := rdb.Get(ctx, \"subscribers\").Int()\n\tfmt.Printf(\"if drained = published*subscribers: %d\\n\", publishedInt*subscribersInt)\n\n\ttime.Sleep(2 * time.Second)\n\tfmt.Println(\"errors:\", cntErrors.Load())\n\tfmt.Println(\"success:\", cntSuccess.Load())\n\tfmt.Println(\"time:\", time.Since(startTime))\n}\n\nfunc floodThePool(ctx context.Context, rdb *redis.Client, wg *sync.WaitGroup) {\n\tdefer wg.Done()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tdefault:\n\t\t}\n\t\terr := rdb.Publish(ctx, \"test2\", \"hello\").Err()\n\t\tif err != nil {\n\t\t\tif err.Error() != \"context canceled\" {\n\t\t\t\tlog.Println(\"publish error:\", err)\n\t\t\t\tcntErrors.Add(1)\n\t\t\t}\n\t\t}\n\n\t\terr = rdb.Incr(ctx, \"published\").Err()\n\t\tif err != nil {\n\t\t\tif err.Error() != \"context canceled\" {\n\t\t\t\tlog.Println(\"incr error:\", err)\n\t\t\t\tcntErrors.Add(1)\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(10 * time.Nanosecond)\n\t}\n}\n\nfunc subscribe(ctx context.Context, rdb *redis.Client, topic string, subscriberId int, wg *sync.WaitGroup) {\n\tdefer wg.Done()\n\trec := rdb.Subscribe(ctx, topic)\n\trecChan := rec.Channel()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\trec.Close()\n\t\t\treturn\n\t\tdefault:\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\trec.Close()\n\t\t\t\treturn\n\t\t\tcase msg := <-recChan:\n\t\t\t\terr := rdb.Incr(ctx, \"received\").Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\tif err.Error() != \"context canceled\" {\n\t\t\t\t\t\tlog.Printf(\"%s\\n\", err.Error())\n\t\t\t\t\t\tcntErrors.Add(1)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_ = msg // Use the message to avoid unused variable warning\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "example/otel/README.md",
    "content": "# go-redis OpenTelemetry Monitoring with Uptrace\n\nThis example demonstrates how to instrument and monitor Redis operations in Go applications using\nOpenTelemetry and [Uptrace](https://github.com/uptrace/uptrace), providing comprehensive\nobservability into your Redis performance and operations.\n\n## Overview\n\nThis integration provides:\n\n- **Distributed tracing** for Redis operations\n- **Performance monitoring** with latency and throughput metrics\n- **Error tracking** and debugging capabilities\n- **Visual dashboards** for Redis health monitoring\n- **Production-ready** observability stack with Docker\n\n## Prerequisites\n\n- Go 1.19+\n- Docker and Docker Compose\n- Basic understanding of Redis and OpenTelemetry\n\n## Quick Start\n\n### 1. Clone and Navigate\n\n```bash\ngit clone https://github.com/redis/go-redis.git\ncd example/otel\n```\n\n### 2. Start the Monitoring Stack\n\nLaunch Redis and Uptrace services:\n\n```bash\ndocker compose up -d\n```\n\nThis starts:\n\n- Redis server on `localhost:6379`\n- Uptrace APM on `http://localhost:14318`\n\n### 3. Verify Services\n\nCheck that Uptrace is running properly:\n\n```bash\ndocker compose logs uptrace\n```\n\nLook for successful startup messages without errors.\n\n### 4. Run the Example\n\nExecute the instrumented Redis client:\n\n```bash\ngo run client.go\n```\n\nYou should see output similar to:\n\n```\ntrace: http://localhost:14318/traces/ee029d8782242c8ed38b16d961093b35\n```\n\nClick the trace URL to view detailed operation traces in Uptrace.\n\n![Redis trace visualization](./image/redis-trace.png)\n\n### 5. Explore the Dashboard\n\nOpen the Uptrace UI at [http://localhost:14318](http://localhost:14318/metrics/1) to explore:\n\n- **Traces**: Individual Redis operation details\n- **Metrics**: Performance statistics and trends\n- **Logs**: Application and system logs\n- **Service Map**: Visual representation of dependencies\n\n## Advanced Monitoring Setup\n\n### Redis Performance Metrics\n\nFor production environments, enable comprehensive Redis monitoring by installing the OpenTelemetry\nCollector:\n\nThe [OpenTelemetry Collector](https://uptrace.dev/opentelemetry/collector) acts as a telemetry agent\nthat:\n\n- Pulls performance metrics directly from Redis\n- Collects system-level statistics\n- Forwards data to Uptrace via OTLP protocol\n\nWhen configured, Uptrace automatically generates a Redis dashboard:\n\n![Redis performance dashboard](./image/metrics.png)\n\n### Key Metrics Monitored\n\n- **Connection Statistics**: Active connections, connection pool utilization\n- **Command Performance**: Operation latency, throughput, error rates\n- **Memory Usage**: Memory consumption, key distribution\n- **Replication Health**: Master-slave sync status and lag\n\n### Logs and Debugging\n\nView service logs:\n\n```bash\n# All services\ndocker compose logs\n\n# Specific service\ndocker compose logs redis\ndocker compose logs uptrace\n```\n\n## Additional Resources\n\n- [Complete go-redis Monitoring Guide](https://redis.uptrace.dev/guide/go-redis-monitoring.html)\n- [OpenTelemetry Go Instrumentation](https://uptrace.dev/get/opentelemetry-go/tracing)\n- [Uptrace Open Source APM](https://uptrace.dev/get/hosted/open-source-apm)\n"
  },
  {
    "path": "example/otel/client.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/codes\"\n\n\t\"github.com/uptrace/uptrace-go/uptrace\"\n\n\t\"github.com/redis/go-redis/extra/redisotel/v9\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar tracer = otel.Tracer(\"github.com/redis/go-redis/example/otel\")\n\nfunc main() {\n\tctx := context.Background()\n\n\tuptrace.ConfigureOpentelemetry(\n\t\t// copy your project DSN here or use UPTRACE_DSN env var\n\t\tuptrace.WithDSN(\"http://project1_secret@localhost:14318/2?grpc=14317\"),\n\n\t\tuptrace.WithServiceName(\"myservice\"),\n\t\tuptrace.WithServiceVersion(\"v1.0.0\"),\n\t)\n\tdefer uptrace.Shutdown(ctx)\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\tif err := redisotel.InstrumentTracing(rdb); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := redisotel.InstrumentMetrics(rdb); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor i := 0; i < 1e6; i++ {\n\t\tctx, rootSpan := tracer.Start(ctx, \"handleRequest\")\n\n\t\tif err := handleRequest(ctx, rdb); err != nil {\n\t\t\trootSpan.RecordError(err)\n\t\t\trootSpan.SetStatus(codes.Error, err.Error())\n\t\t}\n\n\t\trootSpan.End()\n\n\t\tif i == 0 {\n\t\t\tfmt.Printf(\"view trace: %s\\n\", uptrace.TraceURL(rootSpan))\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc handleRequest(ctx context.Context, rdb *redis.Client) error {\n\tif err := rdb.Set(ctx, \"First value\", \"value_1\", 0).Err(); err != nil {\n\t\treturn err\n\t}\n\tif err := rdb.Set(ctx, \"Second value\", \"value_2\", 0).Err(); err != nil {\n\t\treturn err\n\t}\n\n\tvar group sync.WaitGroup\n\n\tfor i := 0; i < 20; i++ {\n\t\tgroup.Add(1)\n\t\tgo func() {\n\t\t\tdefer group.Done()\n\t\t\tval := rdb.Get(ctx, \"Second value\").Val()\n\t\t\tif val != \"value_2\" {\n\t\t\t\tlog.Printf(\"%q != %q\", val, \"value_2\")\n\t\t\t}\n\t\t}()\n\t}\n\n\tgroup.Wait()\n\n\tif err := rdb.Del(ctx, \"First value\").Err(); err != nil {\n\t\treturn err\n\t}\n\tif err := rdb.Del(ctx, \"Second value\").Err(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "example/otel/config/otel-collector.yaml",
    "content": "extensions:\n  health_check:\n  pprof:\n    endpoint: 0.0.0.0:1777\n  zpages:\n    endpoint: 0.0.0.0:55679\n\nreceivers:\n  otlp:\n    protocols:\n      grpc:\n      http:\n  hostmetrics:\n    collection_interval: 10s\n    scrapers:\n      cpu:\n      disk:\n      load:\n      filesystem:\n      memory:\n      network:\n      paging:\n  redis:\n    endpoint: 'redis-server:6379'\n    collection_interval: 10s\n  postgresql:\n    endpoint: postgres:5432\n    transport: tcp\n    username: uptrace\n    password: uptrace\n    databases:\n      - uptrace\n    tls:\n      insecure: true\n\nprocessors:\n  resourcedetection:\n    detectors: ['system']\n  cumulativetodelta:\n  batch:\n    send_batch_size: 10000\n    timeout: 10s\n\nexporters:\n  otlp/uptrace:\n    endpoint: http://uptrace:4317\n    tls:\n      insecure: true\n    headers: { 'uptrace-dsn': 'http://project1_secret@localhost:14318/2?grpc=14317' }\n  debug:\n\nservice:\n  # telemetry:\n  #   logs:\n  #     level: DEBUG\n  pipelines:\n    traces:\n      receivers: [otlp]\n      processors: [batch]\n      exporters: [otlp/uptrace]\n    metrics:\n      receivers: [otlp]\n      processors: [cumulativetodelta, batch]\n      exporters: [otlp/uptrace]\n    metrics/hostmetrics:\n      receivers: [hostmetrics, redis, postgresql]\n      processors: [cumulativetodelta, batch, resourcedetection]\n      exporters: [otlp/uptrace]\n    logs:\n      receivers: [otlp]\n      processors: [batch]\n      exporters: [otlp/uptrace]\n\n  extensions: [health_check, pprof, zpages]\n"
  },
  {
    "path": "example/otel/config/vector.toml",
    "content": "[sources.syslog_logs]\ntype = \"demo_logs\"\nformat = \"syslog\"\n\n[sources.apache_common_logs]\ntype = \"demo_logs\"\nformat = \"apache_common\"\n\n[sources.apache_error_logs]\ntype = \"demo_logs\"\nformat = \"apache_error\"\n\n[sources.json_logs]\ntype = \"demo_logs\"\nformat = \"json\"\n\n# Parse Syslog logs\n# See the Vector Remap Language reference for more info: https://vrl.dev\n[transforms.parse_logs]\ntype = \"remap\"\ninputs = [\"syslog_logs\"]\nsource = '''\n. = parse_syslog!(string!(.message))\n'''\n\n# Export data to Uptrace.\n[sinks.uptrace]\ntype = \"http\"\ninputs = [\"parse_logs\", \"apache_common_logs\", \"apache_error_logs\", \"json_logs\"]\nencoding.codec = \"json\"\nframing.method = \"newline_delimited\"\ncompression = \"gzip\"\nuri = \"http://uptrace:14318/api/v1/vector/logs\"\n#uri = \"https://api.uptrace.dev/api/v1/vector/logs\"\nheaders.uptrace-dsn = \"http://project2_secret_token@localhost:14317/2\"\n"
  },
  {
    "path": "example/otel/docker-compose.yml",
    "content": "services:\n  clickhouse:\n    image: clickhouse/clickhouse-server:25.3.5\n    restart: on-failure\n    environment:\n      CLICKHOUSE_USER: uptrace\n      CLICKHOUSE_PASSWORD: uptrace\n      CLICKHOUSE_DB: uptrace\n    healthcheck:\n      test: ['CMD', 'wget', '--spider', '-q', 'localhost:8123/ping']\n      interval: 1s\n      timeout: 1s\n      retries: 30\n    volumes:\n      - ch_data:/var/lib/clickhouse\n    ports:\n      - '8123:8123'\n      - '9000:9000'\n\n  postgres:\n    image: postgres:17-alpine\n    restart: on-failure\n    environment:\n      PGDATA: /var/lib/postgresql/data/pgdata\n      POSTGRES_USER: uptrace\n      POSTGRES_PASSWORD: uptrace\n      POSTGRES_DB: uptrace\n    healthcheck:\n      test: ['CMD-SHELL', 'pg_isready', '-U', 'uptrace', '-d', 'uptrace']\n      interval: 1s\n      timeout: 1s\n      retries: 30\n    volumes:\n      - 'pg_data:/var/lib/postgresql/data/pgdata'\n    ports:\n      - '5432:5432'\n\n  uptrace:\n    image: 'uptrace/uptrace:2.0.0'\n    #image: 'uptrace/uptrace-dev:latest'\n    restart: on-failure\n    volumes:\n      - ./uptrace.yml:/etc/uptrace/config.yml\n    #environment:\n    #  - DEBUG=2\n    ports:\n      - '14317:4317'\n      - '14318:80'\n    depends_on:\n      clickhouse:\n        condition: service_healthy\n\n  otelcol:\n    image: otel/opentelemetry-collector-contrib:0.123.0\n    restart: on-failure\n    volumes:\n      - ./config/otel-collector.yaml:/etc/otelcol-contrib/config.yaml\n    ports:\n      - '4317:4317'\n      - '4318:4318'\n\n  vector:\n    image: timberio/vector:0.28.X-alpine\n    volumes:\n      - ./config/vector.toml:/etc/vector/vector.toml:ro\n\n  mailpit:\n    image: axllent/mailpit\n    restart: always\n    ports:\n      - 1025:1025\n      - 8025:8025\n    environment:\n      MP_MAX_MESSAGES: 5000\n      MP_DATA_FILE: /data/mailpit.db\n      MP_SMTP_AUTH_ACCEPT_ANY: 1\n      MP_SMTP_AUTH_ALLOW_INSECURE: 1\n    volumes:\n      - mailpit_data:/data\n\n  redis-server:\n    image: redis\n    ports:\n      - '6379:6379'\n  redis-cli:\n    image: redis\n\nvolumes:\n  ch_data:\n  pg_data:\n  mailpit_data:\n"
  },
  {
    "path": "example/otel/go.mod",
    "content": "module github.com/redis/go-redis/example/otel\n\ngo 1.24\n\ntoolchain go1.24.1\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nreplace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel\n\nreplace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd\n\nrequire (\n\tgithub.com/redis/go-redis/extra/redisotel/v9 v9.18.0\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgithub.com/uptrace/uptrace-go v1.21.0\n\tgo.opentelemetry.io/otel v1.22.0\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v4 v4.2.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/go-logr/logr v1.4.1 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect\n\tgithub.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.22.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.22.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.22.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.0.0 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/net v0.36.0 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n\tgolang.org/x/text v0.22.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect\n\tgoogle.golang.org/grpc v1.60.1 // indirect\n\tgoogle.golang.org/protobuf v1.33.0 // indirect\n)\n"
  },
  {
    "path": "example/otel/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=\ngithub.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=\ngithub.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/uptrace/uptrace-go v1.21.0 h1:oJoUjhiVT7aiuoG6B3ClVHtJozLn3cK9hQt8U5dQO1M=\ngithub.com/uptrace/uptrace-go v1.21.0/go.mod h1:/aXAFGKOqeAFBqWa1xtzLnGX2xJm1GScqz9NJ0TJjLM=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 h1:m9ReioVPIffxjJlGNRd0d5poy+9oTro3D+YbiEzUDOc=\ngo.opentelemetry.io/contrib/instrumentation/runtime v0.46.1/go.mod h1:CANkrsXNzqOKXfOomu2zhOmc1/J5UZK9SGjrat6ZCG0=\ngo.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=\ngo.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=\ngo.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=\ngo.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=\ngo.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=\ngo.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=\ngo.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=\ngo.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=\ngo.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=\ngo.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=\ngo.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=\ngo.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=\ngolang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=\ngolang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs=\ngoogle.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=\ngoogle.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=\ngoogle.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "example/otel/uptrace.yml",
    "content": "# =============================================================================\n# Uptrace Configuration\n# =============================================================================\n# Complete configuration reference: https://uptrace.dev/get/hosted/config\n#\n# Environment Variable Support:\n#   - Simple substitution: foo: $FOO\n#   - With braces: bar: ${BAR}\n#   - With defaults: baz: ${BAZ:default_value}\n#   - To escape '$': foo: $$FOO_BAR\n# =============================================================================\n\n# -----------------------------------------------------------------------------\n# Service Configuration\n# -----------------------------------------------------------------------------\n# Core service settings that define the runtime environment and security\nservice:\n  env: hosted           # Environment: hosted, development, production\n  secret: FIXME         # Secret key for cryptographic operations (CHANGE THIS!)\n\n# -----------------------------------------------------------------------------\n# Site Configuration\n# -----------------------------------------------------------------------------\n# External-facing URLs and endpoints for client connections\nsite:\n  # Primary URL where users access the Uptrace UI and API\n  # This URL is used for:\n  # - Generating dashboard links in notifications\n  # - CORS validation for browser requests\n  # - Redirect URIs for authentication flows\n  # IMPORTANT: Must be accessible from all clients and match your reverse proxy setup\n  url: http://localhost:14318\n\n  # Dedicated URL for telemetry data ingestion (OTLP endpoints)\n  # Use this to separate data ingestion from UI traffic for:\n  # - Load balancing optimization\n  # - Different security policies\n  # - CDN/edge deployment scenarios\n  # If empty, defaults to site.url\n  ingest_url: http://localhost:14318?grpc=14317\n\n# -----------------------------------------------------------------------------\n# Network Listeners\n# -----------------------------------------------------------------------------\n# Configure network interfaces and ports for different protocols\nlisten:\n  # HTTP server configuration\n  # Handles: OTLP/HTTP API, REST API, and Vue.js web interface\n  http:\n    # Network address and port to bind to\n    # Format: [host]:port or :port (binds to all interfaces)\n    # Common values: :80, :8080, localhost:8080\n    addr: :80\n\n  # gRPC server configuration\n  # Handles: OTLP/gRPC API for high-performance telemetry ingestion\n  # Standard OTLP gRPC port is 4317\n  grpc:\n    addr: :4317\n\n  # TLS/SSL configuration for HTTPS and secure gRPC\n  # Uncomment and configure for production deployments\n  #tls:\n  #  cert_file: /etc/uptrace/server.crt\n  #  key_file: /etc/uptrace/server.key\n\n# -----------------------------------------------------------------------------\n# Authentication & Authorization\n# -----------------------------------------------------------------------------\n# User authentication and access control settings\nauth:\n  # Disable built-in username/password authentication\n  # Useful when using only SSO providers (OAuth, SAML, etc.)\n  # Note: SSO authentication methods will remain available\n  #disabled: true\n\n  # Email domain restriction for user registration\n  # Only users with email addresses matching this regex can register/login\n  # Examples:\n  # - '^.+@example\\.com$' - only @example.com domain\n  # - '^.+@(example|acme)\\.com$' - multiple domains\n  # - '^[^@]+@example\\.com$' - stricter validation\n  #email_regexp: '^.+@example\\.com$'\n\n# -----------------------------------------------------------------------------\n# Bootstrap Data\n# -----------------------------------------------------------------------------\n# Initial data created during first startup - defines default users, organizations, and projects\n# This data is only created once and can be modified through the UI afterward\nseed_data:\n  # Default users created on first startup\n  users:\n    - key: user1                 # Internal reference key (used in relationships below)\n      name: Admin                # Display name in UI\n      email: admin@uptrace.local # Login email (must be unique)\n      password: admin            # Plain text password (CHANGE THIS IMMEDIATELY!)\n      email_confirmed: true\n\n  # API tokens for user authentication\n  # These tokens can be used for API access and programmatic operations\n  user_tokens:\n    - key: user_token1           # Internal reference key\n      user_key: user1            # References user.key above\n      token: user1_secret        # API token value (CHANGE THIS!)\n\n  # Organizations for multi-tenant deployments\n  # Organizations group users and projects together\n  orgs:\n    - key: org1                  # Internal reference key\n      name: Org1                 # Organization display name\n\n  # Organization membership and roles\n  # Defines which users belong to which organizations\n  org_users:\n    - key: org_user1             # Internal reference key\n      org_key: org1              # References org.key above\n      user_key: user1            # References user.key above\n      role: owner                # Role: owner, admin, or member\n\n  # Projects contain telemetry data and are isolated from each other\n  # Each project has its own spans, logs, metrics, and dashboards\n  projects:\n    - key: project1              # Internal reference key\n      name: Project1             # Project display name\n      org_key: org1              # References org.key above\n\n  # Project-specific tokens for telemetry data ingestion\n  # These tokens are used in OTLP DSN strings for sending data\n  project_tokens:\n    - key: project_token1        # Internal reference key\n      project_key: project1      # References project.key above\n      token: project1_secret     # Token value for DSN (CHANGE THIS!)\n\n  # Project user permissions\n  # Controls who can access and modify project data\n  project_users:\n    - key: project_user1         # Internal reference key\n      project_key: project1      # References project.key above\n      org_user_key: org_user1    # References org_user.key above\n      perm_level: admin          # Permission level: admin, editor, or viewer\n\n# -----------------------------------------------------------------------------\n# ClickHouse Database Configuration\n# -----------------------------------------------------------------------------\n# Primary storage for high-volume telemetry data (spans, logs, metrics)\n# ClickHouse is optimized for analytical queries and time-series data\nch_cluster:\n  # Cluster name for ClickHouse operations\n  # Used internally for distributed queries and table management\n  cluster: uptrace1\n\n  # Enable ClickHouse replicated tables for high availability\n  # Requires cluster configuration with multiple replicas\n  # Provides automatic failover and data redundancy\n  replicated: false\n\n  # Enable ClickHouse distributed tables for horizontal scaling\n  # Requires cluster configuration across multiple shards\n  # Only available in Premium Edition\n  distributed: false\n\n  # Database shards configuration\n  # Each shard can have multiple replicas for redundancy\n  shards:\n    - replicas:\n        - addr: clickhouse:9000         # ClickHouse server address\n          database: uptrace             # Database name (must exist)\n          user: uptrace                 # Database user with write permissions\n          password: uptrace             # Database password\n\n          # Connection timeout settings\n          dial_timeout: 3s              # Time to wait for connection establishment\n          write_timeout: 5s             # Time to wait for write operations\n          max_retries: 3                # Number of retry attempts for failed operations\n\n          # Query execution timeout\n          # Prevents long-running queries from consuming resources\n          max_execution_time: 15s\n\n          # TLS configuration for secure database connections\n          # Uncomment for production deployments with SSL/TLS\n          #tls:\n          #  insecure_skip_verify: true  # WARNING: Only use for self-signed certificates\n\n# -----------------------------------------------------------------------------\n# PostgreSQL Database Configuration\n# -----------------------------------------------------------------------------\n# Metadata storage for application data (users, projects, dashboards, alerts, etc.)\n# PostgreSQL provides ACID compliance for critical application state\npg:\n  addr: postgres:5432           # PostgreSQL server address\n  user: uptrace                 # Database user with full permissions\n  password: uptrace             # Database password\n  database: uptrace             # Database name (must exist)\n\n  # TLS configuration for secure database connections\n  # Recommended for production deployments\n  #tls:\n  #  insecure_skip_verify: true  # WARNING: Only use for self-signed certificates\n\n# -----------------------------------------------------------------------------\n# ClickHouse Schema Configuration\n# -----------------------------------------------------------------------------\n# Advanced schema settings for performance optimization\n# WARNING: Changes require 'ch reset' command and will delete all data\nch_schema:\n  # Data compression algorithm for storage efficiency\n  # Options:\n  # - LZ4: Fast compression/decompression, moderate compression ratio\n  # - ZSTD(1): Better compression ratio, slightly slower\n  # - Default: ClickHouse default compression\n  compression: ZSTD(1)\n\n  # Storage policies for different data types\n  # Allows using different storage tiers (SSD, HDD, S3) for different data\n  spans_index: { storage_policy: default }   # Span search indexes\n  spans_data: { storage_policy: default }    # Raw span data\n  span_links: { storage_policy: default }    # Span relationship data\n  logs_index: { storage_policy: default }    # Log search indexes\n  logs_data: { storage_policy: default }     # Raw log data\n  events_index: { storage_policy: default }  # Event search indexes\n  events_data: { storage_policy: default }   # Raw event data\n  metrics: { storage_policy: default }       # Metrics time-series data\n\n# -----------------------------------------------------------------------------\n# Redis Cache Configuration\n# -----------------------------------------------------------------------------\n# In-memory cache for improved query performance and session storage\n# Reduces database load and improves response times\nredis_cache:\n  # Redis server addresses\n  # For cluster setup, add multiple addresses\n  addrs:\n    1: redis-server:6379\n\n  # Redis authentication credentials\n  username: \"\"                  # Redis username (Redis 6.0+)\n  password: \"\"                  # Redis password\n  db: 0                         # Redis database number (0-15)\n\n  # TLS configuration for secure Redis connections\n  # Recommended for production deployments\n  #tls:\n  #  insecure_skip_verify: true  # WARNING: Only use for self-signed certificates\n\n# -----------------------------------------------------------------------------\n# SSL/TLS Certificate Management\n# -----------------------------------------------------------------------------\n# Automatic certificate issuance and renewal via Let's Encrypt ACME protocol\ncertmagic:\n  enabled: false                # Enable automatic certificate management\n  staging_ca: false             # Use Let's Encrypt staging environment for testing\n  http_challenge_addr: :80      # Address for HTTP-01 challenge validation\n\n# -----------------------------------------------------------------------------\n# Email Configuration\n# -----------------------------------------------------------------------------\n# SMTP configuration for alert notifications and user management emails\n# Required for: password resets, alert notifications, user invitations\n# Documentation: https://uptrace.dev/features/alerting\nmailer:\n  smtp:\n    enabled: false                # Enable email notifications\n    host: localhost               # SMTP server hostname\n    port: 1025                    # SMTP server port (25, 465, 587, 1025)\n    username: mailhog             # SMTP authentication username\n    password: mailhog             # SMTP authentication password\n    from: no-reply@uptrace.local  # Sender email address (must be authorized)\n\n    # TLS configuration\n    # Most production SMTP servers require TLS\n    #tls: { insecure: true }     # Uncomment to disable opportunistic TLS\n\n# -----------------------------------------------------------------------------\n# Telemetry Data Processing Configuration\n# -----------------------------------------------------------------------------\n# Performance tuning for different types of telemetry data ingestion\n\n# Spans (distributed tracing data)\n# Contains trace information showing request flow across services\nspans:\n  # Number of parallel processing threads\n  # Default: GOMAXPROCS (number of CPU cores)\n  # Increase for high-volume tracing workloads\n  #max_threads: 10\n\n  # Batch size for database insertions\n  # Larger batches improve throughput but increase memory usage\n  # Tune based on your ingestion rate and memory constraints\n  #max_insert_size: 10000\n\n  # In-memory buffer capacity for incoming spans\n  # Spans are dropped when buffer is full (check metrics for drops)\n  # Default scales with max_threads\n  #max_buffered_records: 100e3\n\n# Span links (relationships between spans)\n# Used for connecting spans across trace boundaries\nspan_links:\n  # Uncomment to disable span link processing\n  # This saves resources if you don't use span links\n  #disabled: true\n\n  #max_threads: 10              # Processing parallelism\n  #max_insert_size: 10000       # Batch size for insertions\n  #max_buffered_records: 100e3  # Buffer capacity\n\n# Application logs\n# Structured and unstructured log data from applications\nlogs:\n  #max_threads: 10              # Processing parallelism\n  #max_insert_size: 10000       # Batch size for insertions\n  #max_buffered_records: 100e3  # Buffer capacity\n\n# Custom events\n# Application-specific events and business metrics\nevents:\n  #max_threads: 10              # Processing parallelism\n  #max_insert_size: 10000       # Batch size for insertions\n  #max_buffered_records: 100e3  # Buffer capacity\n\n# Metrics and time series data\n# Numerical measurements over time (counters, gauges, histograms)\nmetrics:\n  #max_threads: 10              # Processing parallelism\n  #max_insert_size: 10000       # Batch size for insertions\n  #max_buffered_records: 100e3  # Buffer capacity\n\n  # Memory limit for cumulative to delta conversion\n  # Affects processing of cumulative counters from OpenTelemetry\n  #max_cumulative_timeseries: 1e6\n\n# -----------------------------------------------------------------------------\n# Query & Performance Limits\n# -----------------------------------------------------------------------------\n# Resource limits to prevent expensive queries from affecting system performance\n\n# Trace query resource limits\ntrace:\n  # Maximum number of spans to return in a single query\n  # Prevents UI timeouts and excessive memory usage\n  # Users can adjust time ranges to stay within limits\n  #query_limit: 200_000\n\n  # Maximum memory usage per query (in bytes)\n  # Prevents OOM errors from complex queries\n  # Adjust based on available system memory\n  #max_memory_usage_bytes: 200_000_000\n\n# -----------------------------------------------------------------------------\n# Feature Modules\n# -----------------------------------------------------------------------------\n# Optional features that can be enabled/disabled for performance or security\n\n# Monitoring and alerting system\n# Provides proactive monitoring with notifications\nalerting:\n  # Uncomment to disable the entire alerting system\n  # This saves resources if you don't use monitoring alerts\n  #disabled: true\n\n# Service dependency graph generation\n# Automatically builds service topology from trace data\nservice_graph:\n  # Uncomment to disable service graph processing\n  # This saves CPU and memory if you don't use the service map\n  #disabled: true\n\n# JavaScript error sourcemap processing\n# Provides better error stack traces for frontend applications\n# Requires internet access to download source maps from URLs\nsourcemaps:\n  # Uncomment to disable sourcemap processing\n  # Disable in air-gapped environments or if not using JS error tracking\n  #disabled: true\n\n# -----------------------------------------------------------------------------\n# External Services\n# -----------------------------------------------------------------------------\n# Integration with external services and self-monitoring\n\n# Internal telemetry collection\n# Uptrace monitors itself and sends telemetry to the configured DSN\nself_monitoring:\n  # Uncomment to disable self-monitoring\n  # This prevents Uptrace from generating its own telemetry data\n  #disabled: true\n\n  # DSN for internal telemetry\n  # Format: http://project_token@host?grpc=port\n  # This can point to a project in this Uptrace instance or to the Uptrace Cloud\n  dsn: http://project1_secret@localhost:14318?grpc=14317\n\n  #tls:\n  #  insecure_skip_verify: true\n\n# Telegram bot for notifications\n# Enables sending alert notifications to Telegram channels/users\n# Setup guide: https://sendpulse.com/knowledge-base/chatbot/telegram/create-telegram-chatbot\ntelegram:\n  # Telegram bot token obtained from @BotFather\n  # Required for sending notifications to Telegram\n  bot_token: ''\n\n# -----------------------------------------------------------------------------\n# System Configuration\n# -----------------------------------------------------------------------------\n# Global system settings and licensing\n\n# Application logging configuration\n# Controls Uptrace's own log output (not application logs)\nlogging:\n  # Log level affects verbosity and performance\n  # DEBUG: Very verbose, use only for troubleshooting\n  # INFO: Standard operational information\n  # WARN: Warning messages and errors\n  # ERROR: Only error messages\n  level: INFO\n\n# Premium features license\n# Enables advanced features like distributed tables, SSO, etc.\n# Details: https://uptrace.dev/get/hosted#premium-edition\nlicense:\n  # Premium license key from Uptrace\n  # Required for enterprise features\n  key: ''\n"
  },
  {
    "path": "example/otel-metrics/README.md",
    "content": "# OpenTelemetry Metrics Example\n\nThis example demonstrates how to enable OpenTelemetry metrics for Redis operations using the `extra/redisotel-native` package.\n\n## Features\n\n- ✅ OTLP exporter configuration\n- ✅ Periodic metric export (every 10 seconds)\n- ✅ Concurrent Redis operations\n- ✅ Automatic metric collection for:\n  - Operation duration\n  - Connection metrics\n  - Error tracking\n\n## Prerequisites\n\n- Go 1.24.0 or later\n- Redis server running on `localhost:6379`\n- OTLP collector running on `localhost:4317` (optional)\n\n## Running the Example\n\n```bash\n# Start Redis (if not already running)\nredis-server\n\n# Optional: Start OTLP collector\n# See: https://opentelemetry.io/docs/collector/\n\n# Run the example\ngo run main.go\n```\n\n## What It Does\n\n1. Creates an OTLP exporter that sends metrics to a collector\n2. Sets up a meter provider with periodic export (every 10 seconds)\n3. Initializes Redis client with OTel instrumentation\n4. Executes concurrent Redis operations (SET commands)\n5. Waits for metrics to be exported\n\n## Metrics Collected\n\nThe example automatically collects:\n\n- **db.client.operation.duration** - Operation latency histogram\n- **db.client.connection.create_time** - Connection creation time\n- **db.client.connection.count** - Active connection count\n- **db.client.errors** - Error counter with error type classification\n\n## Configuration\n\nTo use with a production OTLP collector:\n\n```go\nexporter, err := otlpmetricgrpc.New(ctx,\n    otlpmetricgrpc.WithEndpoint(\"your-collector:4317\"),\n    otlpmetricgrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(certPool, \"\")),\n)\n```\n\n## See Also\n\n- [OpenTelemetry Go SDK](https://opentelemetry.io/docs/languages/go/)\n- [OTLP Exporter Documentation](https://opentelemetry.io/docs/specs/otlp/)\n- [Redis OTel Native Package](../../extra/redisotel-native/)\n\n"
  },
  {
    "path": "example/otel-metrics/go.mod",
    "content": "module github.com/redis/go-redis/example/otel-metrics\n\ngo 1.24.0\n\ntoolchain go1.24.4\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nreplace github.com/redis/go-redis/extra/redisotel-native/v9 => ../../extra/redisotel-native\n\nrequire (\n\tgithub.com/redis/go-redis/extra/redisotel-native/v9 v9.18.0\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgo.opentelemetry.io/otel v1.40.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0\n\tgo.opentelemetry.io/otel/sdk/metric v1.40.0\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/otel/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/net v0.50.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgoogle.golang.org/grpc v1.79.1 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n)\n"
  },
  {
    "path": "example/otel-metrics/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 h1:NpbJl/eVbvrGE0MJ6X16X9SAifesl6Fwxg/YmCvubRI=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8/go.mod h1:mi7YA+gCzVem12exXy46ZespvGtX/lZmD/RLnQhVW7U=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=\ngoogle.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "example/otel-metrics/main.go",
    "content": "// EXAMPLE: otel_metrics\n// HIDE_START\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\tredisotel \"github.com/redis/go-redis/extra/redisotel-native/v9\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc\"\n\t\"go.opentelemetry.io/otel/sdk/metric\"\n)\n\n// ExampleClient_otel_metrics demonstrates how to enable OpenTelemetry metrics\n// for Redis operations and export them to an OTLP collector.\nfunc main() {\n\tctx := context.Background()\n\n\t// HIDE_END\n\n\t// STEP_START otel_exporter_setup\n\t// Create OTLP exporter that sends metrics to the collector\n\t// Default endpoint is localhost:4317 (gRPC)\n\texporter, err := otlpmetricgrpc.New(ctx,\n\t\totlpmetricgrpc.WithInsecure(), // Use insecure for local development\n\t\t// For production, configure TLS and authentication:\n\t\t// otlpmetricgrpc.WithEndpoint(\"your-collector:4317\"),\n\t\t// otlpmetricgrpc.WithTLSCredentials(...),\n\t)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create OTLP exporter: %v\", err)\n\t}\n\t// STEP_END\n\n\t// STEP_START otel_meter_provider\n\t// Create meter provider with periodic reader\n\t// Metrics are exported every 10 seconds\n\tmeterProvider := metric.NewMeterProvider(\n\t\tmetric.WithReader(\n\t\t\tmetric.NewPeriodicReader(exporter,\n\t\t\t\tmetric.WithInterval(10*time.Second),\n\t\t\t),\n\t\t),\n\t)\n\tdefer func() {\n\t\tif err := meterProvider.Shutdown(ctx); err != nil {\n\t\t\tlog.Printf(\"Error shutting down meter provider: %v\", err)\n\t\t}\n\t}()\n\n\t// Set the global meter provider\n\totel.SetMeterProvider(meterProvider)\n\t// STEP_END\n\n\t// STEP_START redis_client_setup\n\t// Initialize OTel instrumentation BEFORE creating Redis clients\n\totelInstance := redisotel.GetObservabilityInstance()\n\tconfig := redisotel.NewConfig().WithEnabled(true)\n\tif err := otelInstance.Init(config); err != nil {\n\t\tlog.Fatalf(\"Failed to initialize OTel: %v\", err)\n\t}\n\tdefer otelInstance.Shutdown()\n\n\t// Create Redis client - automatically instrumented\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tdefer rdb.Close()\n\t// STEP_END\n\n\t// STEP_START redis_operations\n\t// Execute Redis operations - metrics are automatically collected\n\tlog.Println(\"Executing Redis operations...\")\n\tvar wg sync.WaitGroup\n\twg.Add(50)\n\tfor i := range 50 {\n\t\tgo func(i int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor j := range 10 {\n\t\t\t\tif err := rdb.Set(ctx, \"key\"+strconv.Itoa(i*10+j), \"value\", 0).Err(); err != nil {\n\t\t\t\t\tlog.Printf(\"Error setting key: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(time.Millisecond * time.Duration(rand.Intn(400)))\n\t\t\t}\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\twg.Add(10)\n\tfor i := range 10 {\n\t\tgo func(i int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor j := range 10 {\n\t\t\t\tif err := rdb.Set(ctx, \"key\"+strconv.Itoa(i*10+j), \"value\", 0).Err(); err != nil {\n\t\t\t\t\tlog.Printf(\"Error setting key: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(time.Millisecond * time.Duration(rand.Intn(400)))\n\t\t\t}\n\t\t}(i)\n\t}\n\twg.Wait()\n\n\tfor j := range 10 {\n\t\tif err := rdb.Set(ctx, \"key\"+strconv.Itoa(j), \"value\", 0).Err(); err != nil {\n\t\t\tlog.Printf(\"Error setting key: %v\", err)\n\t\t}\n\t\ttime.Sleep(time.Millisecond * time.Duration(rand.Intn(400)))\n\t}\n\n\tlog.Println(\"Operations complete. Waiting for metrics to be exported...\")\n\n\t// Wait for metrics to be exported\n\ttime.Sleep(15 * time.Second)\n\t// STEP_END\n}\n"
  },
  {
    "path": "example/redis-bloom/README.md",
    "content": "# RedisBloom example for go-redis\n\nThis is an example for\n[Bloom, Cuckoo, Count-Min, Top-K](https://redis.uptrace.dev/guide/bloom-cuckoo-count-min-top-k.html)\narticle.\n\nTo run it, you need to compile and install\n[RedisBloom](https://oss.redis.com/redisbloom/Quick_Start/#building) module:\n\n```shell\ngo run .\n```\n"
  },
  {
    "path": "example/redis-bloom/go.mod",
    "content": "module github.com/redis/go-redis/example/redis-bloom\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.18.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/redis-bloom/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/redis-bloom/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\t_ = rdb.FlushDB(ctx).Err()\n\n\tfmt.Printf(\"# BLOOM\\n\")\n\tbloomFilter(ctx, rdb)\n\n\tfmt.Printf(\"\\n# CUCKOO\\n\")\n\tcuckooFilter(ctx, rdb)\n\n\tfmt.Printf(\"\\n# COUNT-MIN\\n\")\n\tcountMinSketch(ctx, rdb)\n\n\tfmt.Printf(\"\\n# TOP-K\\n\")\n\ttopK(ctx, rdb)\n}\n\nfunc bloomFilter(ctx context.Context, rdb *redis.Client) {\n\tinserted, err := rdb.Do(ctx, \"BF.ADD\", \"bf_key\", \"item0\").Bool()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif inserted {\n\t\tfmt.Println(\"item0 was inserted\")\n\t} else {\n\t\tfmt.Println(\"item0 already exists\")\n\t}\n\n\tfor _, item := range []string{\"item0\", \"item1\"} {\n\t\texists, err := rdb.Do(ctx, \"BF.EXISTS\", \"bf_key\", item).Bool()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif exists {\n\t\t\tfmt.Printf(\"%s does exist\\n\", item)\n\t\t} else {\n\t\t\tfmt.Printf(\"%s does not exist\\n\", item)\n\t\t}\n\t}\n\n\tbools, err := rdb.Do(ctx, \"BF.MADD\", \"bf_key\", \"item1\", \"item2\", \"item3\").BoolSlice()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"adding multiple items:\", bools)\n}\n\nfunc cuckooFilter(ctx context.Context, rdb *redis.Client) {\n\tinserted, err := rdb.Do(ctx, \"CF.ADDNX\", \"cf_key\", \"item0\").Bool()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif inserted {\n\t\tfmt.Println(\"item0 was inserted\")\n\t} else {\n\t\tfmt.Println(\"item0 already exists\")\n\t}\n\n\tfor _, item := range []string{\"item0\", \"item1\"} {\n\t\texists, err := rdb.Do(ctx, \"CF.EXISTS\", \"cf_key\", item).Bool()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif exists {\n\t\t\tfmt.Printf(\"%s does exist\\n\", item)\n\t\t} else {\n\t\t\tfmt.Printf(\"%s does not exist\\n\", item)\n\t\t}\n\t}\n\n\tdeleted, err := rdb.Do(ctx, \"CF.DEL\", \"cf_key\", \"item0\").Bool()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif deleted {\n\t\tfmt.Println(\"item0 was deleted\")\n\t}\n}\n\nfunc countMinSketch(ctx context.Context, rdb *redis.Client) {\n\tif err := rdb.Do(ctx, \"CMS.INITBYPROB\", \"count_min\", 0.001, 0.01).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\n\titems := []string{\"item1\", \"item2\", \"item3\", \"item4\", \"item5\"}\n\tcounts := make(map[string]int, len(items))\n\n\tfor i := 0; i < 10000; i++ {\n\t\tn := rand.Intn(len(items))\n\t\titem := items[n]\n\n\t\tif err := rdb.Do(ctx, \"CMS.INCRBY\", \"count_min\", item, 1).Err(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tcounts[item]++\n\t}\n\n\tfor item, count := range counts {\n\t\tns, err := rdb.Do(ctx, \"CMS.QUERY\", \"count_min\", item).Int64Slice()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfmt.Printf(\"%s: count-min=%d actual=%d\\n\", item, ns[0], count)\n\t}\n}\n\nfunc topK(ctx context.Context, rdb *redis.Client) {\n\tif err := rdb.Do(ctx, \"TOPK.RESERVE\", \"top_items\", 3).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tcounts := map[string]int{\n\t\t\"item1\": 1000,\n\t\t\"item2\": 2000,\n\t\t\"item3\": 3000,\n\t\t\"item4\": 4000,\n\t\t\"item5\": 5000,\n\t\t\"item6\": 6000,\n\t}\n\n\tfor item, count := range counts {\n\t\tfor i := 0; i < count; i++ {\n\t\t\tif err := rdb.Do(ctx, \"TOPK.INCRBY\", \"top_items\", item, 1).Err(); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}\n\n\titems, err := rdb.Do(ctx, \"TOPK.LIST\", \"top_items\").StringSlice()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor _, item := range items {\n\t\tns, err := rdb.Do(ctx, \"TOPK.COUNT\", \"top_items\", item).Int64Slice()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfmt.Printf(\"%s: top-k=%d actual=%d\\n\", item, ns[0], counts[item])\n\t}\n}\n"
  },
  {
    "path": "example/scan-struct/README.md",
    "content": "# Example for scanning hash fields into a struct\n\nTo run this example:\n\n```shell\ngo run .\n```\n\nSee\n[Redis: Scanning hash fields into a struct](https://redis.uptrace.dev/guide/scanning-hash-fields.html)\nfor details.\n"
  },
  {
    "path": "example/scan-struct/go.mod",
    "content": "module github.com/redis/go-redis/example/scan-struct\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1\n\tgithub.com/redis/go-redis/v9 v9.18.0\n)\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/scan-struct/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/scan-struct/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype Model struct {\n\tStr1    string   `redis:\"str1\"`\n\tStr2    string   `redis:\"str2\"`\n\tStr3    *string  `redis:\"str3\"`\n\tBytes   []byte   `redis:\"bytes\"`\n\tInt     int      `redis:\"int\"`\n\tInt2    *int     `redis:\"int2\"`\n\tBool    bool     `redis:\"bool\"`\n\tBool2   *bool    `redis:\"bool2\"`\n\tIgnored struct{} `redis:\"-\"`\n}\n\nfunc main() {\n\tctx := context.Background()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\t_ = rdb.FlushDB(ctx).Err()\n\n\t// Set some fields.\n\tif _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {\n\t\trdb.HSet(ctx, \"key\", \"str1\", \"hello\")\n\t\trdb.HSet(ctx, \"key\", \"str2\", \"world\")\n\t\trdb.HSet(ctx, \"key\", \"str3\", \"\")\n\t\trdb.HSet(ctx, \"key\", \"int\", 123)\n\t\trdb.HSet(ctx, \"key\", \"int2\", 0)\n\t\trdb.HSet(ctx, \"key\", \"bool\", 1)\n\t\trdb.HSet(ctx, \"key\", \"bool2\", 0)\n\t\trdb.HSet(ctx, \"key\", \"bytes\", []byte(\"this is bytes !\"))\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar model1, model2 Model\n\n\t// Scan all fields into the model.\n\tif err := rdb.HGetAll(ctx, \"key\").Scan(&model1); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Or scan a subset of the fields.\n\tif err := rdb.HMGet(ctx, \"key\", \"str1\", \"int\").Scan(&model2); err != nil {\n\t\tpanic(err)\n\t}\n\n\tspew.Dump(model1)\n\t// Output:\n\t// (main.Model) {\n\t// \tStr1: (string) (len=5) \"hello\",\n\t// \tStr2: (string) (len=5) \"world\",\n\t// \tBytes: ([]uint8) (len=15 cap=16) {\n\t// \t 00000000  74 68 69 73 20 69 73 20  62 79 74 65 73 20 21     |this is bytes !|\n\t// \t},\n\t// \tInt: (int) 123,\n\t// \tBool: (bool) true,\n\t// \tIgnored: (struct {}) {\n\t// \t}\n\t// }\n\n\tspew.Dump(model2)\n\t// Output:\n\t// (main.Model) {\n\t// \tStr1: (string) (len=5) \"hello\",\n\t// \tStr2: (string) \"\",\n\t// \tBytes: ([]uint8) <nil>,\n\t// \tInt: (int) 123,\n\t// \tBool: (bool) false,\n\t// \tIgnored: (struct {}) {\n\t// \t}\n\t// }\n}\n"
  },
  {
    "path": "example/tls-cert-auth/README.md",
    "content": "# TLS Certificate Authentication Example\n\nThis example demonstrates how to use TLS client certificates for automatic authentication with Redis 8.6+.\n\nWhen Redis is configured with `tls-auth-clients-user CN`, it uses the Common Name (CN) field from the client certificate as the username, eliminating the need for password-based authentication.\n\n## Prerequisites\n\n- Redis 8.6+ with TLS enabled\n- Redis configured with: `tls-auth-clients-user CN`\n- Client certificate with CN matching a Redis ACL user\n- The ACL user must exist and have `nopass` set\n\n## How It Works\n\n1. **Load CA certificate** - Used to verify the Redis server's certificate\n2. **Load client certificate** - The CN field must match a Redis ACL username\n3. **Connect with TLS** - No username/password needed in the connection options\n4. **Redis authenticates automatically** - Based on the certificate's CN field\n\n## Running the Example\n\n```bash\n# Start Redis with TLS (from the go-redis root directory)\ndocker compose --profile standalone up -d\n\n# Run the example\ncd example/tls-cert-auth\ngo run main.go\n```\n\n## Expected Output\n\n```\n✅ Authenticated as: testcertuser (via TLS certificate CN)\n✅ SET/GET successful: hello from cert auth!\n\n🎉 TLS certificate authentication working!\n```\n\n## Docker Configuration\n\nThe go-redis test environment is configured with these environment variables:\n\n```yaml\nenvironment:\n  - TLS_ENABLED=yes\n  - TLS_CLIENT_CNS=testcertuser      # Generates testcertuser.{crt,key}\n  - TLS_AUTH_CLIENTS_USER=CN         # Enables CN-based authentication\n```\n\n## Key Code\n\n```go\n// Load client certificate (CN must match Redis ACL username)\nclientCert, err := tls.LoadX509KeyPair(\n    \"testcertuser.crt\",\n    \"testcertuser.key\",\n)\n\n// Create TLS config\ntlsConfig := &tls.Config{\n    RootCAs:      caCertPool,\n    Certificates: []tls.Certificate{clientCert},\n}\n\n// Connect - NO username/password needed!\nclient := redis.NewClient(&redis.Options{\n    Addr:      \"localhost:6666\",\n    TLSConfig: tlsConfig,\n})\n```\n\n## Fallback Behavior\n\nIf the certificate CN doesn't match any existing ACL user, Redis falls back to the `default` user. See `tls_cert_auth_test.go` for tests covering both scenarios.\n\n"
  },
  {
    "path": "example/tls-cert-auth/go.mod",
    "content": "module github.com/redis/go-redis/example/tls-cert-auth\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.18.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/tls-cert-auth/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/tls-cert-auth/main.go",
    "content": "// TLS Certificate Authentication Example\n//\n// This example demonstrates how to use TLS client certificates for\n// automatic authentication with Redis 8.6+.\n//\n// When Redis is configured with `tls-auth-clients-user CN`, it uses\n// the Common Name (CN) field from the client certificate as the username,\n// eliminating the need for password-based authentication.\n//\n// Prerequisites:\n//   - Redis 8.6+ with TLS enabled\n//   - Redis configured with: tls-auth-clients-user CN\n//   - Client certificate with CN matching a Redis ACL user\n//   - The ACL user must exist\n//\n// To run with the go-redis test environment:\n//\n//\tdocker compose --profile standalone up -d\n//\tgo run main.go\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\t// Configuration - adjust paths as needed\n\tcertDir := \"../../dockers/standalone/tls\"\n\tusername := \"testcertuser\" // Must match CN in certificate and ACL user\n\ttlsPort := \"6666\"\n\tnonTLSPort := \"6379\"\n\n\t// Step 1: First, ensure the ACL user exists (using non-TLS connection)\n\tsetupClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:\" + nonTLSPort,\n\t})\n\tdefer setupClient.Close()\n\n\t// Create the ACL user if it doesn't exist\n\terr := setupClient.ACLSetUser(ctx,\n\t\tusername,\n\t\t\"on\",     // Enable the user\n\t\t\"nopass\", // No password - will use cert auth\n\t\t\"~*\",     // Access all keys\n\t\t\"+@all\",  // All commands (adjust as needed)\n\t).Err()\n\tif err != nil {\n\t\tlog.Printf(\"Note: Could not create ACL user (may already exist): %v\", err)\n\t}\n\n\t// Step 2: Load CA certificate\n\tcaCert, err := os.ReadFile(certDir + \"/ca.crt\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to load CA certificate: %v\", err)\n\t}\n\tcaCertPool := x509.NewCertPool()\n\tcaCertPool.AppendCertsFromPEM(caCert)\n\n\t// Step 3: Load client certificate (CN must match the Redis ACL username)\n\tclientCert, err := tls.LoadX509KeyPair(\n\t\tcertDir+\"/\"+username+\".crt\",\n\t\tcertDir+\"/\"+username+\".key\",\n\t)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to load client certificate: %v\", err)\n\t}\n\n\t// Step 4: Create TLS config\n\ttlsConfig := &tls.Config{\n\t\tRootCAs:            caCertPool,\n\t\tCertificates:       []tls.Certificate{clientCert},\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // Only for self-signed certs in testing\n\t}\n\n\t// Step 5: Connect to Redis with TLS - NO username/password needed!\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr:      \"localhost:\" + tlsPort,\n\t\tTLSConfig: tlsConfig,\n\t\t// Note: No Username or Password fields - auth happens via certificate\n\t})\n\tdefer client.Close()\n\n\t// Step 6: Verify authentication\n\twhoami, err := client.ACLWhoAmI(ctx).Result()\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to get current user: %v\", err)\n\t}\n\tfmt.Printf(\"✅ Authenticated as: %s (via TLS certificate CN)\\n\", whoami)\n\n\t// Step 7: Test some commands\n\terr = client.Set(ctx, \"tls-auth-example\", \"hello from cert auth!\", 0).Err()\n\tif err != nil {\n\t\tlog.Fatalf(\"SET failed: %v\", err)\n\t}\n\n\tval, err := client.Get(ctx, \"tls-auth-example\").Result()\n\tif err != nil {\n\t\tlog.Fatalf(\"GET failed: %v\", err)\n\t}\n\tfmt.Printf(\"✅ SET/GET successful: %s\\n\", val)\n\n\t// Cleanup\n\tclient.Del(ctx, \"tls-auth-example\")\n\n\tfmt.Println(\"\\n🎉 TLS certificate authentication working!\")\n}\n"
  },
  {
    "path": "example/tls-connection/README.md",
    "content": "# TLS Connection Examples\n\nShows different ways to connect to Redis over TLS.\n\n## Running\n\nStart Redis with TLS:\n\n```shell\ncd ../..\ndocker compose --profile standalone up -d\n```\n\nThen run the example:\n\n```shell\ngo run .\n```\n\n## Connection Methods\n\n### 1. InsecureSkipVerify (for testing)\n\nQuick way to test with self-signed certs:\n\n```go\nclient := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6666\",\n    TLSConfig: &tls.Config{\n        InsecureSkipVerify: true,\n    },\n})\n```\n\nDon't use this in production.\n\n### 2. With CA certificate\n\nProper way for production:\n\n```go\ncaCert, _ := os.ReadFile(\"path/to/ca.crt\")\ncaCertPool := x509.NewCertPool()\ncaCertPool.AppendCertsFromPEM(caCert)\n\nclient := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6666\",\n    TLSConfig: &tls.Config{\n        RootCAs:    caCertPool,\n        ServerName: \"localhost\",\n    },\n})\n```\n\n### 3. Mutual TLS\n\nIf Redis requires client certs:\n\n```go\ncaCert, _ := os.ReadFile(\"path/to/ca.crt\")\ncaCertPool := x509.NewCertPool()\ncaCertPool.AppendCertsFromPEM(caCert)\n\ncert, _ := tls.LoadX509KeyPair(\"path/to/client.crt\", \"path/to/client.key\")\n\nclient := redis.NewClient(&redis.Options{\n    Addr: \"localhost:6666\",\n    TLSConfig: &tls.Config{\n        RootCAs:      caCertPool,\n        Certificates: []tls.Certificate{cert},\n        ServerName:   \"localhost\",\n    },\n})\n```\n\n### 4. Using rediss:// URL\n\n```go\nopt, _ := redis.ParseURL(\"rediss://localhost:6666\")\nopt.TLSConfig = &tls.Config{\n    InsecureSkipVerify: true, // for testing only\n}\nclient := redis.NewClient(opt)\n```\n\n### 5. Certificate-based auth\n\nRedis 6.2+ can authenticate users based on the certificate CN field. You need to configure Redis with:\n\n```\ntls-auth-clients optional\ntls-auth-clients-user CN\n```\n\nThen the CN in your client cert becomes your username - no password needed.\n\nCheck `../../tls_cert_auth_test.go` for a working example that:\n- Generates a client cert with a specific CN\n- Connects to Redis with that cert\n- Verifies auth based on the CN\n\nNote: Current Redis test build doesn't support this yet, so the test skips gracefully.\n\n## Tests\n\nRun the TLS tests:\n\n```shell\ngo test -v -run \"^TestTLS\" -timeout 30s\n```\n\n## Notes\n\n- Always verify certs in production (don't use InsecureSkipVerify)\n- Keep your private keys safe\n- Use TLS 1.2 or higher\n\n"
  },
  {
    "path": "example/tls-connection/go.mod",
    "content": "module github.com/redis/go-redis/example/tls-connection\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire github.com/redis/go-redis/v9 v9.18.0\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n"
  },
  {
    "path": "example/tls-connection/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "example/tls-connection/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\t// Example 1: TLS with InsecureSkipVerify (for testing with self-signed certs)\n\tfmt.Println(\"Example 1: TLS with InsecureSkipVerify\")\n\tclient1 := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6666\", // TLS port\n\t\tTLSConfig: &tls.Config{\n\t\t\tInsecureSkipVerify: true,\n\t\t},\n\t})\n\tdefer client1.Close()\n\n\tif err := client1.Ping(ctx).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to connect: %v\\n\", err)\n\t} else {\n\t\tfmt.Println(\"✅ Connected successfully with InsecureSkipVerify\")\n\t}\n\n\t// Example 2: TLS with CA certificate verification\n\tfmt.Println(\"\\nExample 2: TLS with CA certificate verification\")\n\t\n\t// Load CA certificate\n\tcaCert, err := os.ReadFile(\"path/to/ca.crt\")\n\tif err != nil {\n\t\tfmt.Printf(\"Note: CA cert not found (this is expected in this example): %v\\n\", err)\n\t} else {\n\t\tcaCertPool := x509.NewCertPool()\n\t\tcaCertPool.AppendCertsFromPEM(caCert)\n\n\t\tclient2 := redis.NewClient(&redis.Options{\n\t\t\tAddr: \"localhost:6666\",\n\t\t\tTLSConfig: &tls.Config{\n\t\t\t\tRootCAs:    caCertPool,\n\t\t\t\tServerName: \"localhost\",\n\t\t\t},\n\t\t})\n\t\tdefer client2.Close()\n\n\t\tif err := client2.Ping(ctx).Err(); err != nil {\n\t\t\tfmt.Printf(\"Failed to connect: %v\\n\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"✅ Connected successfully with CA verification\")\n\t\t}\n\t}\n\n\t// Example 3: TLS with client certificate (mutual TLS)\n\tfmt.Println(\"\\nExample 3: TLS with client certificate (mutual TLS)\")\n\t\n\t// Load CA certificate\n\tcaCert, err = os.ReadFile(\"path/to/ca.crt\")\n\tif err != nil {\n\t\tfmt.Printf(\"Note: CA cert not found (this is expected in this example): %v\\n\", err)\n\t} else {\n\t\tcaCertPool := x509.NewCertPool()\n\t\tcaCertPool.AppendCertsFromPEM(caCert)\n\n\t\t// Load client certificate and key\n\t\tcert, err := tls.LoadX509KeyPair(\"path/to/client.crt\", \"path/to/client.key\")\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Note: Client cert not found (this is expected in this example): %v\\n\", err)\n\t\t} else {\n\t\t\tclient3 := redis.NewClient(&redis.Options{\n\t\t\t\tAddr: \"localhost:6666\",\n\t\t\t\tTLSConfig: &tls.Config{\n\t\t\t\t\tRootCAs:      caCertPool,\n\t\t\t\t\tCertificates: []tls.Certificate{cert},\n\t\t\t\t\tServerName:   \"localhost\",\n\t\t\t\t},\n\t\t\t})\n\t\t\tdefer client3.Close()\n\n\t\t\tif err := client3.Ping(ctx).Err(); err != nil {\n\t\t\t\tfmt.Printf(\"Failed to connect: %v\\n\", err)\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"✅ Connected successfully with client certificate\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Example 4: Using rediss:// URL scheme\n\tfmt.Println(\"\\nExample 4: Using rediss:// URL scheme\")\n\t\n\topt, err := redis.ParseURL(\"rediss://localhost:6666\")\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to parse URL: %v\\n\", err)\n\t\treturn\n\t}\n\t\n\t// Add InsecureSkipVerify for testing with self-signed certs\n\topt.TLSConfig = &tls.Config{\n\t\tInsecureSkipVerify: true,\n\t}\n\t\n\tclient4 := redis.NewClient(opt)\n\tdefer client4.Close()\n\n\tif err := client4.Ping(ctx).Err(); err != nil {\n\t\tfmt.Printf(\"Failed to connect: %v\\n\", err)\n\t} else {\n\t\tfmt.Println(\"✅ Connected successfully using rediss:// URL\")\n\t}\n\n\t// Example 5: TLS with certificate-based authentication (future feature)\n\t// This demonstrates how to use client certificates for authentication\n\t// when Redis is configured with: tls-auth-clients-user CN\n\tfmt.Println(\"\\nExample 5: TLS with certificate-based authentication\")\n\tfmt.Println(\"Note: This requires Redis 6.2+ with tls-auth-clients-user CN configuration\")\n\tfmt.Println(\"The certificate's CN (Common Name) field will be used as the Redis username\")\n\tfmt.Println(\"See tls_cert_auth_test.go for a complete working example\")\n}\n\n"
  },
  {
    "path": "example_instrumentation_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype redisHook struct{}\n\nvar _ redis.Hook = redisHook{}\n\nfunc (redisHook) DialHook(hook redis.DialHook) redis.DialHook {\n\treturn func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\tfmt.Printf(\"dialing %s %s\\n\", network, addr)\n\t\tconn, err := hook(ctx, network, addr)\n\t\tfmt.Printf(\"finished dialing %s %s\\n\", network, addr)\n\t\treturn conn, err\n\t}\n}\n\nfunc (redisHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {\n\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\tfmt.Printf(\"starting processing: <%v>\\n\", cmd.Args())\n\t\terr := hook(ctx, cmd)\n\t\tfmt.Printf(\"finished processing: <%v>\\n\", cmd.Args())\n\t\treturn err\n\t}\n}\n\nfunc (redisHook) ProcessPipelineHook(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\tnames := make([]string, 0, len(cmds))\n\t\tfor _, cmd := range cmds {\n\t\t\tnames = append(names, fmt.Sprintf(\"%v\", cmd.Args()))\n\t\t}\n\t\tfmt.Printf(\"pipeline starting processing: %v\\n\", names)\n\t\terr := hook(ctx, cmds)\n\t\tfmt.Printf(\"pipeline finished processing: %v\\n\", names)\n\t\treturn err\n\t}\n}\n\nfunc Example_instrumentation() {\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:            \":6379\",\n\t\tDisableIdentity: true,\n\t})\n\trdb.AddHook(redisHook{})\n\n\trdb.Ping(ctx)\n\t// Output:\n\t// starting processing: <[ping]>\n\t// dialing tcp :6379\n\t// finished dialing tcp :6379\n\t// starting processing: <[hello 3]>\n\t// finished processing: <[hello 3]>\n\t// starting processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>\n\t// finished processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>\n\t// finished processing: <[ping]>\n}\n\nfunc ExamplePipeline_instrumentation() {\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:            \":6379\",\n\t\tDisableIdentity: true,\n\t})\n\trdb.AddHook(redisHook{})\n\n\trdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\tpipe.Ping(ctx)\n\t\tpipe.Ping(ctx)\n\t\treturn nil\n\t})\n\t// Output:\n\t// pipeline starting processing: [[ping] [ping]]\n\t// dialing tcp :6379\n\t// finished dialing tcp :6379\n\t// starting processing: <[hello 3]>\n\t// finished processing: <[hello 3]>\n\t// starting processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>\n\t// finished processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>\n\t// pipeline finished processing: [[ping] [ping]]\n}\n\nfunc ExampleClient_Watch_instrumentation() {\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:            \":6379\",\n\t\tDisableIdentity: true,\n\t})\n\trdb.AddHook(redisHook{})\n\n\trdb.Watch(ctx, func(tx *redis.Tx) error {\n\t\ttx.Ping(ctx)\n\t\ttx.Ping(ctx)\n\t\treturn nil\n\t}, \"foo\")\n\t// Output:\n\t// starting processing: <[watch foo]>\n\t// dialing tcp :6379\n\t// finished dialing tcp :6379\n\t// starting processing: <[hello 3]>\n\t// finished processing: <[hello 3]>\n\t// starting processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>\n\t// finished processing: <[client maint_notifications on moving-endpoint-type internal-fqdn]>\n\t// finished processing: <[watch foo]>\n\t// starting processing: <[ping]>\n\t// finished processing: <[ping]>\n\t// starting processing: <[ping]>\n\t// finished processing: <[ping]>\n\t// starting processing: <[unwatch]>\n\t// finished processing: <[unwatch]>\n}\n"
  },
  {
    "path": "example_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar (\n\tctx = context.Background()\n\trdb *redis.Client\n)\n\nfunc init() {\n\trdb = redis.NewClient(&redis.Options{\n\t\tAddr:         \":6379\",\n\t\tDialTimeout:  10 * time.Second,\n\t\tReadTimeout:  30 * time.Second,\n\t\tWriteTimeout: 30 * time.Second,\n\t\tPoolSize:     10,\n\t\tPoolTimeout:  30 * time.Second,\n\t})\n}\n\nfunc ExampleNewClient() {\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\", // use default Addr\n\t\tPassword: \"\",               // no password set\n\t\tDB:       0,                // use default DB\n\t})\n\n\tpong, err := rdb.Ping(ctx).Result()\n\tfmt.Println(pong, err)\n\t// Output: PONG <nil>\n}\n\nfunc ExampleParseURL() {\n\topt, err := redis.ParseURL(\"redis://:qwerty@localhost:6379/1?dial_timeout=5s\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"addr is\", opt.Addr)\n\tfmt.Println(\"db is\", opt.DB)\n\tfmt.Println(\"password is\", opt.Password)\n\tfmt.Println(\"dial timeout is\", opt.DialTimeout)\n\n\t// Create client as usually.\n\t_ = redis.NewClient(opt)\n\n\t// Output: addr is localhost:6379\n\t// db is 1\n\t// password is qwerty\n\t// dial timeout is 5s\n}\n\nfunc ExampleNewFailoverClient() {\n\t// See https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel for instructions how to\n\t// setup Redis Sentinel.\n\trdb := redis.NewFailoverClient(&redis.FailoverOptions{\n\t\tMasterName:    \"master\",\n\t\tSentinelAddrs: []string{\":26379\"},\n\t})\n\trdb.Ping(ctx)\n}\n\nfunc ExampleNewClusterClient() {\n\t// See https://redis.io/docs/latest/operate/oss_and_stack/management/scaling for instructions\n\t// how to setup Redis Cluster.\n\trdb := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs: []string{\":7000\", \":7001\", \":7002\", \":7003\", \":7004\", \":7005\"},\n\t})\n\trdb.Ping(ctx)\n}\n\n// Following example creates a cluster from 2 master nodes and 2 slave nodes\n// without using cluster mode or Redis Sentinel.\nfunc ExampleNewClusterClient_manualSetup() {\n\t// clusterSlots returns cluster slots information.\n\t// It can use service like ZooKeeper to maintain configuration information\n\t// and Cluster.ReloadState to manually trigger state reloading.\n\tclusterSlots := func(ctx context.Context) ([]redis.ClusterSlot, error) {\n\t\tslots := []redis.ClusterSlot{\n\t\t\t// First node with 1 master and 1 slave.\n\t\t\t{\n\t\t\t\tStart: 0,\n\t\t\t\tEnd:   8191,\n\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\tAddr: \":7000\", // master\n\t\t\t\t}, {\n\t\t\t\t\tAddr: \":8000\", // 1st slave\n\t\t\t\t}},\n\t\t\t},\n\t\t\t// Second node with 1 master and 1 slave.\n\t\t\t{\n\t\t\t\tStart: 8192,\n\t\t\t\tEnd:   16383,\n\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\tAddr: \":7001\", // master\n\t\t\t\t}, {\n\t\t\t\t\tAddr: \":8001\", // 1st slave\n\t\t\t\t}},\n\t\t\t},\n\t\t}\n\t\treturn slots, nil\n\t}\n\n\trdb := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tClusterSlots:  clusterSlots,\n\t\tRouteRandomly: true,\n\t})\n\trdb.Ping(ctx)\n\n\t// ReloadState reloads cluster state. It calls ClusterSlots func\n\t// to get cluster slots information.\n\trdb.ReloadState(ctx)\n}\n\nfunc ExampleNewRing() {\n\trdb := redis.NewRing(&redis.RingOptions{\n\t\tAddrs: map[string]string{\n\t\t\t\"shard1\": \":7000\",\n\t\t\t\"shard2\": \":7001\",\n\t\t\t\"shard3\": \":7002\",\n\t\t},\n\t})\n\trdb.Ping(ctx)\n}\n\nfunc ExampleClient() {\n\terr := rdb.Set(ctx, \"key\", \"value\", 0).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tval, err := rdb.Get(ctx, \"key\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"key\", val)\n\n\tval2, err := rdb.Get(ctx, \"missing_key\").Result()\n\tif err == redis.Nil {\n\t\tfmt.Println(\"missing_key does not exist\")\n\t} else if err != nil {\n\t\tpanic(err)\n\t} else {\n\t\tfmt.Println(\"missing_key\", val2)\n\t}\n\t// Output: key value\n\t// missing_key does not exist\n}\n\nfunc ExampleConn_name() {\n\tconn := rdb.Conn()\n\n\terr := conn.ClientSetName(ctx, \"foobar\").Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Open other connections.\n\tfor i := 0; i < 10; i++ {\n\t\tgo rdb.Ping(ctx)\n\t}\n\n\ts, err := conn.ClientGetName(ctx).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(s)\n\t// Output: foobar\n}\n\nfunc ExampleConn_client_setInfo_libraryVersion() {\n\tconn := rdb.Conn()\n\n\terr := conn.ClientSetInfo(ctx, redis.WithLibraryVersion(\"1.2.3\")).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Open other connections.\n\tfor i := 0; i < 10; i++ {\n\t\tgo rdb.Ping(ctx)\n\t}\n\n\ts, err := conn.ClientInfo(ctx).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(s.LibVer)\n\t// Output: 1.2.3\n}\n\nfunc ExampleClient_Set() {\n\t// Last argument is expiration. Zero means the key has no\n\t// expiration time.\n\terr := rdb.Set(ctx, \"key\", \"value\", 0).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// key2 will expire in an hour.\n\terr = rdb.Set(ctx, \"key2\", \"value\", time.Hour).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc ExampleClient_SetEx() {\n\terr := rdb.SetEx(ctx, \"key\", \"value\", time.Hour).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc ExampleClient_HSet() {\n\t// Set \"redis\" tag for hash key\n\ttype ExampleUser struct {\n\t\tName string `redis:\"name\"`\n\t\tAge  int    `redis:\"age\"`\n\t}\n\n\titems := ExampleUser{\"jane\", 22}\n\n\terr := rdb.HSet(ctx, \"user:1\", items).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc ExampleClient_Incr() {\n\tresult, err := rdb.Incr(ctx, \"counter\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(result)\n\t// Output: 1\n}\n\nfunc ExampleClient_BLPop() {\n\tif err := rdb.RPush(ctx, \"queue\", \"message\").Err(); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// use `rdb.BLPop(ctx, 0, \"queue\")` for infinite waiting time\n\tresult, err := rdb.BLPop(ctx, 1*time.Second, \"queue\").Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(result[0], result[1])\n\t// Output: queue message\n}\n\nfunc ExampleClient_Scan() {\n\trdb.FlushDB(ctx)\n\tfor i := 0; i < 33; i++ {\n\t\terr := rdb.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tvar cursor uint64\n\tvar n int\n\tfor {\n\t\tvar keys []string\n\t\tvar err error\n\t\tkeys, cursor, err = rdb.Scan(ctx, cursor, \"key*\", 10).Result()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tn += len(keys)\n\t\tif cursor == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfmt.Printf(\"found %d keys\\n\", n)\n\t// Output: found 33 keys\n}\n\nfunc ExampleClient_ScanType() {\n\trdb.FlushDB(ctx)\n\tfor i := 0; i < 33; i++ {\n\t\terr := rdb.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tvar cursor uint64\n\tvar n int\n\tfor {\n\t\tvar keys []string\n\t\tvar err error\n\t\tkeys, cursor, err = rdb.ScanType(ctx, cursor, \"key*\", 10, \"string\").Result()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tn += len(keys)\n\t\tif cursor == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfmt.Printf(\"found %d keys\\n\", n)\n\t// Output: found 33 keys\n}\n\n// ExampleClient_ScanType_hashType uses the keyType \"hash\".\nfunc ExampleClient_ScanType_hashType() {\n\trdb.FlushDB(ctx)\n\tfor i := 0; i < 33; i++ {\n\t\terr := rdb.HSet(context.TODO(), fmt.Sprintf(\"key%d\", i), \"value\", \"foo\").Err()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tvar allKeys []string\n\tvar cursor uint64\n\tvar err error\n\n\tfor {\n\t\tvar keysFromScan []string\n\t\tkeysFromScan, cursor, err = rdb.ScanType(context.TODO(), cursor, \"key*\", 10, \"hash\").Result()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tallKeys = append(allKeys, keysFromScan...)\n\t\tif cursor == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\tfmt.Printf(\"%d keys ready for use\", len(allKeys))\n\t// Output: 33 keys ready for use\n}\n\n// ExampleMapStringStringCmd_Scan shows how to scan the results of a map fetch\n// into a struct.\nfunc ExampleMapStringStringCmd_Scan() {\n\trdb.FlushDB(ctx)\n\terr := rdb.HMSet(ctx, \"map\",\n\t\t\"name\", \"hello\",\n\t\t\"count\", 123,\n\t\t\"correct\", true).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Get the map. The same approach works for HmGet().\n\tres := rdb.HGetAll(ctx, \"map\")\n\tif res.Err() != nil {\n\t\tpanic(res.Err())\n\t}\n\n\ttype data struct {\n\t\tName    string `redis:\"name\"`\n\t\tCount   int    `redis:\"count\"`\n\t\tCorrect bool   `redis:\"correct\"`\n\t}\n\n\t// Scan the results into the struct.\n\tvar d data\n\tif err := res.Scan(&d); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(d)\n\t// Output: {hello 123 true}\n}\n\n// ExampleSliceCmd_Scan shows how to scan the results of a multi key fetch\n// into a struct.\nfunc ExampleSliceCmd_Scan() {\n\trdb.FlushDB(ctx)\n\terr := rdb.MSet(ctx,\n\t\t\"name\", \"hello\",\n\t\t\"count\", 123,\n\t\t\"correct\", true).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tres := rdb.MGet(ctx, \"name\", \"count\", \"empty\", \"correct\")\n\tif res.Err() != nil {\n\t\tpanic(res.Err())\n\t}\n\n\ttype data struct {\n\t\tName    string `redis:\"name\"`\n\t\tCount   int    `redis:\"count\"`\n\t\tCorrect bool   `redis:\"correct\"`\n\t}\n\n\t// Scan the results into the struct.\n\tvar d data\n\tif err := res.Scan(&d); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(d)\n\t// Output: {hello 123 true}\n}\n\nfunc ExampleClient_Pipelined() {\n\tvar incr *redis.IntCmd\n\t_, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\tincr = pipe.Incr(ctx, \"pipelined_counter\")\n\t\tpipe.Expire(ctx, \"pipelined_counter\", time.Hour)\n\t\treturn nil\n\t})\n\tfmt.Println(incr.Val(), err)\n\t// Output: 1 <nil>\n}\n\nfunc ExampleClient_Pipeline() {\n\tpipe := rdb.Pipeline()\n\n\tincr := pipe.Incr(ctx, \"pipeline_counter\")\n\tpipe.Expire(ctx, \"pipeline_counter\", time.Hour)\n\n\t// Execute\n\t//\n\t//     INCR pipeline_counter\n\t//     EXPIRE pipeline_counts 3600\n\t//\n\t// using one rdb-server roundtrip.\n\t_, err := pipe.Exec(ctx)\n\tfmt.Println(incr.Val(), err)\n\t// Output: 1 <nil>\n}\n\nfunc ExampleClient_TxPipelined() {\n\tvar incr *redis.IntCmd\n\t_, err := rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\tincr = pipe.Incr(ctx, \"tx_pipelined_counter\")\n\t\tpipe.Expire(ctx, \"tx_pipelined_counter\", time.Hour)\n\t\treturn nil\n\t})\n\tfmt.Println(incr.Val(), err)\n\t// Output: 1 <nil>\n}\n\nfunc ExampleClient_TxPipeline() {\n\tpipe := rdb.TxPipeline()\n\n\tincr := pipe.Incr(ctx, \"tx_pipeline_counter\")\n\tpipe.Expire(ctx, \"tx_pipeline_counter\", time.Hour)\n\n\t// Execute\n\t//\n\t//     MULTI\n\t//     INCR pipeline_counter\n\t//     EXPIRE pipeline_counts 3600\n\t//     EXEC\n\t//\n\t// using one rdb-server roundtrip.\n\t_, err := pipe.Exec(ctx)\n\tfmt.Println(incr.Val(), err)\n\t// Output: 1 <nil>\n}\n\nfunc ExampleClient_Watch() {\n\tconst maxRetries = 10000\n\n\t// Increment transactionally increments key using GET and SET commands.\n\tincrement := func(key string) error {\n\t\t// Transactional function.\n\t\ttxf := func(tx *redis.Tx) error {\n\t\t\t// Get current value or zero.\n\t\t\tn, err := tx.Get(ctx, key).Int()\n\t\t\tif err != nil && err != redis.Nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Actual operation (local in optimistic lock).\n\t\t\tn++\n\n\t\t\t// Operation is committed only if the watched keys remain unchanged.\n\t\t\t_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Set(ctx, key, n, 0)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < maxRetries; i++ {\n\t\t\terr := rdb.Watch(ctx, txf, key)\n\t\t\tif err == nil {\n\t\t\t\t// Success.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif err == redis.TxFailedErr {\n\t\t\t\t// Optimistic lock lost. Retry.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Return any other error.\n\t\t\treturn err\n\t\t}\n\n\t\treturn errors.New(\"increment reached maximum number of retries\")\n\t}\n\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < 100; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tif err := increment(\"counter3\"); err != nil {\n\t\t\t\tfmt.Println(\"increment error:\", err)\n\t\t\t}\n\t\t}()\n\t}\n\twg.Wait()\n\n\tn, err := rdb.Get(ctx, \"counter3\").Int()\n\tfmt.Println(\"ended with\", n, err)\n\t// Output: ended with 100 <nil>\n}\n\nfunc ExamplePubSub() {\n\tpubsub := rdb.Subscribe(ctx, \"mychannel1\")\n\n\t// Wait for confirmation that subscription is created before publishing anything.\n\t_, err := pubsub.Receive(ctx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Go channel which receives messages.\n\tch := pubsub.Channel()\n\n\t// Publish a message.\n\terr = rdb.Publish(ctx, \"mychannel1\", \"hello\").Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\ttime.AfterFunc(time.Second, func() {\n\t\t// When pubsub is closed channel is closed too.\n\t\t_ = pubsub.Close()\n\t})\n\n\t// Consume messages.\n\tfor msg := range ch {\n\t\tfmt.Println(msg.Channel, msg.Payload)\n\t}\n\n\t// Output: mychannel1 hello\n}\n\nfunc ExamplePubSub_Receive() {\n\tpubsub := rdb.Subscribe(ctx, \"mychannel2\")\n\tdefer pubsub.Close()\n\n\tfor i := 0; i < 2; i++ {\n\t\t// ReceiveTimeout is a low level API. Use ReceiveMessage instead.\n\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tswitch msg := msgi.(type) {\n\t\tcase *redis.Subscription:\n\t\t\tfmt.Println(\"subscribed to\", msg.Channel)\n\n\t\t\t_, err := rdb.Publish(ctx, \"mychannel2\", \"hello\").Result()\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\tcase *redis.Message:\n\t\t\tfmt.Println(\"received\", msg.Payload, \"from\", msg.Channel)\n\t\tdefault:\n\t\t\tpanic(\"unreached\")\n\t\t}\n\t}\n\n\t// sent message to 1 rdb\n\t// received hello from mychannel2\n}\n\nfunc ExampleScript() {\n\tIncrByXX := redis.NewScript(`\n\t\tif redis.call(\"GET\", KEYS[1]) ~= false then\n\t\t\treturn redis.call(\"INCRBY\", KEYS[1], ARGV[1])\n\t\tend\n\t\treturn false\n\t`)\n\n\tn, err := IncrByXX.Run(ctx, rdb, []string{\"xx_counter\"}, 2).Result()\n\tfmt.Println(n, err)\n\n\terr = rdb.Set(ctx, \"xx_counter\", \"40\", 0).Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tn, err = IncrByXX.Run(ctx, rdb, []string{\"xx_counter\"}, 2).Result()\n\tfmt.Println(n, err)\n\n\t// Output: <nil> redis: nil\n\t// 42 <nil>\n}\n\nfunc Example_customCommand() {\n\tGet := func(ctx context.Context, rdb *redis.Client, key string) *redis.StringCmd {\n\t\tcmd := redis.NewStringCmd(ctx, \"get\", key)\n\t\trdb.Process(ctx, cmd)\n\t\treturn cmd\n\t}\n\n\tv, err := Get(ctx, rdb, \"key_does_not_exist\").Result()\n\tfmt.Printf(\"%q %s\", v, err)\n\t// Output: \"\" redis: nil\n}\n\nfunc Example_customCommand2() {\n\tv, err := rdb.Do(ctx, \"get\", \"key_does_not_exist\").Text()\n\tfmt.Printf(\"%q %s\", v, err)\n\t// Output: \"\" redis: nil\n}\n\nfunc ExampleScanIterator() {\n\titer := rdb.Scan(ctx, 0, \"\", 0).Iterator()\n\tfor iter.Next(ctx) {\n\t\tfmt.Println(iter.Val())\n\t}\n\tif err := iter.Err(); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc ExampleScanCmd_Iterator() {\n\titer := rdb.Scan(ctx, 0, \"\", 0).Iterator()\n\tfor iter.Next(ctx) {\n\t\tfmt.Println(iter.Val())\n\t}\n\tif err := iter.Err(); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc ExampleNewUniversalClient_simple() {\n\trdb := redis.NewUniversalClient(&redis.UniversalOptions{\n\t\tAddrs: []string{\":6379\"},\n\t})\n\tdefer rdb.Close()\n\n\trdb.Ping(ctx)\n}\n\nfunc ExampleNewUniversalClient_failover() {\n\trdb := redis.NewUniversalClient(&redis.UniversalOptions{\n\t\tMasterName: \"master\",\n\t\tAddrs:      []string{\":26379\"},\n\t})\n\tdefer rdb.Close()\n\n\trdb.Ping(ctx)\n}\n\nfunc ExampleNewUniversalClient_cluster() {\n\trdb := redis.NewUniversalClient(&redis.UniversalOptions{\n\t\tAddrs: []string{\":7000\", \":7001\", \":7002\", \":7003\", \":7004\", \":7005\"},\n\t})\n\tdefer rdb.Close()\n\n\trdb.Ping(ctx)\n}\n\nfunc ExampleClient_SlowLogGet() {\n\tif RECluster {\n\t\t// skip slowlog test for cluster\n\t\tfmt.Println(2)\n\t\treturn\n\t}\n\tconst key = \"slowlog-log-slower-than\"\n\n\told := rdb.ConfigGet(ctx, key).Val()\n\trdb.ConfigSet(ctx, key, \"0\")\n\tdefer rdb.ConfigSet(ctx, key, old[key])\n\n\tif err := rdb.Do(ctx, \"slowlog\", \"reset\").Err(); err != nil {\n\t\tpanic(err)\n\t}\n\n\trdb.Set(ctx, \"test\", \"true\", 0)\n\n\tresult, err := rdb.SlowLogGet(ctx, -1).Result()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(len(result))\n\t// Output: 2\n}\n"
  },
  {
    "path": "export_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\nfunc (c *baseClient) Pool() pool.Pooler {\n\treturn c.connPool\n}\n\nfunc (c *PubSub) SetNetConn(netConn net.Conn) {\n\tc.cn = pool.NewConn(netConn)\n}\n\nfunc (c *ClusterClient) LoadState(ctx context.Context) (*clusterState, error) {\n\t// return c.state.Reload(ctx)\n\treturn c.loadState(ctx)\n}\n\nfunc (c *ClusterClient) SlotAddrs(ctx context.Context, slot int) []string {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar addrs []string\n\tfor _, n := range state.slotNodes(slot) {\n\t\taddrs = append(addrs, n.Client.getAddr())\n\t}\n\treturn addrs\n}\n\nfunc (c *ClusterClient) Nodes(ctx context.Context, key string) ([]*clusterNode, error) {\n\tstate, err := c.state.Reload(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tslot := hashtag.Slot(key)\n\tnodes := state.slotNodes(slot)\n\tif len(nodes) != 2 {\n\t\treturn nil, fmt.Errorf(\"slot=%d does not have enough nodes: %v\", slot, nodes)\n\t}\n\treturn nodes, nil\n}\n\nfunc (c *ClusterClient) SwapNodes(ctx context.Context, key string) error {\n\tnodes, err := c.Nodes(ctx, key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnodes[0], nodes[1] = nodes[1], nodes[0]\n\treturn nil\n}\n\nfunc (c *clusterState) IsConsistent(ctx context.Context) bool {\n\tif len(c.Masters) < 3 {\n\t\treturn false\n\t}\n\tfor _, master := range c.Masters {\n\t\ts := master.Client.Info(ctx, \"replication\").Val()\n\t\tif !strings.Contains(s, \"role:master\") {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tif len(c.Slaves) < 3 {\n\t\treturn false\n\t}\n\tfor _, slave := range c.Slaves {\n\t\ts := slave.Client.Info(ctx, \"replication\").Val()\n\t\tif !strings.Contains(s, \"role:slave\") {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc GetSlavesAddrByName(ctx context.Context, c *SentinelClient, name string) []string {\n\taddrs, err := c.Replicas(ctx, name).Result()\n\tif err != nil {\n\t\tinternal.Logger.Printf(ctx, \"sentinel: Replicas name=%q failed: %s\",\n\t\t\tname, err)\n\t\treturn []string{}\n\t}\n\treturn parseReplicaAddrs(addrs, false)\n}\n\nfunc (c *Ring) ShardByName(name string) *ringShard {\n\tshard, _ := c.sharding.GetByName(name)\n\treturn shard\n}\n\nfunc (c *ModuleLoadexConfig) ToArgs() []interface{} {\n\treturn c.toArgs()\n}\n\nfunc ShouldRetry(err error, retryTimeout bool) bool {\n\treturn shouldRetry(err, retryTimeout)\n}\n"
  },
  {
    "path": "extra/rediscensus/go.mod",
    "content": "module github.com/redis/go-redis/extra/rediscensus/v9\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nreplace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd\n\nrequire (\n\tgithub.com/redis/go-redis/extra/rediscmd/v9 v9.18.0\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgo.opencensus.io v0.24.0\n)\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n\nretract (\n\tv9.7.2 // This version was accidentally released. Please use version 9.7.3 instead.\n\tv9.5.3 // This version was accidentally released. Please use version 9.6.0 instead.\n)\n"
  },
  {
    "path": "extra/rediscensus/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "extra/rediscensus/rediscensus.go",
    "content": "package rediscensus\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"go.opencensus.io/trace\"\n\n\t\"github.com/redis/go-redis/extra/rediscmd/v9\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype TracingHook struct{}\n\nvar _ redis.Hook = (*TracingHook)(nil)\n\nfunc NewTracingHook() *TracingHook {\n\treturn new(TracingHook)\n}\n\nfunc (TracingHook) DialHook(next redis.DialHook) redis.DialHook {\n\treturn func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\tctx, span := trace.StartSpan(ctx, \"dial\")\n\t\tdefer span.End()\n\n\t\tspan.AddAttributes(\n\t\t\ttrace.StringAttribute(\"db.system\", \"redis\"),\n\t\t\ttrace.StringAttribute(\"network\", network),\n\t\t\ttrace.StringAttribute(\"addr\", addr),\n\t\t)\n\n\t\tconn, err := next(ctx, network, addr)\n\t\tif err != nil {\n\t\t\trecordErrorOnOCSpan(ctx, span, err)\n\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn conn, nil\n\t}\n}\n\nfunc (TracingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {\n\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\tctx, span := trace.StartSpan(ctx, cmd.FullName())\n\t\tdefer span.End()\n\n\t\tspan.AddAttributes(\n\t\t\ttrace.StringAttribute(\"db.system\", \"redis\"),\n\t\t\ttrace.StringAttribute(\"redis.cmd\", rediscmd.CmdString(cmd)),\n\t\t)\n\n\t\terr := next(ctx, cmd)\n\t\tif err != nil {\n\t\t\trecordErrorOnOCSpan(ctx, span, err)\n\t\t\treturn err\n\t\t}\n\n\t\tif err = cmd.Err(); err != nil {\n\t\t\trecordErrorOnOCSpan(ctx, span, err)\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\nfunc (TracingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\treturn next\n}\n\nfunc recordErrorOnOCSpan(ctx context.Context, span *trace.Span, err error) {\n\tif err != redis.Nil {\n\t\tspan.AddAttributes(trace.BoolAttribute(\"error\", true))\n\t\tspan.Annotate([]trace.Attribute{trace.StringAttribute(\"Error\", \"redis error\")}, err.Error())\n\t}\n}\n"
  },
  {
    "path": "extra/rediscmd/go.mod",
    "content": "module github.com/redis/go-redis/extra/rediscmd/v9\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire (\n\tgithub.com/bsm/ginkgo/v2 v2.12.0\n\tgithub.com/bsm/gomega v1.27.10\n\tgithub.com/redis/go-redis/v9 v9.18.0\n)\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n\nretract (\n\tv9.7.2 // This version was accidentally released. Please use version 9.7.3 instead.\n\tv9.5.3 // This version was accidentally released. Please use version 9.6.0 instead.\n)\n"
  },
  {
    "path": "extra/rediscmd/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "extra/rediscmd/rediscmd.go",
    "content": "package rediscmd\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc CmdString(cmd redis.Cmder) string {\n\tb := make([]byte, 0, 32)\n\tb = AppendCmd(b, cmd)\n\treturn String(b)\n}\n\nfunc CmdsString(cmds []redis.Cmder) (string, string) {\n\tconst numNameLimit = 10\n\n\tseen := make(map[string]struct{}, numNameLimit)\n\tunqNames := make([]string, 0, numNameLimit)\n\n\tb := make([]byte, 0, 32*len(cmds))\n\n\tfor i, cmd := range cmds {\n\t\tif i > 0 {\n\t\t\tb = append(b, '\\n')\n\t\t}\n\t\tb = AppendCmd(b, cmd)\n\n\t\tif len(unqNames) >= numNameLimit {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := cmd.FullName()\n\t\tif _, ok := seen[name]; !ok {\n\t\t\tseen[name] = struct{}{}\n\t\t\tunqNames = append(unqNames, name)\n\t\t}\n\t}\n\n\tsummary := strings.Join(unqNames, \" \")\n\treturn summary, String(b)\n}\n\nfunc AppendCmd(b []byte, cmd redis.Cmder) []byte {\n\tfor i, arg := range cmd.Args() {\n\t\tif i > 0 {\n\t\t\tb = append(b, ' ')\n\t\t}\n\t\tb = appendArg(b, arg)\n\t}\n\n\tif err := cmd.Err(); err != nil {\n\t\tb = append(b, \": \"...)\n\t\tb = append(b, err.Error()...)\n\t}\n\n\treturn b\n}\n\nfunc appendArg(b []byte, v interface{}) []byte {\n\tswitch v := v.(type) {\n\tcase nil:\n\t\treturn append(b, \"<nil>\"...)\n\tcase string:\n\t\treturn appendUTF8String(b, Bytes(v))\n\tcase []byte:\n\t\treturn appendUTF8String(b, v)\n\tcase int:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int8:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int16:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int32:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int64:\n\t\treturn strconv.AppendInt(b, v, 10)\n\tcase uint:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint8:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint16:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint32:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint64:\n\t\treturn strconv.AppendUint(b, v, 10)\n\tcase float32:\n\t\treturn strconv.AppendFloat(b, float64(v), 'f', -1, 64)\n\tcase float64:\n\t\treturn strconv.AppendFloat(b, v, 'f', -1, 64)\n\tcase bool:\n\t\tif v {\n\t\t\treturn append(b, \"true\"...)\n\t\t}\n\t\treturn append(b, \"false\"...)\n\tcase time.Time:\n\t\treturn v.AppendFormat(b, time.RFC3339Nano)\n\tdefault:\n\t\treturn append(b, fmt.Sprint(v)...)\n\t}\n}\n\nfunc appendUTF8String(dst []byte, src []byte) []byte {\n\tif isSimple(src) {\n\t\tdst = append(dst, src...)\n\t\treturn dst\n\t}\n\n\ts := len(dst)\n\tdst = append(dst, make([]byte, hex.EncodedLen(len(src)))...)\n\thex.Encode(dst[s:], src)\n\treturn dst\n}\n\nfunc isSimple(b []byte) bool {\n\tfor _, c := range b {\n\t\tif !isSimpleByte(c) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isSimpleByte(c byte) bool {\n\treturn c >= 0x20 && c <= 0x7e\n}\n"
  },
  {
    "path": "extra/rediscmd/rediscmd_test.go",
    "content": "package rediscmd\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nfunc TestGinkgo(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"redisext\")\n}\n\nvar _ = Describe(\"AppendArg\", func() {\n\tDescribeTable(\"...\",\n\t\tfunc(src string, wanted string) {\n\t\t\tb := appendArg(nil, src)\n\t\t\tExpect(string(b)).To(Equal(wanted))\n\t\t},\n\n\t\tEntry(\"\", \"-inf\", \"-inf\"),\n\t\tEntry(\"\", \"+inf\", \"+inf\"),\n\t\tEntry(\"\", \"foo.bar\", \"foo.bar\"),\n\t\tEntry(\"\", \"foo:bar\", \"foo:bar\"),\n\t\tEntry(\"\", \"foo bar\", \"foo bar\"),\n\t\tEntry(\"\", \"foo{bar}\", \"foo{bar}\"),\n\t\tEntry(\"\", \"foo-123_BAR\", \"foo-123_BAR\"),\n\t\tEntry(\"\", \"foo\\nbar\", \"666f6f0a626172\"),\n\t\tEntry(\"\", \"\\000\", \"00\"),\n\t)\n})\n"
  },
  {
    "path": "extra/rediscmd/safe.go",
    "content": "//go:build appengine\n// +build appengine\n\npackage rediscmd\n\nfunc String(b []byte) string {\n\treturn string(b)\n}\n\nfunc Bytes(s string) []byte {\n\treturn []byte(s)\n}\n"
  },
  {
    "path": "extra/rediscmd/unsafe.go",
    "content": "//go:build !appengine\n// +build !appengine\n\npackage rediscmd\n\nimport \"unsafe\"\n\n// String converts byte slice to string.\nfunc String(b []byte) string {\n\treturn *(*string)(unsafe.Pointer(&b))\n}\n\n// Bytes converts string to byte slice.\nfunc Bytes(s string) []byte {\n\treturn *(*[]byte)(unsafe.Pointer(\n\t\t&struct {\n\t\t\tstring\n\t\t\tCap int\n\t\t}{s, len(s)},\n\t))\n}\n"
  },
  {
    "path": "extra/redisotel/README.md",
    "content": "# OpenTelemetry instrumentation for go-redis\n\n## Installation\n\n```bash\ngo get github.com/redis/go-redis/extra/redisotel/v9\n```\n\n## Usage\n\nTracing is enabled by adding a hook:\n\n```go\nimport (\n    \"github.com/redis/go-redis/v9\"\n    \"github.com/redis/go-redis/extra/redisotel/v9\"\n)\n\nrdb := rdb.NewClient(&rdb.Options{...})\n\n// Enable tracing instrumentation.\nif err := redisotel.InstrumentTracing(rdb); err != nil {\n\tpanic(err)\n}\n\n// Enable metrics instrumentation.\nif err := redisotel.InstrumentMetrics(rdb); err != nil {\n\tpanic(err)\n}\n```\n\nSee [example](../../example/otel) and\n[Monitoring Go Redis Performance and Errors](https://redis.uptrace.dev/guide/go-redis-monitoring.html)\nfor details.\n"
  },
  {
    "path": "extra/redisotel/config.go",
    "content": "package redisotel\n\nimport (\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/metric\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.24.0\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\ntype config struct {\n\t// Common options.\n\n\tdbSystem string\n\tattrs    []attribute.KeyValue\n\n\t// Tracing options.\n\n\ttp     trace.TracerProvider\n\ttracer trace.Tracer\n\n\tdbStmtEnabled         bool\n\tcallerEnabled         bool\n\tfilterDial            bool\n\tfilterProcessPipeline func(cmds []redis.Cmder) bool\n\tfilterProcess         func(cmd redis.Cmder) bool\n\n\t// Metrics options.\n\n\tmp    metric.MeterProvider\n\tmeter metric.Meter\n\n\tpoolName string\n\n\tcloseChan chan struct{}\n}\n\ntype baseOption interface {\n\tapply(conf *config)\n}\n\ntype Option interface {\n\tbaseOption\n\ttracing()\n\tmetrics()\n}\n\ntype option func(conf *config)\n\nfunc (fn option) apply(conf *config) {\n\tfn(conf)\n}\n\nfunc (fn option) tracing() {}\n\nfunc (fn option) metrics() {}\n\nfunc newConfig(opts ...baseOption) *config {\n\tconf := &config{\n\t\tdbSystem: \"redis\",\n\t\tattrs:    []attribute.KeyValue{},\n\n\t\ttp:            otel.GetTracerProvider(),\n\t\tmp:            otel.GetMeterProvider(),\n\t\tdbStmtEnabled: true,\n\t\tcallerEnabled: true,\n\t\tfilterProcess: DefaultCommandFilter,\n\t\tfilterProcessPipeline: func(cmds []redis.Cmder) bool {\n\t\t\tfor _, cmd := range cmds {\n\t\t\t\tif DefaultCommandFilter(cmd) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t}\n\n\tfor _, opt := range opts {\n\t\topt.apply(conf)\n\t}\n\n\tconf.attrs = append(conf.attrs, semconv.DBSystemKey.String(conf.dbSystem))\n\n\treturn conf\n}\n\nfunc WithDBSystem(dbSystem string) Option {\n\treturn option(func(conf *config) {\n\t\tconf.dbSystem = dbSystem\n\t})\n}\n\n// WithAttributes specifies additional attributes to be added to the span.\nfunc WithAttributes(attrs ...attribute.KeyValue) Option {\n\treturn option(func(conf *config) {\n\t\tconf.attrs = append(conf.attrs, attrs...)\n\t})\n}\n\n//------------------------------------------------------------------------------\n\ntype TracingOption interface {\n\tbaseOption\n\ttracing()\n}\n\ntype tracingOption func(conf *config)\n\nvar _ TracingOption = (*tracingOption)(nil)\n\nfunc (fn tracingOption) apply(conf *config) {\n\tfn(conf)\n}\n\nfunc (fn tracingOption) tracing() {}\n\n// WithTracerProvider specifies a tracer provider to use for creating a tracer.\n// If none is specified, the global provider is used.\nfunc WithTracerProvider(provider trace.TracerProvider) TracingOption {\n\treturn tracingOption(func(conf *config) {\n\t\tconf.tp = provider\n\t})\n}\n\n// WithDBStatement tells the tracing hook to log raw redis commands.\nfunc WithDBStatement(on bool) TracingOption {\n\treturn tracingOption(func(conf *config) {\n\t\tconf.dbStmtEnabled = on\n\t})\n}\n\n// WithCallerEnabled tells the tracing hook to log the calling function, file and line.\nfunc WithCallerEnabled(on bool) TracingOption {\n\treturn tracingOption(func(conf *config) {\n\t\tconf.callerEnabled = on\n\t})\n}\n\n// WithCommandFilter allows filtering of commands when tracing to omit commands that may have sensitive details like\n// passwords.\nfunc WithCommandFilter(filter func(cmd redis.Cmder) bool) TracingOption {\n\treturn tracingOption(func(conf *config) {\n\t\tconf.filterProcess = filter\n\t})\n}\n\n// WithCommandsFilter allows filtering of pipeline commands\n// when tracing to omit commands that may have sensitive details like\n// passwords in a pipeline.\nfunc WithCommandsFilter(filter func(cmds []redis.Cmder) bool) TracingOption {\n\treturn tracingOption(func(conf *config) {\n\t\tconf.filterProcessPipeline = filter\n\t})\n}\n\n// WithDialFilter enables or disables filtering of dial commands.\nfunc WithDialFilter(on bool) TracingOption {\n\treturn tracingOption(func(conf *config) {\n\t\tconf.filterDial = on\n\t})\n}\n\n// DefaultCommandFilter filters out AUTH commands from tracing.\nfunc DefaultCommandFilter(cmd redis.Cmder) bool {\n\tif strings.ToLower(cmd.Name()) == \"auth\" {\n\t\treturn true\n\t}\n\n\tif strings.ToLower(cmd.Name()) == \"hello\" {\n\t\tif len(cmd.Args()) < 3 {\n\t\t\treturn false\n\t\t}\n\n\t\targ, exists := cmd.Args()[2].(string)\n\t\tif !exists {\n\t\t\treturn false\n\t\t}\n\n\t\tif strings.ToLower(arg) == \"auth\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// BasicCommandFilter filters out AUTH commands from tracing.\n// Deprecated: use DefaultCommandFilter instead.\nfunc BasicCommandFilter(cmd redis.Cmder) bool {\n\treturn DefaultCommandFilter(cmd)\n}\n\n//------------------------------------------------------------------------------\n\ntype MetricsOption interface {\n\tbaseOption\n\tmetrics()\n}\n\ntype metricsOption func(conf *config)\n\nvar _ MetricsOption = (*metricsOption)(nil)\n\nfunc (fn metricsOption) apply(conf *config) {\n\tfn(conf)\n}\n\nfunc (fn metricsOption) metrics() {}\n\n// WithMeterProvider configures a metric.Meter used to create instruments.\nfunc WithMeterProvider(mp metric.MeterProvider) MetricsOption {\n\treturn metricsOption(func(conf *config) {\n\t\tconf.mp = mp\n\t})\n}\n\nfunc WithCloseChan(closeChan chan struct{}) MetricsOption {\n\treturn metricsOption(func(conf *config) {\n\t\tconf.closeChan = closeChan\n\t})\n}\n"
  },
  {
    "path": "extra/redisotel/go.mod",
    "content": "module github.com/redis/go-redis/extra/redisotel/v9\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nreplace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd\n\nrequire (\n\tgithub.com/redis/go-redis/extra/rediscmd/v9 v9.18.0\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgo.opentelemetry.io/otel v1.22.0\n\tgo.opentelemetry.io/otel/metric v1.22.0\n\tgo.opentelemetry.io/otel/sdk v1.22.0\n\tgo.opentelemetry.io/otel/trace v1.22.0\n)\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/go-logr/logr v1.4.1 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n)\n\nretract (\n\tv9.7.2 // This version was accidentally released. Please use version 9.7.3 instead.\n\tv9.5.3 // This version was accidentally released. Please use version 9.6.0 instead.\n)\n"
  },
  {
    "path": "extra/redisotel/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=\ngithub.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=\ngo.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=\ngo.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=\ngo.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=\ngo.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=\ngo.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=\ngo.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=\ngo.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "extra/redisotel/metrics.go",
    "content": "package redisotel\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/metric\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype metricsState struct {\n\tregistrations []metric.Registration\n\tclosed        bool\n\tmutex         sync.Mutex\n}\n\n// InstrumentMetrics starts reporting OpenTelemetry Metrics.\n//\n// Based on https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-metrics.md\nfunc InstrumentMetrics(rdb redis.UniversalClient, opts ...MetricsOption) error {\n\tbaseOpts := make([]baseOption, len(opts))\n\tfor i, opt := range opts {\n\t\tbaseOpts[i] = opt\n\t}\n\tconf := newConfig(baseOpts...)\n\n\tif conf.meter == nil {\n\t\tconf.meter = conf.mp.Meter(\n\t\t\tinstrumName,\n\t\t\tmetric.WithInstrumentationVersion(\"semver:\"+redis.Version()),\n\t\t)\n\t}\n\n\tvar state *metricsState\n\tif conf.closeChan != nil {\n\t\tstate = &metricsState{\n\t\t\tregistrations: make([]metric.Registration, 0),\n\t\t\tclosed:        false,\n\t\t\tmutex:         sync.Mutex{},\n\t\t}\n\n\t\tgo func() {\n\t\t\t<-conf.closeChan\n\n\t\t\tstate.mutex.Lock()\n\t\t\tstate.closed = true\n\n\t\t\tfor _, registration := range state.registrations {\n\t\t\t\tif err := registration.Unregister(); err != nil {\n\t\t\t\t\totel.Handle(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstate.mutex.Unlock()\n\t\t}()\n\t}\n\n\tswitch rdb := rdb.(type) {\n\tcase *redis.Client:\n\t\treturn registerClient(rdb, conf, state)\n\tcase *redis.ClusterClient:\n\t\trdb.OnNewNode(func(rdb *redis.Client) {\n\t\t\tif err := registerClient(rdb, conf, state); err != nil {\n\t\t\t\totel.Handle(err)\n\t\t\t}\n\t\t})\n\t\treturn nil\n\tcase *redis.Ring:\n\t\trdb.OnNewNode(func(rdb *redis.Client) {\n\t\t\tif err := registerClient(rdb, conf, state); err != nil {\n\t\t\t\totel.Handle(err)\n\t\t\t}\n\t\t})\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"redisotel: %T not supported\", rdb)\n\t}\n}\n\nfunc registerClient(rdb *redis.Client, conf *config, state *metricsState) error {\n\tif state != nil {\n\t\tstate.mutex.Lock()\n\t\tdefer state.mutex.Unlock()\n\n\t\tif state.closed {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif conf.poolName == \"\" {\n\t\topt := rdb.Options()\n\t\tconf.poolName = opt.Addr\n\t}\n\tconf.attrs = append(conf.attrs, attribute.String(\"pool.name\", conf.poolName))\n\n\tregistration, err := reportPoolStats(rdb, conf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif state != nil {\n\t\tstate.registrations = append(state.registrations, registration)\n\t}\n\n\tif err := addMetricsHook(rdb, conf); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc poolStatsAttrs(conf *config) (poolAttrs, idleAttrs, usedAttrs attribute.Set) {\n\tpoolAttrs = attribute.NewSet(conf.attrs...)\n\tidleAttrs = attribute.NewSet(append(poolAttrs.ToSlice(), attribute.String(\"state\", \"idle\"))...)\n\tusedAttrs = attribute.NewSet(append(poolAttrs.ToSlice(), attribute.String(\"state\", \"used\"))...)\n\treturn\n}\n\nfunc reportPoolStats(rdb *redis.Client, conf *config) (metric.Registration, error) {\n\tpoolAttrs, idleAttrs, usedAttrs := poolStatsAttrs(conf)\n\n\tidleMax, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.idle.max\",\n\t\tmetric.WithDescription(\"The maximum number of idle open connections allowed\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidleMin, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.idle.min\",\n\t\tmetric.WithDescription(\"The minimum number of idle open connections allowed\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconnsMax, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.max\",\n\t\tmetric.WithDescription(\"The maximum number of open connections allowed\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tusage, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.usage\",\n\t\tmetric.WithDescription(\"The number of connections that are currently in state described by the state attribute\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\twaits, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.waits\",\n\t\tmetric.WithDescription(\"The number of times a connection was waited for\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\twaitsDuration, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.waits_duration\",\n\t\tmetric.WithDescription(\"The total time spent for waiting a connection in nanoseconds\"),\n\t\tmetric.WithUnit(\"ns\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttimeouts, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.timeouts\",\n\t\tmetric.WithDescription(\"The number of connection timeouts that have occurred trying to obtain a connection from the pool\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thits, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.hits\",\n\t\tmetric.WithDescription(\"The number of times free connection was found in the pool\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmisses, err := conf.meter.Int64ObservableUpDownCounter(\n\t\t\"db.client.connections.misses\",\n\t\tmetric.WithDescription(\"The number of times free connection was not found in the pool\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tredisConf := rdb.Options()\n\treturn conf.meter.RegisterCallback(\n\t\tfunc(ctx context.Context, o metric.Observer) error {\n\t\t\tstats := rdb.PoolStats()\n\n\t\t\to.ObserveInt64(idleMax, int64(redisConf.MaxIdleConns), metric.WithAttributeSet(poolAttrs))\n\t\t\to.ObserveInt64(idleMin, int64(redisConf.MinIdleConns), metric.WithAttributeSet(poolAttrs))\n\t\t\to.ObserveInt64(connsMax, int64(redisConf.PoolSize), metric.WithAttributeSet(poolAttrs))\n\n\t\t\to.ObserveInt64(usage, int64(stats.IdleConns), metric.WithAttributeSet(idleAttrs))\n\t\t\to.ObserveInt64(usage, int64(stats.TotalConns-stats.IdleConns), metric.WithAttributeSet(usedAttrs))\n\n\t\t\to.ObserveInt64(waits, int64(stats.WaitCount), metric.WithAttributeSet(poolAttrs))\n\t\t\to.ObserveInt64(waitsDuration, stats.WaitDurationNs, metric.WithAttributeSet(poolAttrs))\n\n\t\t\to.ObserveInt64(timeouts, int64(stats.Timeouts), metric.WithAttributeSet(poolAttrs))\n\t\t\to.ObserveInt64(hits, int64(stats.Hits), metric.WithAttributeSet(poolAttrs))\n\t\t\to.ObserveInt64(misses, int64(stats.Misses), metric.WithAttributeSet(poolAttrs))\n\t\t\treturn nil\n\t\t},\n\t\tidleMax,\n\t\tidleMin,\n\t\tconnsMax,\n\t\tusage,\n\t\twaits,\n\t\twaitsDuration,\n\t\ttimeouts,\n\t\thits,\n\t\tmisses,\n\t)\n}\n\nfunc addMetricsHook(rdb *redis.Client, conf *config) error {\n\tcreateTime, err := conf.meter.Float64Histogram(\n\t\t\"db.client.connections.create_time\",\n\t\tmetric.WithDescription(\"The time it took to create a new connection.\"),\n\t\tmetric.WithUnit(\"ms\"),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuseTime, err := conf.meter.Float64Histogram(\n\t\t\"db.client.connections.use_time\",\n\t\tmetric.WithDescription(\"The time between borrowing a connection and returning it to the pool.\"),\n\t\tmetric.WithUnit(\"ms\"),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trdb.AddHook(&metricsHook{\n\t\tcreateTime: createTime,\n\t\tuseTime:    useTime,\n\t\tattrs:      conf.attrs,\n\t})\n\treturn nil\n}\n\ntype metricsHook struct {\n\tcreateTime metric.Float64Histogram\n\tuseTime    metric.Float64Histogram\n\tattrs      []attribute.KeyValue\n}\n\nvar _ redis.Hook = (*metricsHook)(nil)\n\nfunc (mh *metricsHook) DialHook(hook redis.DialHook) redis.DialHook {\n\treturn func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\tstart := time.Now()\n\n\t\tconn, err := hook(ctx, network, addr)\n\n\t\tdur := time.Since(start)\n\n\t\tattrs := make([]attribute.KeyValue, 0, len(mh.attrs)+2)\n\t\tattrs = append(attrs, mh.attrs...)\n\t\tattrs = append(attrs, statusAttr(err))\n\t\tattrs = append(attrs, errorTypeAttribute(err))\n\n\t\tmh.createTime.Record(ctx, milliseconds(dur), metric.WithAttributeSet(attribute.NewSet(attrs...)))\n\t\treturn conn, err\n\t}\n}\n\nfunc (mh *metricsHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {\n\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\tstart := time.Now()\n\n\t\terr := hook(ctx, cmd)\n\n\t\tdur := time.Since(start)\n\n\t\tattrs := make([]attribute.KeyValue, 0, len(mh.attrs)+3)\n\t\tattrs = append(attrs, mh.attrs...)\n\t\tattrs = append(attrs, attribute.String(\"type\", \"command\"))\n\t\tattrs = append(attrs, statusAttr(err))\n\t\tattrs = append(attrs, errorTypeAttribute(err))\n\n\t\tmh.useTime.Record(ctx, milliseconds(dur), metric.WithAttributeSet(attribute.NewSet(attrs...)))\n\n\t\treturn err\n\t}\n}\n\nfunc (mh *metricsHook) ProcessPipelineHook(\n\thook redis.ProcessPipelineHook,\n) redis.ProcessPipelineHook {\n\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\tstart := time.Now()\n\n\t\terr := hook(ctx, cmds)\n\n\t\tdur := time.Since(start)\n\n\t\tattrs := make([]attribute.KeyValue, 0, len(mh.attrs)+3)\n\t\tattrs = append(attrs, mh.attrs...)\n\t\tattrs = append(attrs, attribute.String(\"type\", \"pipeline\"))\n\t\tattrs = append(attrs, statusAttr(err))\n\t\tattrs = append(attrs, errorTypeAttribute(err))\n\n\t\tmh.useTime.Record(ctx, milliseconds(dur), metric.WithAttributeSet(attribute.NewSet(attrs...)))\n\n\t\treturn err\n\t}\n}\n\nfunc milliseconds(d time.Duration) float64 {\n\treturn float64(d) / float64(time.Millisecond)\n}\n\nfunc statusAttr(err error) attribute.KeyValue {\n\tif err != nil {\n\t\tif err == redis.Nil {\n\t\t\treturn attribute.String(\"status\", \"nil\")\n\t\t}\n\t\treturn attribute.String(\"status\", \"error\")\n\t}\n\treturn attribute.String(\"status\", \"ok\")\n}\n\nfunc errorTypeAttribute(err error) attribute.KeyValue {\n\tswitch {\n\tcase err == nil:\n\t\treturn attribute.String(\"error_type\", \"none\")\n\tcase errors.Is(err, context.Canceled):\n\t\treturn attribute.String(\"error_type\", \"context_canceled\")\n\tcase errors.Is(err, context.DeadlineExceeded):\n\t\treturn attribute.String(\"error_type\", \"context_timeout\")\n\tdefault:\n\t\treturn attribute.String(\"error_type\", \"other\")\n\t}\n}\n"
  },
  {
    "path": "extra/redisotel/metrics_test.go",
    "content": "package redisotel\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"go.opentelemetry.io/otel/attribute\"\n)\n\nfunc Test_poolStatsAttrs(t *testing.T) {\n\tt.Parallel()\n\ttype args struct {\n\t\tconf *config\n\t}\n\ttests := []struct {\n\t\tname          string\n\t\targs          args\n\t\twantPoolAttrs attribute.Set\n\t\twantIdleAttrs attribute.Set\n\t\twantUsedAttrs attribute.Set\n\t}{\n\t\t{\n\t\t\tname: \"#3122\",\n\t\t\targs: func() args {\n\t\t\t\tconf := &config{\n\t\t\t\t\tattrs: make([]attribute.KeyValue, 0, 4),\n\t\t\t\t}\n\t\t\t\tconf.attrs = append(conf.attrs, attribute.String(\"foo1\", \"bar1\"), attribute.String(\"foo2\", \"bar2\"))\n\t\t\t\tconf.attrs = append(conf.attrs, attribute.String(\"pool.name\", \"pool1\"))\n\t\t\t\treturn args{conf: conf}\n\t\t\t}(),\n\t\t\twantPoolAttrs: attribute.NewSet(attribute.String(\"foo1\", \"bar1\"), attribute.String(\"foo2\", \"bar2\"),\n\t\t\t\tattribute.String(\"pool.name\", \"pool1\")),\n\t\t\twantIdleAttrs: attribute.NewSet(attribute.String(\"foo1\", \"bar1\"), attribute.String(\"foo2\", \"bar2\"),\n\t\t\t\tattribute.String(\"pool.name\", \"pool1\"), attribute.String(\"state\", \"idle\")),\n\t\t\twantUsedAttrs: attribute.NewSet(attribute.String(\"foo1\", \"bar1\"), attribute.String(\"foo2\", \"bar2\"),\n\t\t\t\tattribute.String(\"pool.name\", \"pool1\"), attribute.String(\"state\", \"used\")),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotPoolAttrs, gotIdleAttrs, gotUsedAttrs := poolStatsAttrs(tt.args.conf)\n\t\t\tif !reflect.DeepEqual(gotPoolAttrs, tt.wantPoolAttrs) {\n\t\t\t\tt.Errorf(\"poolStatsAttrs() gotPoolAttrs = %v, want %v\", gotPoolAttrs, tt.wantPoolAttrs)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(gotIdleAttrs, tt.wantIdleAttrs) {\n\t\t\t\tt.Errorf(\"poolStatsAttrs() gotIdleAttrs = %v, want %v\", gotIdleAttrs, tt.wantIdleAttrs)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(gotUsedAttrs, tt.wantUsedAttrs) {\n\t\t\t\tt.Errorf(\"poolStatsAttrs() gotUsedAttrs = %v, want %v\", gotUsedAttrs, tt.wantUsedAttrs)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extra/redisotel/tracing.go",
    "content": "package redisotel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.24.0\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/redis/go-redis/extra/rediscmd/v9\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tinstrumName = \"github.com/redis/go-redis/extra/redisotel\"\n)\n\nfunc InstrumentTracing(rdb redis.UniversalClient, opts ...TracingOption) error {\n\tswitch rdb := rdb.(type) {\n\tcase *redis.Client:\n\t\topt := rdb.Options()\n\t\tconnString := formatDBConnString(opt.Network, opt.Addr)\n\t\topts = addServerAttributes(opts, opt.Addr)\n\t\trdb.AddHook(newTracingHook(connString, opts...))\n\t\treturn nil\n\tcase *redis.ClusterClient:\n\t\trdb.OnNewNode(func(rdb *redis.Client) {\n\t\t\topt := rdb.Options()\n\t\t\topts = addServerAttributes(opts, opt.Addr)\n\t\t\tconnString := formatDBConnString(opt.Network, opt.Addr)\n\t\t\trdb.AddHook(newTracingHook(connString, opts...))\n\t\t})\n\t\treturn nil\n\tcase *redis.Ring:\n\t\trdb.OnNewNode(func(rdb *redis.Client) {\n\t\t\topt := rdb.Options()\n\t\t\topts = addServerAttributes(opts, opt.Addr)\n\t\t\tconnString := formatDBConnString(opt.Network, opt.Addr)\n\t\t\trdb.AddHook(newTracingHook(connString, opts...))\n\t\t})\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"redisotel: %T not supported\", rdb)\n\t}\n}\n\ntype tracingHook struct {\n\tconf *config\n\n\tspanOpts []trace.SpanStartOption\n}\n\nvar _ redis.Hook = (*tracingHook)(nil)\n\nfunc newTracingHook(connString string, opts ...TracingOption) *tracingHook {\n\tbaseOpts := make([]baseOption, len(opts))\n\tfor i, opt := range opts {\n\t\tbaseOpts[i] = opt\n\t}\n\tconf := newConfig(baseOpts...)\n\n\tif conf.tracer == nil {\n\t\tconf.tracer = conf.tp.Tracer(\n\t\t\tinstrumName,\n\t\t\ttrace.WithInstrumentationVersion(\"semver:\"+redis.Version()),\n\t\t)\n\t}\n\tif connString != \"\" {\n\t\tconf.attrs = append(conf.attrs, semconv.DBConnectionString(connString))\n\t}\n\n\treturn &tracingHook{\n\t\tconf: conf,\n\n\t\tspanOpts: []trace.SpanStartOption{\n\t\t\ttrace.WithSpanKind(trace.SpanKindClient),\n\t\t\ttrace.WithAttributes(conf.attrs...),\n\t\t},\n\t}\n}\n\nfunc (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook {\n\treturn func(ctx context.Context, network, addr string) (net.Conn, error) {\n\n\t\tif th.conf.filterDial {\n\t\t\treturn hook(ctx, network, addr)\n\t\t}\n\n\t\tctx, span := th.conf.tracer.Start(ctx, \"redis.dial\", th.spanOpts...)\n\t\tdefer span.End()\n\n\t\tconn, err := hook(ctx, network, addr)\n\t\tif err != nil {\n\t\t\trecordError(span, err)\n\t\t\treturn nil, err\n\t\t}\n\t\treturn conn, nil\n\t}\n}\n\nfunc (th *tracingHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {\n\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\n\t\t// Check if the command should be filtered out\n\t\tif th.conf.filterProcess != nil && th.conf.filterProcess(cmd) {\n\t\t\t// If so, just call the next hook\n\t\t\treturn hook(ctx, cmd)\n\t\t}\n\n\t\tattrs := make([]attribute.KeyValue, 0, 8)\n\t\tif th.conf.callerEnabled {\n\t\t\tfn, file, line := funcFileLine(\"github.com/redis/go-redis\")\n\t\t\tattrs = append(attrs,\n\t\t\t\tsemconv.CodeFunction(fn),\n\t\t\t\tsemconv.CodeFilepath(file),\n\t\t\t\tsemconv.CodeLineNumber(line),\n\t\t\t)\n\t\t}\n\n\t\tif th.conf.dbStmtEnabled {\n\t\t\tcmdString := rediscmd.CmdString(cmd)\n\t\t\tattrs = append(attrs, semconv.DBStatement(cmdString))\n\t\t}\n\n\t\topts := th.spanOpts\n\t\topts = append(opts, trace.WithAttributes(attrs...))\n\n\t\tctx, span := th.conf.tracer.Start(ctx, cmd.FullName(), opts...)\n\t\tdefer span.End()\n\n\t\tif err := hook(ctx, cmd); err != nil {\n\t\t\trecordError(span, err)\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc (th *tracingHook) ProcessPipelineHook(\n\thook redis.ProcessPipelineHook,\n) redis.ProcessPipelineHook {\n\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\n\t\tif th.conf.filterProcessPipeline != nil && th.conf.filterProcessPipeline(cmds) {\n\t\t\treturn hook(ctx, cmds)\n\t\t}\n\n\t\tattrs := make([]attribute.KeyValue, 0, 8)\n\t\tattrs = append(attrs,\n\t\t\tattribute.Int(\"db.redis.num_cmd\", len(cmds)),\n\t\t)\n\n\t\tif th.conf.callerEnabled {\n\t\t\tfn, file, line := funcFileLine(\"github.com/redis/go-redis\")\n\t\t\tattrs = append(attrs,\n\t\t\t\tsemconv.CodeFunction(fn),\n\t\t\t\tsemconv.CodeFilepath(file),\n\t\t\t\tsemconv.CodeLineNumber(line),\n\t\t\t)\n\t\t}\n\n\t\tsummary, cmdsString := rediscmd.CmdsString(cmds)\n\t\tif th.conf.dbStmtEnabled {\n\t\t\tattrs = append(attrs, semconv.DBStatement(cmdsString))\n\t\t}\n\n\t\topts := th.spanOpts\n\t\topts = append(opts, trace.WithAttributes(attrs...))\n\n\t\tctx, span := th.conf.tracer.Start(ctx, \"redis.pipeline \"+summary, opts...)\n\t\tdefer span.End()\n\n\t\tif err := hook(ctx, cmds); err != nil {\n\t\t\trecordError(span, err)\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc recordError(span trace.Span, err error) {\n\tif err != redis.Nil {\n\t\tspan.RecordError(err)\n\t\tspan.SetStatus(codes.Error, err.Error())\n\t}\n}\n\nfunc formatDBConnString(network, addr string) string {\n\tif network == \"tcp\" {\n\t\tnetwork = \"redis\"\n\t}\n\treturn fmt.Sprintf(\"%s://%s\", network, addr)\n}\n\nfunc funcFileLine(pkg string) (string, string, int) {\n\tconst depth = 16\n\tvar pcs [depth]uintptr\n\tn := runtime.Callers(3, pcs[:])\n\tff := runtime.CallersFrames(pcs[:n])\n\n\tvar fn, file string\n\tvar line int\n\tfor {\n\t\tf, ok := ff.Next()\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tfn, file, line = f.Function, f.File, f.Line\n\t\tif !strings.Contains(fn, pkg) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif ind := strings.LastIndexByte(fn, '/'); ind != -1 {\n\t\tfn = fn[ind+1:]\n\t}\n\n\treturn fn, file, line\n}\n\n// Database span attributes semantic conventions recommended server address and port\n// https://opentelemetry.io/docs/specs/semconv/database/database-spans/#connection-level-attributes\nfunc addServerAttributes(opts []TracingOption, addr string) []TracingOption {\n\thost, portString, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn opts\n\t}\n\n\topts = append(opts, WithAttributes(\n\t\tsemconv.ServerAddress(host),\n\t))\n\n\t// Parse the port string to an integer\n\tport, err := strconv.Atoi(portString)\n\tif err != nil {\n\t\treturn opts\n\t}\n\n\topts = append(opts, WithAttributes(\n\t\tsemconv.ServerPort(port),\n\t))\n\n\treturn opts\n}\n"
  },
  {
    "path": "extra/redisotel/tracing_test.go",
    "content": "package redisotel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/sdk/trace/tracetest\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.7.0\"\n\n\t\"go.opentelemetry.io/otel\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype providerFunc func(name string, opts ...trace.TracerOption) trace.TracerProvider\n\nfunc (fn providerFunc) TracerProvider(name string, opts ...trace.TracerOption) trace.TracerProvider {\n\treturn fn(name, opts...)\n}\n\nfunc TestNewWithTracerProvider(t *testing.T) {\n\tinvoked := false\n\n\ttp := providerFunc(func(name string, opts ...trace.TracerOption) trace.TracerProvider {\n\t\tinvoked = true\n\t\treturn otel.GetTracerProvider()\n\t})\n\n\t_ = newTracingHook(\"redis-hook\", WithTracerProvider(tp.TracerProvider(\"redis-test\")))\n\n\tif !invoked {\n\t\tt.Fatalf(\"did not call custom TraceProvider\")\n\t}\n}\n\nfunc TestWithDBStatement(t *testing.T) {\n\tprovider := sdktrace.NewTracerProvider()\n\thook := newTracingHook(\n\t\t\"\",\n\t\tWithTracerProvider(provider),\n\t\tWithDBStatement(false),\n\t)\n\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\tcmd := redis.NewCmd(ctx, \"ping\")\n\tdefer span.End()\n\n\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\tattrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()\n\t\tfor _, attr := range attrs {\n\t\t\tif attr.Key == semconv.DBStatementKey {\n\t\t\t\tt.Fatal(\"Attribute with db statement should not exist\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\terr := processHook(ctx, cmd)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestWithoutCaller(t *testing.T) {\n\tprovider := sdktrace.NewTracerProvider()\n\thook := newTracingHook(\n\t\t\"\",\n\t\tWithTracerProvider(provider),\n\t\tWithCallerEnabled(false),\n\t)\n\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\tcmd := redis.NewCmd(ctx, \"ping\")\n\tdefer span.End()\n\n\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\tattrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()\n\t\tfor _, attr := range attrs {\n\t\t\tswitch attr.Key {\n\t\t\tcase semconv.CodeFunctionKey,\n\t\t\t\tsemconv.CodeFilepathKey,\n\t\t\t\tsemconv.CodeLineNumberKey:\n\t\t\t\tt.Fatalf(\"Attribute with %s statement should not exist\", attr.Key)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\terr := processHook(ctx, cmd)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestWithCommandFilter(t *testing.T) {\n\n\tt.Run(\"filter out ping command\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithCommandFilter(func(cmd redis.Cmder) bool {\n\t\t\t\treturn cmd.Name() == \"ping\"\n\t\t\t}),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tcmd := redis.NewCmd(ctx, \"ping\")\n\t\tdefer span.End()\n\n\t\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"redis-test\" || innerSpan.Name() == \"ping\" {\n\t\t\t\tt.Fatalf(\"ping command should not be traced\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t\terr := processHook(ctx, cmd)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n\n\tt.Run(\"do not filter ping command\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithCommandFilter(func(cmd redis.Cmder) bool {\n\t\t\t\treturn false // never filter\n\t\t\t}),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tcmd := redis.NewCmd(ctx, \"ping\")\n\t\tdefer span.End()\n\n\t\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"ping\" {\n\t\t\t\tt.Fatalf(\"ping command should be traced\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t\terr := processHook(ctx, cmd)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n\n\tt.Run(\"auth command filtered with basic command filter\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithCommandFilter(DefaultCommandFilter),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tcmd := redis.NewCmd(ctx, \"auth\", \"test-password\")\n\t\tdefer span.End()\n\n\t\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"redis-test\" || innerSpan.Name() == \"auth\" {\n\t\t\t\tt.Fatalf(\"auth command should not be traced by default\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t\terr := processHook(ctx, cmd)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n\n\tt.Run(\"hello command filtered with basic command filter when sensitive\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithCommandFilter(DefaultCommandFilter),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tcmd := redis.NewCmd(ctx, \"hello\", 3, \"AUTH\", \"test-user\", \"test-password\")\n\t\tdefer span.End()\n\n\t\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"redis-test\" || innerSpan.Name() == \"hello\" {\n\t\t\t\tt.Fatalf(\"auth command should not be traced by default\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t\terr := processHook(ctx, cmd)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n\n\tt.Run(\"hello command not filtered with basic command filter when not sensitive\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithCommandFilter(DefaultCommandFilter),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tcmd := redis.NewCmd(ctx, \"hello\", 3)\n\t\tdefer span.End()\n\n\t\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"hello\" {\n\t\t\t\tt.Fatalf(\"hello command should be traced\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t\terr := processHook(ctx, cmd)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n}\n\nfunc TestWithCommandsFilter(t *testing.T) {\n\tt.Run(\"filter out ping and info commands\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithCommandsFilter(func(cmds []redis.Cmder) bool {\n\t\t\t\tfor _, cmd := range cmds {\n\t\t\t\t\tif cmd.Name() == \"ping\" || cmd.Name() == \"info\" {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}),\n\t\t)\n\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tcmds := []redis.Cmder{\n\t\t\tredis.NewCmd(ctx, \"ping\"),\n\t\t\tredis.NewCmd(ctx, \"info\"),\n\t\t}\n\t\tdefer span.End()\n\n\t\tprocessPipelineHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"redis-test\" || innerSpan.Name() == \"redis.pipeline ping\\ninfo\" {\n\t\t\t\tt.Fatalf(\"ping and info commands should not be traced\")\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\terr := processPipelineHook(ctx, cmds)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n\n\tt.Run(\"do not filter ping and info commands\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithCommandsFilter(func(cmds []redis.Cmder) bool {\n\t\t\t\treturn false // never filter\n\t\t\t}),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tcmds := []redis.Cmder{\n\t\t\tredis.NewCmd(ctx, \"ping\"),\n\t\t\tredis.NewCmd(ctx, \"info\"),\n\t\t}\n\t\tdefer span.End()\n\t\tprocessPipelineHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"redis.pipeline ping info\" {\n\t\t\t\tt.Fatalf(\"ping and info commands should be traced\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\t\terr := processPipelineHook(ctx, cmds)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n}\n\nfunc TestWithDialFilter(t *testing.T) {\n\tt.Run(\"filter out dial\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithDialFilter(true),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tdefer span.End()\n\t\tdialHook := hook.DialHook(func(ctx context.Context, network, addr string) (conn net.Conn, err error) {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() == \"redis.dial\" {\n\t\t\t\tt.Fatalf(\"dial should not be traced\")\n\t\t\t}\n\t\t\treturn nil, nil\n\t\t})\n\n\t\t_, err := dialHook(ctx, \"tcp\", \"localhost:6379\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n\n\tt.Run(\"do not filter dial\", func(t *testing.T) {\n\t\tprovider := sdktrace.NewTracerProvider()\n\t\thook := newTracingHook(\n\t\t\t\"\",\n\t\t\tWithTracerProvider(provider),\n\t\t\tWithDialFilter(false),\n\t\t)\n\t\tctx, span := provider.Tracer(\"redis-test\").Start(context.TODO(), \"redis-test\")\n\t\tdefer span.End()\n\t\tdialHook := hook.DialHook(func(ctx context.Context, network, addr string) (conn net.Conn, err error) {\n\t\t\tinnerSpan := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan)\n\t\t\tif innerSpan.Name() != \"redis.dial\" {\n\t\t\t\tt.Fatalf(\"dial should be traced\")\n\t\t\t}\n\t\t\treturn nil, nil\n\t\t})\n\t\t_, err := dialHook(ctx, \"tcp\", \"localhost:6379\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n}\n\nfunc TestTracingHook_DialHook(t *testing.T) {\n\timsb := tracetest.NewInMemoryExporter()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))\n\thook := newTracingHook(\n\t\t\"redis://localhost:6379\",\n\t\tWithTracerProvider(provider),\n\t)\n\n\ttests := []struct {\n\t\tname    string\n\t\terrTest error\n\t}{\n\t\t{\"nil error\", nil},\n\t\t{\"test error\", fmt.Errorf(\"test error\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer imsb.Reset()\n\n\t\t\tdialHook := hook.DialHook(func(ctx context.Context, network, addr string) (conn net.Conn, err error) {\n\t\t\t\treturn nil, tt.errTest\n\t\t\t})\n\t\t\tif _, err := dialHook(context.Background(), \"tcp\", \"localhost:6379\"); err != tt.errTest {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tassertEqual(t, 1, len(imsb.GetSpans()))\n\n\t\t\tspanData := imsb.GetSpans()[0]\n\t\t\tassertEqual(t, instrumName, spanData.InstrumentationLibrary.Name)\n\t\t\tassertEqual(t, \"redis.dial\", spanData.Name)\n\t\t\tassertEqual(t, trace.SpanKindClient, spanData.SpanKind)\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis)\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String(\"redis://localhost:6379\"))\n\n\t\t\tif tt.errTest == nil {\n\t\t\t\tassertEqual(t, 0, len(spanData.Events))\n\t\t\t\tassertEqual(t, codes.Unset, spanData.Status.Code)\n\t\t\t\tassertEqual(t, \"\", spanData.Status.Description)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassertEqual(t, 1, len(spanData.Events))\n\t\t\tassertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String(\"*errors.errorString\"))\n\t\t\tassertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error()))\n\t\t\tassertEqual(t, codes.Error, spanData.Status.Code)\n\t\t\tassertEqual(t, tt.errTest.Error(), spanData.Status.Description)\n\t\t})\n\t}\n}\n\nfunc TestTracingHook_ProcessHook(t *testing.T) {\n\timsb := tracetest.NewInMemoryExporter()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))\n\thook := newTracingHook(\n\t\t\"redis://localhost:6379\",\n\t\tWithTracerProvider(provider),\n\t)\n\n\ttests := []struct {\n\t\tname    string\n\t\terrTest error\n\t}{\n\t\t{\"nil error\", nil},\n\t\t{\"test error\", fmt.Errorf(\"test error\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer imsb.Reset()\n\n\t\t\tcmd := redis.NewCmd(context.Background(), \"ping\")\n\t\t\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\treturn tt.errTest\n\t\t\t})\n\t\t\tassertEqual(t, tt.errTest, processHook(context.Background(), cmd))\n\t\t\tassertEqual(t, 1, len(imsb.GetSpans()))\n\n\t\t\tspanData := imsb.GetSpans()[0]\n\t\t\tassertEqual(t, instrumName, spanData.InstrumentationLibrary.Name)\n\t\t\tassertEqual(t, \"ping\", spanData.Name)\n\t\t\tassertEqual(t, trace.SpanKindClient, spanData.SpanKind)\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis)\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String(\"redis://localhost:6379\"))\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String(\"ping\"))\n\n\t\t\tif tt.errTest == nil {\n\t\t\t\tassertEqual(t, 0, len(spanData.Events))\n\t\t\t\tassertEqual(t, codes.Unset, spanData.Status.Code)\n\t\t\t\tassertEqual(t, \"\", spanData.Status.Description)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassertEqual(t, 1, len(spanData.Events))\n\t\t\tassertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String(\"*errors.errorString\"))\n\t\t\tassertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error()))\n\t\t\tassertEqual(t, codes.Error, spanData.Status.Code)\n\t\t\tassertEqual(t, tt.errTest.Error(), spanData.Status.Description)\n\t\t})\n\t}\n}\n\nfunc TestTracingHook_ProcessPipelineHook(t *testing.T) {\n\timsb := tracetest.NewInMemoryExporter()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))\n\thook := newTracingHook(\n\t\t\"redis://localhost:6379\",\n\t\tWithTracerProvider(provider),\n\t)\n\n\ttests := []struct {\n\t\tname    string\n\t\terrTest error\n\t}{\n\t\t{\"nil error\", nil},\n\t\t{\"test error\", fmt.Errorf(\"test error\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer imsb.Reset()\n\n\t\t\tcmds := []redis.Cmder{\n\t\t\t\tredis.NewCmd(context.Background(), \"ping\"),\n\t\t\t\tredis.NewCmd(context.Background(), \"ping\"),\n\t\t\t}\n\t\t\tprocessHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\treturn tt.errTest\n\t\t\t})\n\t\t\tassertEqual(t, tt.errTest, processHook(context.Background(), cmds))\n\t\t\tassertEqual(t, 1, len(imsb.GetSpans()))\n\n\t\t\tspanData := imsb.GetSpans()[0]\n\t\t\tassertEqual(t, instrumName, spanData.InstrumentationLibrary.Name)\n\t\t\tassertEqual(t, \"redis.pipeline ping\", spanData.Name)\n\t\t\tassertEqual(t, trace.SpanKindClient, spanData.SpanKind)\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis)\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String(\"redis://localhost:6379\"))\n\t\t\tassertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String(\"ping\\nping\"))\n\n\t\t\tif tt.errTest == nil {\n\t\t\t\tassertEqual(t, 0, len(spanData.Events))\n\t\t\t\tassertEqual(t, codes.Unset, spanData.Status.Code)\n\t\t\t\tassertEqual(t, \"\", spanData.Status.Description)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassertEqual(t, 1, len(spanData.Events))\n\t\t\tassertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String(\"*errors.errorString\"))\n\t\t\tassertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error()))\n\t\t\tassertEqual(t, codes.Error, spanData.Status.Code)\n\t\t\tassertEqual(t, tt.errTest.Error(), spanData.Status.Description)\n\t\t})\n\t}\n}\n\nfunc TestTracingHook_ProcessHook_LongCommand(t *testing.T) {\n\timsb := tracetest.NewInMemoryExporter()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))\n\thook := newTracingHook(\n\t\t\"redis://localhost:6379\",\n\t\tWithTracerProvider(provider),\n\t)\n\tlongValue := strings.Repeat(\"a\", 102400)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcmd      redis.Cmder\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"short command\",\n\t\t\tcmd:      redis.NewCmd(context.Background(), \"SET\", \"key\", \"value\"),\n\t\t\texpected: \"SET key value\",\n\t\t},\n\t\t{\n\t\t\tname:     \"set command with long key\",\n\t\t\tcmd:      redis.NewCmd(context.Background(), \"SET\", longValue, \"value\"),\n\t\t\texpected: \"SET \" + longValue + \" value\",\n\t\t},\n\t\t{\n\t\t\tname:     \"set command with long value\",\n\t\t\tcmd:      redis.NewCmd(context.Background(), \"SET\", \"key\", longValue),\n\t\t\texpected: \"SET key \" + longValue,\n\t\t},\n\t\t{\n\t\t\tname:     \"set command with long key and value\",\n\t\t\tcmd:      redis.NewCmd(context.Background(), \"SET\", longValue, longValue),\n\t\t\texpected: \"SET \" + longValue + \" \" + longValue,\n\t\t},\n\t\t{\n\t\t\tname:     \"short command with many arguments\",\n\t\t\tcmd:      redis.NewCmd(context.Background(), \"MSET\", \"key1\", \"value1\", \"key2\", \"value2\", \"key3\", \"value3\", \"key4\", \"value4\", \"key5\", \"value5\"),\n\t\t\texpected: \"MSET key1 value1 key2 value2 key3 value3 key4 value4 key5 value5\",\n\t\t},\n\t\t{\n\t\t\tname:     \"long command\",\n\t\t\tcmd:      redis.NewCmd(context.Background(), longValue, \"key\", \"value\"),\n\t\t\texpected: longValue + \" key value\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer imsb.Reset()\n\n\t\t\tprocessHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tif err := processHook(context.Background(), tt.cmd); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tassertEqual(t, 1, len(imsb.GetSpans()))\n\n\t\t\tspanData := imsb.GetSpans()[0]\n\n\t\t\tvar dbStatement string\n\t\t\tfor _, attr := range spanData.Attributes {\n\t\t\t\tif attr.Key == semconv.DBStatementKey {\n\t\t\t\t\tdbStatement = attr.Value.AsString()\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif dbStatement != tt.expected {\n\t\t\t\tt.Errorf(\"Expected DB statement: %q\\nGot: %q\", tt.expected, dbStatement)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTracingHook_ProcessPipelineHook_LongCommands(t *testing.T) {\n\timsb := tracetest.NewInMemoryExporter()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb))\n\thook := newTracingHook(\n\t\t\"redis://localhost:6379\",\n\t\tWithTracerProvider(provider),\n\t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcmds     []redis.Cmder\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"multiple short commands\",\n\t\t\tcmds: []redis.Cmder{\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", \"key1\", \"value1\"),\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", \"key2\", \"value2\"),\n\t\t\t},\n\t\t\texpected: \"SET key1 value1\\nSET key2 value2\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple short commands with long key\",\n\t\t\tcmds: []redis.Cmder{\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", strings.Repeat(\"a\", 102400), \"value1\"),\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", strings.Repeat(\"b\", 102400), \"value2\"),\n\t\t\t},\n\t\t\texpected: \"SET \" + strings.Repeat(\"a\", 102400) + \" value1\\nSET \" + strings.Repeat(\"b\", 102400) + \" value2\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple short commands with long value\",\n\t\t\tcmds: []redis.Cmder{\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", \"key1\", strings.Repeat(\"a\", 102400)),\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", \"key2\", strings.Repeat(\"b\", 102400)),\n\t\t\t},\n\t\t\texpected: \"SET key1 \" + strings.Repeat(\"a\", 102400) + \"\\nSET key2 \" + strings.Repeat(\"b\", 102400),\n\t\t},\n\t\t{\n\t\t\tname: \"multiple short commands with long key and value\",\n\t\t\tcmds: []redis.Cmder{\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", strings.Repeat(\"a\", 102400), strings.Repeat(\"b\", 102400)),\n\t\t\t\tredis.NewCmd(context.Background(), \"SET\", strings.Repeat(\"c\", 102400), strings.Repeat(\"d\", 102400)),\n\t\t\t},\n\t\t\texpected: \"SET \" + strings.Repeat(\"a\", 102400) + \" \" + strings.Repeat(\"b\", 102400) + \"\\nSET \" + strings.Repeat(\"c\", 102400) + \" \" + strings.Repeat(\"d\", 102400),\n\t\t},\n\t\t{\n\t\t\tname: \"multiple long commands\",\n\t\t\tcmds: []redis.Cmder{\n\t\t\t\tredis.NewCmd(context.Background(), strings.Repeat(\"a\", 102400), \"key1\", \"value1\"),\n\t\t\t\tredis.NewCmd(context.Background(), strings.Repeat(\"a\", 102400), \"key2\", \"value2\"),\n\t\t\t},\n\t\t\texpected: strings.Repeat(\"a\", 102400) + \" key1 value1\\n\" + strings.Repeat(\"a\", 102400) + \" key2 value2\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer imsb.Reset()\n\n\t\t\tprocessHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tif err := processHook(context.Background(), tt.cmds); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tassertEqual(t, 1, len(imsb.GetSpans()))\n\n\t\t\tspanData := imsb.GetSpans()[0]\n\n\t\t\tvar dbStatement string\n\t\t\tfor _, attr := range spanData.Attributes {\n\t\t\t\tif attr.Key == semconv.DBStatementKey {\n\t\t\t\t\tdbStatement = attr.Value.AsString()\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif dbStatement != tt.expected {\n\t\t\t\tt.Errorf(\"Expected DB statement:\\n%q\\nGot:\\n%q\", tt.expected, dbStatement)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc assertEqual(t *testing.T, expected, actual interface{}) {\n\tt.Helper()\n\tif expected != actual {\n\t\tt.Fatalf(\"expected %v, got %v\", expected, actual)\n\t}\n}\n\nfunc assertAttributeContains(t *testing.T, attrs []attribute.KeyValue, attr attribute.KeyValue) {\n\tt.Helper()\n\tfor _, a := range attrs {\n\t\tif a == attr {\n\t\t\treturn\n\t\t}\n\t}\n\tt.Fatalf(\"attribute %v not found\", attr)\n}\n"
  },
  {
    "path": "extra/redisotel-native/attributes.go",
    "content": "package redisotel\n\n// OpenTelemetry semantic convention attribute keys for Redis client metrics.\n// These constants follow the OTel semantic conventions for database clients.\n// Reference: https://opentelemetry.io/docs/specs/semconv/database/redis/\n\nconst (\n\t// Database semantic convention attributes\n\tAttrDBSystemName         = \"db.system.name\"\n\tAttrDBNamespace          = \"db.namespace\"\n\tAttrDBOperationName      = \"db.operation.name\"\n\tAttrDBOperationBatchSize = \"db.operation.batch.size\"\n\tAttrDBResponseStatusCode = \"db.response.status_code\"\n\n\t// Connection pool attributes\n\tAttrDBClientConnectionPoolName = \"db.client.connection.pool.name\"\n\tAttrDBClientConnectionState    = \"db.client.connection.state\"\n\n\t// Server attributes\n\tAttrServerAddress = \"server.address\"\n\tAttrServerPort    = \"server.port\"\n\n\t// Network attributes\n\tAttrNetworkPeerAddress = \"network.peer.address\"\n\tAttrNetworkPeerPort    = \"network.peer.port\"\n\n\t// Error attributes\n\tAttrErrorType = \"error.type\"\n\n\t// Redis-specific attributes\n\tAttrRedisClientLibrary                = \"redis.client.library\"\n\tAttrRedisClientConnectionPubSub       = \"redis.client.connection.pubsub\"\n\tAttrRedisClientConnectionCloseReason  = \"redis.client.connection.close.reason\"\n\tAttrRedisClientErrorsCategory         = \"redis.client.errors.category\"\n\tAttrRedisClientErrorsInternal         = \"redis.client.errors.internal\"\n\tAttrRedisClientOperationRetryAttempts = \"redis.client.operation.retry_attempts\"\n\n\t// PubSub attributes\n\tAttrRedisClientPubSubDirection = \"redis.client.pubsub.direction\"\n\tAttrRedisClientPubSubSharded   = \"redis.client.pubsub.sharded\"\n\tAttrRedisClientPubSubChannel   = \"redis.client.pubsub.channel\"\n\n\t// Stream attributes\n\tAttrRedisClientStreamName          = \"redis.client.stream.name\"\n\tAttrRedisClientStreamConsumerGroup = \"redis.client.stream.consumer_group\"\n\tAttrRedisClientStreamConsumerName  = \"redis.client.stream.consumer_name\"\n\n\t// Notification attributes\n\tAttrRedisClientConnectionNotification = \"redis.client.connection.notification\"\n)\n\n// Connection state values\nconst (\n\tConnectionStateIdle = \"idle\"\n\tConnectionStateUsed = \"used\"\n)\n\n// PubSub direction values\nconst (\n\tPubSubDirectionSent     = \"sent\"\n\tPubSubDirectionReceived = \"received\"\n)\n\n// DB system value\nconst (\n\tDBSystemRedis = \"redis\"\n)\n"
  },
  {
    "path": "extra/redisotel-native/config.go",
    "content": "package redisotel\n\nimport (\n\t\"strings\"\n\n\t\"go.opentelemetry.io/otel/metric\"\n)\n\ntype MetricGroup string\n\nconst (\n\tMetricGroupCommand            MetricGroup = \"command\"\n\tMetricGroupConnectionBasic    MetricGroup = \"connection-basic\"\n\tMetricGroupResiliency         MetricGroup = \"resiliency\"\n\tMetricGroupConnectionAdvanced MetricGroup = \"connection-advanced\"\n\tMetricGroupPubSub             MetricGroup = \"pubsub\"\n\tMetricGroupStream             MetricGroup = \"stream\"\n)\n\ntype HistogramAggregation string\n\nconst (\n\tHistogramAggregationExplicitBucket   HistogramAggregation = \"explicit_bucket_histogram\"\n\tHistogramAggregationBase2Exponential HistogramAggregation = \"base2_exponential_bucket_histogram\"\n)\n\ntype config struct {\n\t// Core settings\n\tmeterProvider metric.MeterProvider\n\tenabled       bool\n\n\t// Metric group settings\n\tenabledMetricGroups map[MetricGroup]bool\n\n\t// Command filtering\n\tincludeCommands map[string]bool // nil means include all\n\texcludeCommands map[string]bool // nil means exclude none\n\n\t// Cardinality reduction\n\thidePubSubChannelNames bool\n\thideStreamNames        bool\n\n\t// Histogram settings\n\thistAggregation HistogramAggregation\n\n\t// Bucket configurations for different histogram metrics\n\tbucketsOperationDuration        []float64\n\tbucketsStreamProcessingDuration []float64\n\tbucketsConnectionCreateTime     []float64\n\tbucketsConnectionWaitTime       []float64\n}\n\nfunc (c *config) isMetricGroupEnabled(group MetricGroup) bool {\n\treturn c.enabledMetricGroups[group]\n}\n\nfunc (c *config) isCommandIncluded(command string) bool {\n\tcommand = strings.ToLower(command)\n\tif c.excludeCommands != nil && c.excludeCommands[command] {\n\t\treturn false\n\t}\n\n\tif c.includeCommands != nil {\n\t\treturn c.includeCommands[command]\n\t}\n\n\treturn true\n}\n\n// defaultHistogramBuckets returns the default histogram buckets for all duration metrics.\n// These buckets are designed to capture typical Redis operation and connection latencies:\n// - Sub-millisecond: 0.0001s (0.1ms), 0.0005s (0.5ms)\n// - Milliseconds: 0.001s (1ms), 0.005s (5ms), 0.01s (10ms), 0.05s (50ms), 0.1s (100ms)\n// - Sub-second: 0.5s (500ms)\n// - Seconds: 1s, 5s, 10s\n//\n// This covers the range from 0.1ms to 10s, which is suitable for:\n// - db.client.operation.duration (command execution time)\n// - db.client.connection.create_time (connection establishment)\n// - db.client.connection.wait_time (waiting for connection from pool)\n// - redis.client.stream.processing_duration (stream message processing)\nfunc defaultHistogramBuckets() []float64 {\n\treturn []float64{\n\t\t0.0001, // 0.1ms\n\t\t0.0005, // 0.5ms\n\t\t0.001,  // 1ms\n\t\t0.005,  // 5ms\n\t\t0.01,   // 10ms\n\t\t0.05,   // 50ms\n\t\t0.1,    // 100ms\n\t\t0.5,    // 500ms\n\t\t1.0,    // 1s\n\t\t5.0,    // 5s\n\t\t10.0,   // 10s\n\t}\n}\n\n// MetricGroupFlags represents metric groups as bitwise flags\ntype MetricGroupFlags uint32\n\nconst (\n\tMetricGroupFlagCommand            MetricGroupFlags = 1 << 0\n\tMetricGroupFlagConnectionBasic    MetricGroupFlags = 1 << 1\n\tMetricGroupFlagResiliency         MetricGroupFlags = 1 << 2\n\tMetricGroupFlagConnectionAdvanced MetricGroupFlags = 1 << 3\n\tMetricGroupFlagPubSub             MetricGroupFlags = 1 << 4\n\tMetricGroupFlagStream             MetricGroupFlags = 1 << 5\n\n\t// MetricGroupAll enables all metric groups\n\tMetricGroupAll MetricGroupFlags = MetricGroupFlagCommand |\n\t\tMetricGroupFlagConnectionBasic |\n\t\tMetricGroupFlagResiliency |\n\t\tMetricGroupFlagConnectionAdvanced |\n\t\tMetricGroupFlagPubSub |\n\t\tMetricGroupFlagStream\n)\n\n// Use NewConfig() to create a new instance with defaults, then chain\n// builder methods to customize.\n//\n// Example:\n//\n//\tconfig := redisotel.NewConfig().\n//\t    WithEnabled(true).\n//\t    WithMetricGroups(redisotel.MetricGroupAll).\n//\t    WithMeterProvider(myProvider)\n//\n//\totel := redisotel.GetObservabilityInstance()\n//\totel.Init(config)\ntype Config struct {\n\t// Core settings\n\tEnabled       bool\n\tMeterProvider metric.MeterProvider\n\n\t// Metric groups (bitwise flags)\n\tMetricGroups MetricGroupFlags\n\n\t// Command filtering\n\tIncludeCommands map[string]bool // nil means include all\n\tExcludeCommands map[string]bool // nil means exclude none\n\n\t// Cardinality reduction\n\tHidePubSubChannelNames bool\n\tHideStreamNames        bool\n\n\t// Histogram settings\n\tHistogramAggregation HistogramAggregation\n\n\t// Bucket configurations for different histogram metrics\n\tBucketsOperationDuration    []float64\n\tBucketsStreamLag            []float64\n\tBucketsConnectionCreateTime []float64\n\tBucketsConnectionWaitTime   []float64\n}\n\n// NewConfig creates a new Config with default values.\n// Default configuration:\n// - Enabled: false (must explicitly enable)\n// - MetricGroups: all metric groups (command, connection, resiliency, pubsub, stream)\n// - HistogramAggregation: explicit bucket\n// - Buckets: 0.1ms to 10s (suitable for Redis operations)\n//\n// Example:\n//\n//\tconfig := redisotel.NewConfig().\n//\t    WithEnabled(true)\n//\n// To disable specific metric groups, use WithMetricGroups:\n//\n//\tconfig := redisotel.NewConfig().\n//\t    WithEnabled(true).\n//\t    WithMetricGroups(redisotel.MetricGroupFlagConnectionBasic | redisotel.MetricGroupFlagResiliency)\nfunc NewConfig() *Config {\n\treturn &Config{\n\t\tEnabled:       false,\n\t\tMeterProvider: nil, // Will use global otel.GetMeterProvider() if nil\n\n\t\t// Default metric groups: all groups enabled for comprehensive observability\n\t\tMetricGroups: MetricGroupAll,\n\n\t\t// No command filtering by default\n\t\tIncludeCommands: nil,\n\t\tExcludeCommands: nil,\n\n\t\t// Don't hide labels by default\n\t\tHidePubSubChannelNames: false,\n\t\tHideStreamNames:        false,\n\n\t\t// Use explicit bucket histogram by default\n\t\tHistogramAggregation: HistogramAggregationExplicitBucket,\n\n\t\t// Default buckets for all duration metrics\n\t\tBucketsOperationDuration:    defaultHistogramBuckets(),\n\t\tBucketsStreamLag:            defaultHistogramBuckets(),\n\t\tBucketsConnectionCreateTime: defaultHistogramBuckets(),\n\t\tBucketsConnectionWaitTime:   defaultHistogramBuckets(),\n\t}\n}\n\n// WithEnabled enables or disables metrics emission.\n// Default: false (must explicitly enable)\nfunc (c *Config) WithEnabled(enabled bool) *Config {\n\tc.Enabled = enabled\n\treturn c\n}\n\n// WithMeterProvider sets the meter provider to use for creating metrics.\n// If not provided, the global meter provider from otel.GetMeterProvider() will be used.\nfunc (c *Config) WithMeterProvider(provider metric.MeterProvider) *Config {\n\tc.MeterProvider = provider\n\treturn c\n}\n\n// WithMetricGroups sets which metric groups to register using bitwise flags.\n// You can combine multiple groups with the | operator.\nfunc (c *Config) WithMetricGroups(groups MetricGroupFlags) *Config {\n\tc.MetricGroups = groups\n\treturn c\n}\n\n// WithIncludeCommands sets a command allow-list for metrics.\nfunc (c *Config) WithIncludeCommands(commands []string) *Config {\n\tc.IncludeCommands = make(map[string]bool)\n\tfor _, cmd := range commands {\n\t\tc.IncludeCommands[strings.ToLower(cmd)] = true\n\t}\n\treturn c\n}\n\n// WithExcludeCommands sets a command deny-list for metrics.\n// Commands in this list will not have metrics recorded.\nfunc (c *Config) WithExcludeCommands(commands []string) *Config {\n\tc.ExcludeCommands = make(map[string]bool)\n\tfor _, cmd := range commands {\n\t\tc.ExcludeCommands[strings.ToLower(cmd)] = true\n\t}\n\treturn c\n}\n\n// WithHidePubSubChannelNames omits channel label from Pub/Sub metrics to reduce cardinality.\nfunc (c *Config) WithHidePubSubChannelNames(hide bool) *Config {\n\tc.HidePubSubChannelNames = hide\n\treturn c\n}\n\n// WithHideStreamNames omits stream label from stream metrics to reduce cardinality.\nfunc (c *Config) WithHideStreamNames(hide bool) *Config {\n\tc.HideStreamNames = hide\n\treturn c\n}\n\n// WithHistogramAggregation sets the histogram aggregation mode.\nfunc (c *Config) WithHistogramAggregation(agg HistogramAggregation) *Config {\n\tc.HistogramAggregation = agg\n\treturn c\n}\n\n// WithHistogramBuckets sets custom histogram buckets for ALL duration metrics.\n// If not set, uses defaultHistogramBuckets() which covers 0.1ms to 10s.\n// Buckets should be in seconds (e.g., 0.001 = 1ms, 0.1 = 100ms, 1.0 = 1s).\nfunc (c *Config) WithHistogramBuckets(buckets []float64) *Config {\n\tc.BucketsOperationDuration = buckets\n\tc.BucketsStreamLag = buckets\n\tc.BucketsConnectionCreateTime = buckets\n\tc.BucketsConnectionWaitTime = buckets\n\treturn c\n}\n"
  },
  {
    "path": "extra/redisotel-native/go.mod",
    "content": "module github.com/redis/go-redis/extra/redisotel-native/v9\n\ngo 1.24.0\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire (\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgo.opentelemetry.io/otel v1.40.0\n\tgo.opentelemetry.io/otel/metric v1.40.0\n\tgo.opentelemetry.io/otel/sdk/metric v1.40.0\n)\n\nrequire (\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/sys v0.40.0 // indirect\n)\n"
  },
  {
    "path": "extra/redisotel-native/go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "extra/redisotel-native/metrics.go",
    "content": "package redisotel\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/metric\"\n)\n\n// timeoutError is an interface for errors that have a Timeout() method\ntype timeoutError interface {\n\tTimeout() bool\n}\n\nconst (\n\t// Library name for redis.client.library attribute\n\tlibraryName = \"go-redis\"\n)\n\n// getLibraryVersionAttr returns the redis.client.library attribute\nfunc getLibraryVersionAttr() attribute.KeyValue {\n\treturn attribute.String(AttrRedisClientLibrary, fmt.Sprintf(\"%s:%s\", libraryName, redis.Version()))\n}\n\n// addServerPortIfNonDefault adds server.port attribute if port is not the default (6379)\nfunc addServerPortIfNonDefault(attrs []attribute.KeyValue, serverPort string) []attribute.KeyValue {\n\tif serverPort != \"\" && serverPort != \"6379\" {\n\t\treturn append(attrs, attribute.String(AttrServerPort, serverPort))\n\t}\n\treturn attrs\n}\n\n// poolInfo stores information about a registered main connection pool\ntype poolInfo struct {\n\tname string\n\tpool redis.Pooler\n}\n\n// pubsubPoolInfo stores information about a registered PubSub pool\ntype pubsubPoolInfo struct {\n\tname string\n\tpool redis.PubSubPooler\n}\n\n// metricsRecorder implements the otel.Recorder interface\ntype metricsRecorder struct {\n\toperationDuration        metric.Float64Histogram\n\tconnectionCountGauge     metric.Int64ObservableGauge\n\tconnectionCreateTime     metric.Float64Histogram\n\tconnectionRelaxedTimeout metric.Int64UpDownCounter\n\tconnectionHandoff        metric.Int64Counter\n\tclientErrors             metric.Int64Counter\n\tmaintenanceNotifications metric.Int64Counter\n\n\tconnectionWaitTime         metric.Float64Histogram\n\tconnectionClosed           metric.Int64Counter\n\tconnectionPendingReqsGauge metric.Int64ObservableGauge\n\n\tpubsubMessages metric.Int64Counter\n\n\tstreamLag metric.Float64Histogram\n\n\t// Configuration\n\tcfg *config\n\n\t// Pool registry for tracking multiple pools\n\tpoolsMu     sync.RWMutex\n\tpools       []poolInfo\n\tpubsubPools []pubsubPoolInfo\n}\n\n// RecordOperationDuration records db.client.operation.duration metric\nfunc (r *metricsRecorder) RecordOperationDuration(\n\tctx context.Context,\n\tduration time.Duration,\n\tcmd redis.Cmder,\n\tattempts int,\n\terr error,\n\tcn redis.ConnInfo,\n\tdbIndex int,\n) {\n\tif r.operationDuration == nil {\n\t\treturn\n\t}\n\n\t// Check if command should be included\n\tif r.cfg != nil && !r.cfg.isCommandIncluded(cmd.Name()) {\n\t\treturn\n\t}\n\n\t// Convert duration to seconds (OTel convention for duration metrics)\n\tdurationSeconds := duration.Seconds()\n\n\tserverAddr, serverPort := extractServerInfo(cn)\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\t// Required attributes\n\t\tattribute.String(AttrDBOperationName, cmd.FullName()),\n\t\tgetLibraryVersionAttr(),\n\t\tattribute.Int(AttrRedisClientOperationRetryAttempts, attempts-1), // attempts-1 = retry count\n\n\t\t// Recommended attributes\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tattribute.String(AttrServerAddress, serverAddr),\n\t\tattribute.String(AttrDBNamespace, strconv.Itoa(dbIndex)),\n\t}\n\n\t// Add server.port if not default\n\tattrs = addServerPortIfNonDefault(attrs, serverPort)\n\n\t// Add network.peer.address and network.peer.port from connection\n\tif cn != nil {\n\t\tremoteAddr := cn.RemoteAddr()\n\t\tif remoteAddr != nil {\n\t\t\tpeerAddr, peerPort := splitHostPort(remoteAddr.String())\n\t\t\tif peerAddr != \"\" {\n\t\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))\n\t\t\t}\n\t\t\tif peerPort != \"\" {\n\t\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))\n\t\t\t}\n\t\t}\n\t}\n\n\tif err != nil {\n\t\tattrs = append(attrs, attribute.String(AttrErrorType, classifyError(err)))\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategory(err)))\n\t\tif statusCode := extractRedisErrorPrefix(err); statusCode != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrDBResponseStatusCode, statusCode))\n\t\t}\n\t}\n\n\t// Record the histogram\n\tr.operationDuration.Record(ctx, durationSeconds, metric.WithAttributes(attrs...))\n}\n\n// RecordPipelineOperationDuration records db.client.operation.duration metric for pipelines/transactions.\n// operationName should be \"PIPELINE\" for regular pipelines or \"MULTI\" for transactions.\nfunc (r *metricsRecorder) RecordPipelineOperationDuration(\n\tctx context.Context,\n\tduration time.Duration,\n\toperationName string,\n\tcmdCount int,\n\tattempts int,\n\terr error,\n\tcn redis.ConnInfo,\n\tdbIndex int,\n) {\n\tif r.operationDuration == nil {\n\t\treturn\n\t}\n\n\t// Convert duration to seconds (OTel convention for duration metrics)\n\tdurationSeconds := duration.Seconds()\n\n\t// Extract server info from connection\n\tserverAddr, serverPort := extractServerInfo(cn)\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBOperationName, operationName),\n\t\tgetLibraryVersionAttr(),\n\t\tattribute.Int(AttrRedisClientOperationRetryAttempts, attempts-1), // attempts-1 = retry count\n\t\tattribute.Int(AttrDBOperationBatchSize, cmdCount),                // number of commands in pipeline\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tattribute.String(AttrServerAddress, serverAddr),\n\t\tattribute.String(AttrDBNamespace, strconv.Itoa(dbIndex)),\n\t}\n\n\t// Add server.port if not default\n\tattrs = addServerPortIfNonDefault(attrs, serverPort)\n\n\t// Add network.peer.address and network.peer.port from connection\n\tif cn != nil {\n\t\tremoteAddr := cn.RemoteAddr()\n\t\tif remoteAddr != nil {\n\t\t\tpeerAddr, peerPort := splitHostPort(remoteAddr.String())\n\t\t\tif peerAddr != \"\" {\n\t\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))\n\t\t\t}\n\t\t\tif peerPort != \"\" {\n\t\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Add error attributes if pipeline failed\n\tif err != nil {\n\t\tattrs = append(attrs, attribute.String(AttrErrorType, classifyError(err)))\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategory(err)))\n\t\tif statusCode := extractRedisErrorPrefix(err); statusCode != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrDBResponseStatusCode, statusCode))\n\t\t}\n\t}\n\n\t// Record the histogram\n\tr.operationDuration.Record(ctx, durationSeconds, metric.WithAttributes(attrs...))\n}\n\n// classifyError returns the error.type attribute value\n// Format: <category>:<subcategory>:<error_name>\nfunc classifyError(err error) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\t// Timeout errors\n\tif isTimeoutError(err) {\n\t\treturn \"timeout\"\n\t}\n\n\t// Network errors - normalize to avoid high cardinality\n\tif isNetworkError(err) {\n\t\treturn normalizeNetworkError(err)\n\t}\n\n\t// Redis errors (start with error prefix like ERR, WRONGTYPE, etc.)\n\tif prefix := extractRedisErrorPrefix(err); prefix != \"\" {\n\t\treturn fmt.Sprintf(\"redis:%s\", prefix)\n\t}\n\n\t// Generic error - normalize to avoid high cardinality\n\treturn normalizeGenericError(err.Error())\n}\n\n// normalizeNetworkError normalizes network error messages to prevent high cardinality\n// by removing variable data like port numbers and connection details\nfunc normalizeNetworkError(err error) string {\n\tif err == nil {\n\t\treturn \"network:unknown\"\n\t}\n\n\terrStr := strings.ToLower(err.Error())\n\n\t// I/O timeout errors\n\tif strings.Contains(errStr, \"i/o timeout\") {\n\t\treturn \"network:io_timeout\"\n\t}\n\n\t// Connection refused errors\n\tif strings.Contains(errStr, \"connection refused\") {\n\t\treturn \"network:connection_refused\"\n\t}\n\n\t// Connection reset errors\n\tif strings.Contains(errStr, \"connection reset\") {\n\t\treturn \"network:connection_reset\"\n\t}\n\n\t// EOF errors\n\tif strings.Contains(errStr, \"eof\") {\n\t\treturn \"network:eof\"\n\t}\n\n\t// Broken pipe\n\tif strings.Contains(errStr, \"broken pipe\") {\n\t\treturn \"network:broken_pipe\"\n\t}\n\n\t// Connection closed\n\tif strings.Contains(errStr, \"use of closed\") || strings.Contains(errStr, \"connection closed\") {\n\t\treturn \"network:connection_closed\"\n\t}\n\n\t// DNS/hostname errors\n\tif strings.Contains(errStr, \"no such host\") || strings.Contains(errStr, \"lookup\") {\n\t\treturn \"network:dns_error\"\n\t}\n\n\t// Default to generic network error\n\treturn \"network:other\"\n}\n\n// normalizeGenericError normalizes generic error messages to prevent high cardinality\nfunc normalizeGenericError(errStr string) string {\n\t// Remove common variable patterns\n\t// Port numbers, IP addresses, etc. would create high cardinality\n\terrLower := strings.ToLower(errStr)\n\n\t// Context deadline exceeded\n\tif strings.Contains(errLower, \"context deadline exceeded\") {\n\t\treturn \"context_deadline_exceeded\"\n\t}\n\n\t// Context canceled\n\tif strings.Contains(errLower, \"context canceled\") {\n\t\treturn \"context_canceled\"\n\t}\n\n\t// Pool errors\n\tif strings.Contains(errLower, \"pool\") {\n\t\tif strings.Contains(errLower, \"timeout\") {\n\t\t\treturn \"pool_timeout\"\n\t\t}\n\t\tif strings.Contains(errLower, \"closed\") {\n\t\t\treturn \"pool_closed\"\n\t\t}\n\t\treturn \"pool_error\"\n\t}\n\n\t// Return first word if it looks like a category, otherwise return \"other\"\n\tparts := strings.SplitN(errStr, \" \", 2)\n\tif len(parts) > 0 && len(parts[0]) > 0 && len(parts[0]) <= 30 {\n\t\t// Only use the first word if it doesn't contain variable data\n\t\tfirstWord := strings.TrimSuffix(strings.TrimSuffix(parts[0], \":\"), \",\")\n\t\tif !strings.ContainsAny(firstWord, \"0123456789./[]\") {\n\t\t\treturn strings.ToLower(firstWord)\n\t\t}\n\t}\n\n\treturn \"other\"\n}\n\n// extractRedisErrorPrefix extracts the Redis error prefix (e.g., \"ERR\", \"WRONGTYPE\")\n// Redis errors typically start with an uppercase prefix followed by a space\nfunc extractRedisErrorPrefix(err error) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := err.Error()\n\n\t// Redis errors typically start with an uppercase prefix\n\t// Examples: \"ERR ...\", \"WRONGTYPE ...\", \"CLUSTERDOWN ...\"\n\tparts := strings.SplitN(errStr, \" \", 2)\n\tif len(parts) > 0 {\n\t\tprefix := parts[0]\n\t\t// Check if it's all uppercase (Redis error convention)\n\t\tif prefix == strings.ToUpper(prefix) && len(prefix) > 0 {\n\t\t\treturn prefix\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// isNetworkError checks if an error is a network-related error\nfunc isNetworkError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\t// Check for net.Error interface (standard way to detect network errors)\n\t_, ok := err.(net.Error)\n\treturn ok\n}\n\n// isTimeoutError checks if an error is a timeout error\nfunc isTimeoutError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\t// Check for timeoutError interface (works with wrapped errors)\n\tvar te timeoutError\n\tif errors.As(err, &te) {\n\t\treturn te.Timeout()\n\t}\n\n\t// Check for net.Error specifically (common case for network timeouts)\n\tvar netErr net.Error\n\tif errors.As(err, &netErr) {\n\t\treturn netErr.Timeout()\n\t}\n\n\treturn false\n}\n\n// getErrorCategory returns the error category from an error.\n// It checks for TLS, auth, network, and server errors.\nfunc getErrorCategory(err error) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\terrStr := err.Error()\n\n\t// For actual errors, also check error types\n\tif isNetworkError(err) || isTimeoutError(err) {\n\t\treturn \"network\"\n\t}\n\n\t// Check for Redis server errors by prefix\n\tif prefix := extractRedisErrorPrefix(err); prefix != \"\" {\n\t\treturn \"server\"\n\t}\n\n\treturn getErrorCategoryFromString(errStr)\n}\n\n// getErrorCategoryFromString returns the error category from an error string or error type.\n// This is used both by getErrorCategory (for errors) and directly for error type strings.\nfunc getErrorCategoryFromString(errStr string) string {\n\tif errStr == \"\" {\n\t\treturn \"\"\n\t}\n\n\terrLower := strings.ToLower(errStr)\n\n\t// Check for TLS errors\n\tif strings.Contains(errLower, \"tls\") || strings.Contains(errLower, \"certificate\") ||\n\t\tstrings.Contains(errLower, \"x509\") || strings.Contains(errLower, \"ssl\") {\n\t\treturn \"tls\"\n\t}\n\n\t// Check for auth errors\n\tif strings.Contains(errLower, \"auth\") || strings.Contains(errLower, \"noauth\") ||\n\t\tstrings.Contains(errLower, \"wrongpass\") || strings.Contains(errLower, \"noperm\") ||\n\t\tstrings.Contains(errLower, \"permission\") {\n\t\treturn \"auth\"\n\t}\n\n\t// Check for network errors (transport/DNS/socket issues)\n\tif strings.Contains(errLower, \"network\") || strings.Contains(errLower, \"timeout\") ||\n\t\tstrings.Contains(errLower, \"connection refused\") ||\n\t\tstrings.Contains(errLower, \"connection reset\") ||\n\t\tstrings.Contains(errLower, \"no route to host\") ||\n\t\tstrings.Contains(errLower, \"network is unreachable\") ||\n\t\tstrings.Contains(errLower, \"dns lookup\") ||\n\t\tstrings.Contains(errLower, \"dial\") ||\n\t\tstrings.Contains(errLower, \"eof\") ||\n\t\tstrings.Contains(errLower, \"broken pipe\") ||\n\t\tstrings.Contains(errLower, \"i/o\") {\n\t\treturn \"network\"\n\t}\n\n\t// Check for Redis server errors (including cluster errors)\n\t// Common Redis error prefixes: ERR, WRONGTYPE, CLUSTERDOWN, MOVED, ASK, READONLY, etc.\n\tserverErrors := []string{\"err\", \"wrongtype\", \"clusterdown\", \"moved\", \"ask\", \"readonly\",\n\t\t\"crossslot\", \"tryagain\", \"loading\", \"busy\", \"noscript\", \"oom\", \"execabort\", \"noquorum\", \"redis:\"}\n\tfor _, prefix := range serverErrors {\n\t\tif strings.Contains(errLower, prefix) {\n\t\t\treturn \"server\"\n\t\t}\n\t}\n\n\t// If it looks like an uppercase Redis error prefix, it's a server error\n\tif len(errStr) > 0 && errStr == strings.ToUpper(errStr) {\n\t\treturn \"server\"\n\t}\n\n\t// Uncategorized errors\n\treturn \"other\"\n}\n\n// splitHostPort splits a host:port string into host and port\n// This is a simplified version that handles the common cases\nfunc splitHostPort(addr string) (host, port string) {\n\t// Handle Unix sockets\n\tif strings.HasPrefix(addr, \"/\") || strings.HasPrefix(addr, \"@\") {\n\t\treturn addr, \"\"\n\t}\n\n\thost, port, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\t// If split fails, return the whole address as host\n\t\treturn addr, \"\"\n\t}\n\n\treturn host, port\n}\n\n// parseAddr parses a Redis address into host and port\nfunc parseAddr(addr string) (host, port string) {\n\t// Handle Unix sockets\n\tif strings.HasPrefix(addr, \"/\") || strings.HasPrefix(addr, \"unix://\") {\n\t\treturn addr, \"\"\n\t}\n\n\t// Remove protocol prefix if present\n\taddr = strings.TrimPrefix(addr, \"redis://\")\n\taddr = strings.TrimPrefix(addr, \"rediss://\")\n\n\thost, port, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\t// No port specified, use default\n\t\treturn addr, \"6379\"\n\t}\n\n\treturn host, port\n}\n\n// extractServerInfo extracts server address and port from connection info\n// For client connections, this is the remote endpoint (server address)\nfunc extractServerInfo(cn redis.ConnInfo) (addr, port string) {\n\tif cn == nil {\n\t\treturn \"\", \"\"\n\t}\n\n\tremoteAddr := cn.RemoteAddr()\n\tif remoteAddr == nil {\n\t\treturn \"\", \"\"\n\t}\n\n\taddrStr := remoteAddr.String()\n\thost, portStr := parseAddr(addrStr)\n\treturn host, portStr\n}\n\n// RecordConnectionCreateTime records the time it took to create a new connection\nfunc (r *metricsRecorder) RecordConnectionCreateTime(\n\tctx context.Context,\n\tduration time.Duration,\n\tcn redis.ConnInfo,\n) {\n\tif r.connectionCreateTime == nil {\n\t\treturn\n\t}\n\n\t// Convert duration to seconds (OTel convention)\n\tdurationSeconds := duration.Seconds()\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Use pool name from connection (set when connection was created)\n\t// This ensures consistency with gauge metrics which also use the registered pool name\n\tif cn != nil {\n\t\tpoolName := cn.PoolName()\n\t\tif poolName != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))\n\t\t}\n\t}\n\n\t// Record the histogram\n\tr.connectionCreateTime.Record(ctx, durationSeconds, metric.WithAttributes(attrs...))\n}\n\n// RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed\nfunc (r *metricsRecorder) RecordConnectionRelaxedTimeout(\n\tctx context.Context,\n\tdelta int,\n\tcn redis.ConnInfo,\n\tpoolName, notificationType string,\n) {\n\tif r.connectionRelaxedTimeout == nil {\n\t\treturn\n\t}\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Use pool name from connection (set when connection was created)\n\tif cn != nil {\n\t\tconnPoolName := cn.PoolName()\n\t\tif connPoolName != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, connPoolName))\n\t\t}\n\t}\n\n\t// Add notification type\n\tattrs = append(attrs, attribute.String(AttrRedisClientConnectionNotification, notificationType))\n\n\t// Record the counter (delta can be +1 or -1)\n\tr.connectionRelaxedTimeout.Add(ctx, int64(delta), metric.WithAttributes(attrs...))\n}\n\n// RecordConnectionHandoff records when a connection is handed off to another node\nfunc (r *metricsRecorder) RecordConnectionHandoff(\n\tctx context.Context,\n\tcn redis.ConnInfo,\n\tpoolName string,\n) {\n\tif r.connectionHandoff == nil {\n\t\treturn\n\t}\n\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Use pool name from connection (set when connection was created)\n\tif cn != nil {\n\t\tconnPoolName := cn.PoolName()\n\t\tif connPoolName != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, connPoolName))\n\t\t}\n\t}\n\n\t// Record the counter\n\tr.connectionHandoff.Add(ctx, 1, metric.WithAttributes(attrs...))\n}\n\n// RecordError records client errors (ASK, MOVED, handshake failures, etc.)\nfunc (r *metricsRecorder) RecordError(\n\tctx context.Context,\n\terrorType string,\n\tcn redis.ConnInfo,\n\tstatusCode string,\n\tisInternal bool,\n\tretryAttempts int,\n) {\n\tif r.clientErrors == nil {\n\t\treturn\n\t}\n\n\t// Extract server address and peer address from connection (may be nil for some errors)\n\t// For client connections, peer address is the same as server address (remote endpoint)\n\tvar serverAddr, serverPort, peerAddr, peerPort string\n\tif cn != nil {\n\t\tserverAddr, serverPort = extractServerInfo(cn)\n\t\tpeerAddr, peerPort = serverAddr, serverPort // Peer is same as server for client connections\n\t}\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tattribute.String(AttrErrorType, errorType),\n\t\tattribute.String(AttrRedisClientErrorsCategory, getErrorCategoryFromString(errorType)),\n\t\tattribute.String(AttrDBResponseStatusCode, statusCode),\n\t\tattribute.Bool(AttrRedisClientErrorsInternal, isInternal),\n\t\tattribute.Int(AttrRedisClientOperationRetryAttempts, retryAttempts),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Add server info if available\n\tif serverAddr != \"\" {\n\t\tattrs = append(attrs, attribute.String(AttrServerAddress, serverAddr))\n\t\tattrs = addServerPortIfNonDefault(attrs, serverPort)\n\t}\n\n\t// Add peer info if available\n\tif peerAddr != \"\" {\n\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))\n\t\tif peerPort != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))\n\t\t}\n\t}\n\n\t// Record the counter\n\tr.clientErrors.Add(ctx, 1, metric.WithAttributes(attrs...))\n}\n\n// RecordMaintenanceNotification records when a maintenance notification is received\nfunc (r *metricsRecorder) RecordMaintenanceNotification(\n\tctx context.Context,\n\tcn redis.ConnInfo,\n\tnotificationType string,\n) {\n\tif r.maintenanceNotifications == nil {\n\t\treturn\n\t}\n\n\t// Extract server address and peer address from connection\n\t// For client connections, peer address is the same as server address (remote endpoint)\n\tserverAddr, serverPort := extractServerInfo(cn)\n\tpeerAddr, peerPort := serverAddr, serverPort // Peer is same as server for client connections\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tattribute.String(AttrServerAddress, serverAddr),\n\t\tgetLibraryVersionAttr(),\n\t\tattribute.String(AttrRedisClientConnectionNotification, notificationType),\n\t}\n\n\t// Add server.port if not default\n\tattrs = addServerPortIfNonDefault(attrs, serverPort)\n\n\t// Add peer info if available\n\tif peerAddr != \"\" {\n\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))\n\t\tif peerPort != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))\n\t\t}\n\t}\n\n\t// Record the counter\n\tr.maintenanceNotifications.Add(ctx, 1, metric.WithAttributes(attrs...))\n}\n\n// RecordConnectionWaitTime records db.client.connection.wait_time metric\nfunc (r *metricsRecorder) RecordConnectionWaitTime(\n\tctx context.Context,\n\tduration time.Duration,\n\tcn redis.ConnInfo,\n) {\n\tif r.connectionWaitTime == nil {\n\t\treturn\n\t}\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Use pool name from connection (set when connection was created)\n\tif cn != nil {\n\t\tpoolName := cn.PoolName()\n\t\tif poolName != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))\n\t\t}\n\t}\n\n\t// Record the histogram (duration in seconds)\n\tr.connectionWaitTime.Record(ctx, duration.Seconds(), metric.WithAttributes(attrs...))\n}\n\n// RecordConnectionClosed records redis.client.connection.closed metric\nfunc (r *metricsRecorder) RecordConnectionClosed(\n\tctx context.Context,\n\tcn redis.ConnInfo,\n\treason string,\n\terr error,\n) {\n\tif r.connectionClosed == nil {\n\t\treturn\n\t}\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Use pool name from connection (set when connection was created)\n\tif cn != nil {\n\t\tpoolName := cn.PoolName()\n\t\tif poolName != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))\n\t\t}\n\t}\n\n\t// Add error type and category (always required per spec)\n\t// Use classifyError to normalize error messages and prevent high cardinality\n\tif err != nil {\n\t\t// Normalize the close reason to prevent high cardinality from variable data\n\t\t// (e.g., port numbers, connection IDs in error messages)\n\t\tnormalizedReason := classifyError(err)\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientConnectionCloseReason, normalizedReason))\n\t\tattrs = append(attrs, attribute.String(AttrErrorType, normalizedReason))\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategory(err)))\n\t} else {\n\t\t// For non-error closures, use reason directly (these are controlled strings like \"pool_closed\")\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientConnectionCloseReason, reason))\n\t\tattrs = append(attrs, attribute.String(AttrErrorType, reason))\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientErrorsCategory, getErrorCategoryFromString(reason)))\n\t}\n\n\t// Record the counter\n\tr.connectionClosed.Add(ctx, 1, metric.WithAttributes(attrs...))\n}\n\n// RecordPubSubMessage records redis.client.pubsub.messages metric\nfunc (r *metricsRecorder) RecordPubSubMessage(\n\tctx context.Context,\n\tcn redis.ConnInfo,\n\tdirection string,\n\tchannel string,\n\tsharded bool,\n) {\n\tif r.pubsubMessages == nil {\n\t\treturn\n\t}\n\n\t// Extract server address and peer address from connection\n\tserverAddr, serverPort := extractServerInfo(cn)\n\tpeerAddr, peerPort := serverAddr, serverPort\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tattribute.String(AttrServerAddress, serverAddr),\n\t\tattribute.String(AttrRedisClientPubSubDirection, direction), // \"sent\" or \"received\"\n\t\tattribute.Bool(AttrRedisClientPubSubSharded, sharded),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Add channel name if not hidden for cardinality reduction\n\tif !r.cfg.hidePubSubChannelNames && channel != \"\" {\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientPubSubChannel, channel))\n\t}\n\n\t// Add server.port if not default\n\tattrs = addServerPortIfNonDefault(attrs, serverPort)\n\n\t// Add peer info\n\tif peerAddr != \"\" {\n\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))\n\t\tif peerPort != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))\n\t\t}\n\t}\n\n\t// Record the counter\n\tr.pubsubMessages.Add(ctx, 1, metric.WithAttributes(attrs...))\n}\n\n// RecordStreamLag records redis.client.stream.lag metric\nfunc (r *metricsRecorder) RecordStreamLag(\n\tctx context.Context,\n\tlag time.Duration,\n\tcn redis.ConnInfo,\n\tstreamName string,\n\tconsumerGroup string,\n\tconsumerName string,\n) {\n\tif r.streamLag == nil {\n\t\treturn\n\t}\n\n\t// Extract server address and peer address from connection\n\tserverAddr, serverPort := extractServerInfo(cn)\n\tpeerAddr, peerPort := serverAddr, serverPort\n\n\t// Build attributes\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\tattribute.String(AttrServerAddress, serverAddr),\n\t\tattribute.String(AttrRedisClientStreamConsumerGroup, consumerGroup),\n\t\tattribute.String(AttrRedisClientStreamConsumerName, consumerName),\n\t\tgetLibraryVersionAttr(),\n\t}\n\n\t// Add stream name if not hidden for cardinality reduction\n\tif !r.cfg.hideStreamNames && streamName != \"\" {\n\t\tattrs = append(attrs, attribute.String(AttrRedisClientStreamName, streamName))\n\t}\n\n\t// Add server.port if not default\n\tattrs = addServerPortIfNonDefault(attrs, serverPort)\n\n\t// Add peer info\n\tif peerAddr != \"\" {\n\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerAddress, peerAddr))\n\t\tif peerPort != \"\" {\n\t\t\tattrs = append(attrs, attribute.String(AttrNetworkPeerPort, peerPort))\n\t\t}\n\t}\n\n\t// Record the histogram (lag in seconds)\n\tr.streamLag.Record(ctx, lag.Seconds(), metric.WithAttributes(attrs...))\n}\n\n// RegisterPool implements the OTelPoolRegistrar interface.\n// The pools are used by async gauge callbacks to pull statistics.\nfunc (r *metricsRecorder) RegisterPool(poolName string, pool redis.Pooler) {\n\tr.poolsMu.Lock()\n\tdefer r.poolsMu.Unlock()\n\n\t// Add pool to registry\n\tr.pools = append(r.pools, poolInfo{\n\t\tname: poolName,\n\t\tpool: pool,\n\t})\n}\n\n// UnregisterPool implements the OTelPoolRegistrar interface.\n// This ensures async gauge callbacks don't try to access closed pools.\nfunc (r *metricsRecorder) UnregisterPool(pool redis.Pooler) {\n\tr.poolsMu.Lock()\n\tdefer r.poolsMu.Unlock()\n\n\t// Find and remove the pool from registry\n\tfor i, p := range r.pools {\n\t\tif p.pool == pool {\n\t\t\t// Remove by swapping with last element and truncating\n\t\t\tr.pools[i] = r.pools[len(r.pools)-1]\n\t\t\tr.pools = r.pools[:len(r.pools)-1]\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// RegisterPubSubPool implements the OTelPoolRegistrar interface.\n// The pools are used by async gauge callbacks to pull statistics.\nfunc (r *metricsRecorder) RegisterPubSubPool(poolName string, pool redis.PubSubPooler) {\n\tr.poolsMu.Lock()\n\tdefer r.poolsMu.Unlock()\n\n\t// Add PubSub pool to registry\n\tr.pubsubPools = append(r.pubsubPools, pubsubPoolInfo{\n\t\tname: poolName,\n\t\tpool: pool,\n\t})\n}\n\n// UnregisterPubSubPool implements the OTelPoolRegistrar interface.\n// This ensures async gauge callbacks don't try to access closed pools.\nfunc (r *metricsRecorder) UnregisterPubSubPool(pool redis.PubSubPooler) {\n\tr.poolsMu.Lock()\n\tdefer r.poolsMu.Unlock()\n\n\t// Find and remove the pool from registry\n\tfor i, p := range r.pubsubPools {\n\t\tif p.pool == pool {\n\t\t\t// Remove by swapping with last element and truncating\n\t\t\tr.pubsubPools[i] = r.pubsubPools[len(r.pubsubPools)-1]\n\t\t\tr.pubsubPools = r.pubsubPools[:len(r.pubsubPools)-1]\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "extra/redisotel-native/metrics_definitions_test.go",
    "content": "package redisotel\n\nimport \"testing\"\n\n// Expected metric names per OTel semantic conventions.\n// Reference: https://opentelemetry.io/docs/specs/semconv/database/database-metrics/\nconst (\n\tsemconvOperationDuration    = \"db.client.operation.duration\"\n\tsemconvConnectionCount      = \"db.client.connection.count\"\n\tsemconvConnectionCreateTime = \"db.client.connection.create_time\"\n\tsemconvConnectionWaitTime   = \"db.client.connection.wait_time\"\n\tsemconvConnectionPending    = \"db.client.connection.pending_requests\"\n)\n\n// TestMetricDefinitionsMatchSemconv verifies metric names match OTel semantic conventions.\nfunc TestMetricDefinitionsMatchSemconv(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tgot      string\n\t\texpected string\n\t}{\n\t\t{\"db.client.operation.duration\", MetricOperationDuration, semconvOperationDuration},\n\t\t{\"db.client.connection.count\", MetricConnectionCount, semconvConnectionCount},\n\t\t{\"db.client.connection.create_time\", MetricConnectionCreateTime, semconvConnectionCreateTime},\n\t\t{\"db.client.connection.wait_time\", MetricConnectionWaitTime, semconvConnectionWaitTime},\n\t\t{\"db.client.connection.pending_requests\", MetricConnectionPendingReqs, semconvConnectionPending},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.got != tt.expected {\n\t\t\t\tt.Errorf(\"got %q, want %q\", tt.got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestSemconvMetricTypes documents expected metric instrument types.\n// Semconv specifies UpDownCounter for connection.count and pending_requests,\n// but this implementation uses ObservableGauge (known deviation, see issue #3730).\nfunc TestSemconvMetricTypes(t *testing.T) {\n\tt.Run(\"connection.count uses Gauge (semconv specifies UpDownCounter)\", func(t *testing.T) {\n\t\t// Known deviation: using ObservableGauge instead of UpDownCounter\n\t})\n\n\tt.Run(\"pending_requests uses Gauge (semconv specifies UpDownCounter)\", func(t *testing.T) {\n\t\t// Known deviation: using ObservableGauge instead of UpDownCounter\n\t})\n\n\tt.Run(\"operation.duration uses Histogram (correct)\", func(t *testing.T) {\n\t\t// Matches semconv: Float64Histogram\n\t})\n\n\tt.Run(\"connection.create_time uses Histogram (correct)\", func(t *testing.T) {\n\t\t// Matches semconv: Float64Histogram\n\t})\n\n\tt.Run(\"connection.wait_time uses Histogram (correct)\", func(t *testing.T) {\n\t\t// Matches semconv: Float64Histogram\n\t})\n}\n"
  },
  {
    "path": "extra/redisotel-native/metrics_stress_test.go",
    "content": "package redisotel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/sdk/metric\"\n\t\"go.opentelemetry.io/otel/sdk/metric/metricdata\"\n)\n\nconst (\n\tstressTestDuration       = 30 * time.Second\n\tstressTestConcurrency    = 50\n\tstressTestMinDelay       = 10 * time.Millisecond\n\tstressTestMaxDelay       = 100 * time.Millisecond\n\tstressTestStatusInterval = 5 * time.Second\n)\n\n// TestMetricsUnderStress validates metrics recording under concurrent load.\nfunc TestMetricsUnderStress(t *testing.T) {\n\tctx := context.Background()\n\ttestClient := redis.NewClient(&redis.Options{Addr: \"localhost:6379\"})\n\tif err := testClient.Ping(ctx).Err(); err != nil {\n\t\ttestClient.Close()\n\t\tt.Skip(\"Redis not available at localhost:6379\")\n\t}\n\ttestClient.Close()\n\n\treader := metric.NewManualReader()\n\tmeterProvider := metric.NewMeterProvider(metric.WithReader(reader))\n\tdefer func() {\n\t\t_ = meterProvider.Shutdown(ctx)\n\t}()\n\n\totel.SetMeterProvider(meterProvider)\n\tresetObservabilityForTest()\n\n\totelInstance := GetObservabilityInstance()\n\tconfig := NewConfig().\n\t\tWithEnabled(true).\n\t\tWithMeterProvider(meterProvider).\n\t\tWithMetricGroups(MetricGroupAll)\n\n\tif err := otelInstance.Init(config); err != nil {\n\t\tt.Fatalf(\"Failed to initialize OTel: %v\", err)\n\t}\n\tdefer otelInstance.Shutdown()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:         \"localhost:6379\",\n\t\tPoolSize:     stressTestConcurrency,\n\t\tMinIdleConns: 10,\n\t})\n\tdefer rdb.Close()\n\n\tvar opsCompleted atomic.Int64\n\tvar opsErrors atomic.Int64\n\tstartTime := time.Now()\n\tdeadline := startTime.Add(stressTestDuration)\n\n\tstatusTicker := time.NewTicker(stressTestStatusInterval)\n\tdefer statusTicker.Stop()\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-statusTicker.C:\n\t\t\t\telapsed := time.Since(startTime).Seconds()\n\t\t\t\tops := opsCompleted.Load()\n\t\t\t\tt.Logf(\"[%.0fs] %d ops, %.1f ops/sec\", elapsed, ops, float64(ops)/elapsed)\n\t\t\tcase <-done:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < stressTestConcurrency; i++ {\n\t\twg.Add(1)\n\t\tgo func(workerID int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor time.Now().Before(deadline) {\n\t\t\t\tkey := fmt.Sprintf(\"stress_test_key_%d_%d\", workerID, rand.Int63())\n\t\t\t\tvalue := fmt.Sprintf(\"value_%d\", time.Now().UnixNano())\n\n\t\t\t\tif err := rdb.Set(ctx, key, value, time.Minute).Err(); err != nil {\n\t\t\t\t\topsErrors.Add(1)\n\t\t\t\t} else {\n\t\t\t\t\topsCompleted.Add(1)\n\t\t\t\t}\n\n\t\t\t\tif _, err := rdb.Get(ctx, key).Result(); err != nil && err != redis.Nil {\n\t\t\t\t\topsErrors.Add(1)\n\t\t\t\t} else {\n\t\t\t\t\topsCompleted.Add(1)\n\t\t\t\t}\n\n\t\t\t\tdelay := stressTestMinDelay + time.Duration(rand.Int63n(int64(stressTestMaxDelay-stressTestMinDelay)))\n\t\t\t\ttime.Sleep(delay)\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\tclose(done)\n\n\ttotalOps := opsCompleted.Load()\n\ttotalErrors := opsErrors.Load()\n\telapsed := time.Since(startTime)\n\tt.Logf(\"Completed: %d ops in %v (%.1f ops/sec), %d errors\",\n\t\ttotalOps, elapsed.Round(time.Second), float64(totalOps)/elapsed.Seconds(), totalErrors)\n\n\tvar rm metricdata.ResourceMetrics\n\tif err := reader.Collect(ctx, &rm); err != nil {\n\t\tt.Fatalf(\"Failed to collect metrics: %v\", err)\n\t}\n\n\tvalidateMetrics(t, rm)\n}\n\nfunc validateMetrics(t *testing.T, rm metricdata.ResourceMetrics) {\n\tmetricsFound := make(map[string]bool)\n\tfor _, sm := range rm.ScopeMetrics {\n\t\tfor _, m := range sm.Metrics {\n\t\t\tmetricsFound[m.Name] = true\n\t\t}\n\t}\n\n\trequired := []string{\n\t\tMetricConnectionCount,\n\t\tMetricConnectionCreateTime,\n\t\tMetricOperationDuration,\n\t}\n\n\tfor _, name := range required {\n\t\tif !metricsFound[name] {\n\t\t\tt.Errorf(\"Required metric not found: %s\", name)\n\t\t}\n\t}\n}\n\nfunc resetObservabilityForTest() {\n\tobservabilityInstanceOnce = sync.Once{}\n\tobservabilityInstance = nil\n}\n\n// TestTracingAndMetricsCompatibility verifies that redisotel (tracing) and\n// redisotel-native (metrics) can be used together without conflicts.\n// Tracing uses AddHook (per-client), metrics uses SetOTelRecorder (global).\nfunc TestTracingAndMetricsCompatibility(t *testing.T) {\n\tctx := context.Background()\n\ttestClient := redis.NewClient(&redis.Options{Addr: \"localhost:6379\"})\n\tif err := testClient.Ping(ctx).Err(); err != nil {\n\t\ttestClient.Close()\n\t\tt.Skip(\"Redis not available at localhost:6379\")\n\t}\n\ttestClient.Close()\n\n\treader := metric.NewManualReader()\n\tmeterProvider := metric.NewMeterProvider(metric.WithReader(reader))\n\tdefer meterProvider.Shutdown(ctx)\n\n\totel.SetMeterProvider(meterProvider)\n\tresetObservabilityForTest()\n\n\totelInstance := GetObservabilityInstance()\n\tconfig := NewConfig().\n\t\tWithEnabled(true).\n\t\tWithMeterProvider(meterProvider).\n\t\tWithMetricGroups(MetricGroupAll)\n\n\tif err := otelInstance.Init(config); err != nil {\n\t\tt.Fatalf(\"Failed to initialize OTel metrics: %v\", err)\n\t}\n\tdefer otelInstance.Shutdown()\n\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPoolSize: 5,\n\t})\n\tdefer rdb.Close()\n\n\t// In production, also call: redisotel.InstrumentTracing(rdb)\n\n\tfor i := 0; i < 10; i++ {\n\t\tkey := fmt.Sprintf(\"compat-test-%d\", i)\n\t\tif err := rdb.Set(ctx, key, \"value\", time.Minute).Err(); err != nil {\n\t\t\tt.Fatalf(\"SET failed: %v\", err)\n\t\t}\n\t\tif _, err := rdb.Get(ctx, key).Result(); err != nil {\n\t\t\tt.Fatalf(\"GET failed: %v\", err)\n\t\t}\n\t}\n\n\tvar rm metricdata.ResourceMetrics\n\tif err := reader.Collect(ctx, &rm); err != nil {\n\t\tt.Fatalf(\"Failed to collect metrics: %v\", err)\n\t}\n\n\tfound := false\n\tfor _, sm := range rm.ScopeMetrics {\n\t\tfor _, m := range sm.Metrics {\n\t\t\tif m.Name == MetricOperationDuration {\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif !found {\n\t\tt.Error(\"Expected to find db.client.operation.duration metric\")\n\t}\n}\n"
  },
  {
    "path": "extra/redisotel-native/redisotel.go",
    "content": "package redisotel\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/metric\"\n\t\"go.opentelemetry.io/otel/semconv/v1.38.0/dbconv\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// Metric name constants\nconst (\n\t// Operation metrics\n\tMetricOperationDuration = \"db.client.operation.duration\"\n\n\t// Connection metrics\n\tMetricConnectionCount          = \"db.client.connection.count\"\n\tMetricConnectionCreateTime     = \"db.client.connection.create_time\"\n\tMetricConnectionWaitTime       = \"db.client.connection.wait_time\"\n\tMetricConnectionPendingReqs    = \"db.client.connection.pending_requests\"\n\tMetricConnectionRelaxedTimeout = \"redis.client.connection.relaxed_timeout\"\n\tMetricConnectionHandoff        = \"redis.client.connection.handoff\"\n\tMetricConnectionClosed         = \"redis.client.connection.closed\"\n\n\t// Resiliency metrics\n\tMetricClientErrors             = \"redis.client.errors\"\n\tMetricMaintenanceNotifications = \"redis.client.maintenance.notifications\"\n\n\t// Pub/Sub metrics\n\tMetricPubSubMessages = \"redis.client.pubsub.messages\"\n\n\t// Stream metrics\n\tMetricStreamLag = \"redis.client.stream.lag\"\n\n\t// Special pool names\n\tPoolNameMain   = \"main\"\n\tPoolNamePubSub = \"pubsub\"\n)\n\nvar (\n\t// Global observability instance\n\tobservabilityInstance     *ObservabilityInstance\n\tobservabilityInstanceOnce sync.Once\n)\n\n// ObservabilityInstance manages the global observability singleton.\ntype ObservabilityInstance struct {\n\tmu          sync.RWMutex\n\tconfig      *Config\n\trecorder    *metricsRecorder\n\tinitialized bool\n}\n\n// GetObservabilityInstance returns the global observability singleton.\nfunc GetObservabilityInstance() *ObservabilityInstance {\n\tobservabilityInstanceOnce.Do(func() {\n\t\tobservabilityInstance = &ObservabilityInstance{}\n\t})\n\treturn observabilityInstance\n}\n\n// Init initializes OpenTelemetry observability globally for all Redis clients.\n// This should be called once at application startup, BEFORE creating any Redis clients.\n// After initialization, all Redis clients will automatically collect and export\n// metrics without needing any additional configuration.\nfunc (o *ObservabilityInstance) Init(cfg *Config) error {\n\to.mu.Lock()\n\tdefer o.mu.Unlock()\n\n\t// If already initialized, return error\n\tif o.initialized {\n\t\treturn errors.New(\"redisotel: already initialized, call Shutdown() before reinitializing\")\n\t}\n\n\to.config = cfg\n\n\tif !cfg.Enabled {\n\t\treturn nil\n\t}\n\n\t// Get meter provider (use global if not provided)\n\tmeterProvider := cfg.MeterProvider\n\tif meterProvider == nil {\n\t\tmeterProvider = otel.GetMeterProvider()\n\t}\n\n\tmeter := meterProvider.Meter(\n\t\t\"github.com/redis/go-redis\",\n\t\tmetric.WithInstrumentationVersion(redis.Version()),\n\t)\n\n\tinternalCfg := o.configToInternal(cfg)\n\trecorder, err := o.createRecorder(meter, internalCfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create metrics recorder: %w\", err)\n\t}\n\n\to.recorder = recorder\n\to.initialized = true\n\tredis.SetOTelRecorder(recorder)\n\n\treturn nil\n}\n\n// IsEnabled returns true if observability is initialized and enabled.\nfunc (o *ObservabilityInstance) IsEnabled() bool {\n\to.mu.RLock()\n\tdefer o.mu.RUnlock()\n\treturn o.initialized && o.config != nil && o.config.Enabled\n}\n\n// Shutdown cleans up resources and flushes any pending metrics.\n// This should be called at application shutdown.\nfunc (o *ObservabilityInstance) Shutdown() error {\n\to.mu.Lock()\n\tdefer o.mu.Unlock()\n\treturn o.shutdownLocked()\n}\n\nfunc (o *ObservabilityInstance) shutdownLocked() error {\n\tif !o.initialized {\n\t\treturn nil\n\t}\n\n\tredis.SetOTelRecorder(nil)\n\n\t// Note: We don't shutdown the MeterProvider since it's owned by the application\n\t// The application should call provider.Shutdown() when appropriate\n\n\to.recorder = nil\n\to.initialized = false\n\n\treturn nil\n}\n\n// configToInternal converts the public Config to internal config format\nfunc (o *ObservabilityInstance) configToInternal(cfg *Config) config {\n\tenabledGroups := make(map[MetricGroup]bool)\n\tif cfg.MetricGroups&MetricGroupFlagCommand != 0 {\n\t\tenabledGroups[MetricGroupCommand] = true\n\t}\n\tif cfg.MetricGroups&MetricGroupFlagConnectionBasic != 0 {\n\t\tenabledGroups[MetricGroupConnectionBasic] = true\n\t}\n\tif cfg.MetricGroups&MetricGroupFlagResiliency != 0 {\n\t\tenabledGroups[MetricGroupResiliency] = true\n\t}\n\tif cfg.MetricGroups&MetricGroupFlagConnectionAdvanced != 0 {\n\t\tenabledGroups[MetricGroupConnectionAdvanced] = true\n\t}\n\tif cfg.MetricGroups&MetricGroupFlagPubSub != 0 {\n\t\tenabledGroups[MetricGroupPubSub] = true\n\t}\n\tif cfg.MetricGroups&MetricGroupFlagStream != 0 {\n\t\tenabledGroups[MetricGroupStream] = true\n\t}\n\n\treturn config{\n\t\tmeterProvider:                   cfg.MeterProvider,\n\t\tenabled:                         cfg.Enabled,\n\t\tenabledMetricGroups:             enabledGroups,\n\t\tincludeCommands:                 cfg.IncludeCommands,\n\t\texcludeCommands:                 cfg.ExcludeCommands,\n\t\thidePubSubChannelNames:          cfg.HidePubSubChannelNames,\n\t\thideStreamNames:                 cfg.HideStreamNames,\n\t\thistAggregation:                 cfg.HistogramAggregation,\n\t\tbucketsOperationDuration:        cfg.BucketsOperationDuration,\n\t\tbucketsStreamProcessingDuration: cfg.BucketsStreamLag,\n\t\tbucketsConnectionCreateTime:     cfg.BucketsConnectionCreateTime,\n\t\tbucketsConnectionWaitTime:       cfg.BucketsConnectionWaitTime,\n\t}\n}\n\n// createRecorder creates a metricsRecorder with all instruments based on config.\nfunc (o *ObservabilityInstance) createRecorder(meter metric.Meter, cfg config) (*metricsRecorder, error) {\n\tvar err error\n\n\tvar operationDuration metric.Float64Histogram\n\tif cfg.isMetricGroupEnabled(MetricGroupCommand) {\n\t\tvar operationDurationOpts []metric.Float64HistogramOption\n\t\tif cfg.histAggregation == HistogramAggregationExplicitBucket {\n\t\t\toperationDurationOpts = append(operationDurationOpts,\n\t\t\t\tmetric.WithExplicitBucketBoundaries(cfg.bucketsOperationDuration...),\n\t\t\t)\n\t\t}\n\t\tvar operationDurationConv dbconv.ClientOperationDuration\n\t\toperationDurationConv, err = dbconv.NewClientOperationDuration(meter, operationDurationOpts...)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create operation duration histogram: %w\", err)\n\t\t}\n\t\toperationDuration = operationDurationConv.Inst()\n\t}\n\n\tvar connectionCountGauge metric.Int64ObservableGauge\n\tvar connectionCreateTime metric.Float64Histogram\n\tvar connectionRelaxedTimeout metric.Int64UpDownCounter\n\tvar connectionHandoff metric.Int64Counter\n\n\tif cfg.isMetricGroupEnabled(MetricGroupConnectionBasic) {\n\t\tconnectionCountGauge, err = meter.Int64ObservableGauge(\n\t\t\tdbconv.ClientConnectionCount{}.Name(),\n\t\t\tmetric.WithDescription(dbconv.ClientConnectionCount{}.Description()),\n\t\t\tmetric.WithUnit(dbconv.ClientConnectionCount{}.Unit()),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create connection count metric: %w\", err)\n\t\t}\n\n\t\tvar connectionCreateTimeOpts []metric.Float64HistogramOption\n\t\tif cfg.histAggregation == HistogramAggregationExplicitBucket {\n\t\t\tconnectionCreateTimeOpts = append(connectionCreateTimeOpts,\n\t\t\t\tmetric.WithExplicitBucketBoundaries(cfg.bucketsConnectionCreateTime...),\n\t\t\t)\n\t\t}\n\t\tvar connectionCreateTimeConv dbconv.ClientConnectionCreateTime\n\t\tconnectionCreateTimeConv, err = dbconv.NewClientConnectionCreateTime(meter, connectionCreateTimeOpts...)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create connection create time histogram: %w\", err)\n\t\t}\n\t\tconnectionCreateTime = connectionCreateTimeConv.Inst()\n\n\t\tconnectionRelaxedTimeout, err = meter.Int64UpDownCounter(\n\t\t\tMetricConnectionRelaxedTimeout,\n\t\t\tmetric.WithDescription(\"How many times the connection timeout has been increased/decreased (after a server maintenance notification)\"),\n\t\t\tmetric.WithUnit(\"{relaxation}\"),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create connection relaxed timeout metric: %w\", err)\n\t\t}\n\n\t\tconnectionHandoff, err = meter.Int64Counter(\n\t\t\tMetricConnectionHandoff,\n\t\t\tmetric.WithDescription(\"Connections that have been handed off to another node (e.g after a MOVING notification)\"),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create connection handoff metric: %w\", err)\n\t\t}\n\t}\n\n\tvar clientErrors metric.Int64Counter\n\tvar maintenanceNotifications metric.Int64Counter\n\n\tif cfg.isMetricGroupEnabled(MetricGroupResiliency) {\n\t\tclientErrors, err = meter.Int64Counter(\n\t\t\tMetricClientErrors,\n\t\t\tmetric.WithDescription(\"Number of errors handled by the Redis client\"),\n\t\t\tmetric.WithUnit(\"{error}\"),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create client errors metric: %w\", err)\n\t\t}\n\n\t\tmaintenanceNotifications, err = meter.Int64Counter(\n\t\t\tMetricMaintenanceNotifications,\n\t\t\tmetric.WithDescription(\"Number of maintenance notifications received\"),\n\t\t\tmetric.WithUnit(\"{notification}\"),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create maintenance notifications metric: %w\", err)\n\t\t}\n\t}\n\n\tvar connectionWaitTime metric.Float64Histogram\n\tvar connectionClosed metric.Int64Counter\n\tvar connectionPendingReqsGauge metric.Int64ObservableGauge\n\n\tif cfg.isMetricGroupEnabled(MetricGroupConnectionAdvanced) {\n\t\tvar connectionWaitTimeOpts []metric.Float64HistogramOption\n\t\tif cfg.histAggregation == HistogramAggregationExplicitBucket {\n\t\t\tconnectionWaitTimeOpts = append(connectionWaitTimeOpts,\n\t\t\t\tmetric.WithExplicitBucketBoundaries(cfg.bucketsConnectionWaitTime...),\n\t\t\t)\n\t\t}\n\t\tvar connectionWaitTimeConv dbconv.ClientConnectionWaitTime\n\t\tconnectionWaitTimeConv, err = dbconv.NewClientConnectionWaitTime(meter, connectionWaitTimeOpts...)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create connection wait time histogram: %w\", err)\n\t\t}\n\t\tconnectionWaitTime = connectionWaitTimeConv.Inst()\n\n\t\tconnectionClosed, err = meter.Int64Counter(\n\t\t\tMetricConnectionClosed,\n\t\t\tmetric.WithDescription(\"The number of connections that have been closed\"),\n\t\t\tmetric.WithUnit(\"{connection}\"),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create connection closed metric: %w\", err)\n\t\t}\n\n\t\tconnectionPendingReqsGauge, err = meter.Int64ObservableGauge(\n\t\t\tdbconv.ClientConnectionPendingRequests{}.Name(),\n\t\t\tmetric.WithDescription(dbconv.ClientConnectionPendingRequests{}.Description()),\n\t\t\tmetric.WithUnit(dbconv.ClientConnectionPendingRequests{}.Unit()),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create connection pending requests metric: %w\", err)\n\t\t}\n\t}\n\n\tvar pubsubMessages metric.Int64Counter\n\n\tif cfg.isMetricGroupEnabled(MetricGroupPubSub) {\n\t\tpubsubMessages, err = meter.Int64Counter(\n\t\t\tMetricPubSubMessages,\n\t\t\tmetric.WithDescription(\"The number of Pub/Sub messages sent or received\"),\n\t\t\tmetric.WithUnit(\"{message}\"),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create Pub/Sub messages metric: %w\", err)\n\t\t}\n\t}\n\n\tvar streamLag metric.Float64Histogram\n\n\tif cfg.isMetricGroupEnabled(MetricGroupStream) {\n\t\tvar streamLagOpts []metric.Float64HistogramOption\n\t\tstreamLagOpts = append(streamLagOpts,\n\t\t\tmetric.WithDescription(\"The lag between message creation and consumption in a stream consumer group\"),\n\t\t\tmetric.WithUnit(\"s\"),\n\t\t)\n\t\tif cfg.histAggregation == HistogramAggregationExplicitBucket {\n\t\t\tstreamLagOpts = append(streamLagOpts,\n\t\t\t\tmetric.WithExplicitBucketBoundaries(cfg.bucketsStreamProcessingDuration...),\n\t\t\t)\n\t\t}\n\t\tstreamLag, err = meter.Float64Histogram(\n\t\t\tMetricStreamLag,\n\t\t\tstreamLagOpts...,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create stream lag histogram: %w\", err)\n\t\t}\n\t}\n\n\t// Create recorder\n\trecorder := &metricsRecorder{\n\t\toperationDuration:          operationDuration,\n\t\tconnectionCountGauge:       connectionCountGauge,\n\t\tconnectionCreateTime:       connectionCreateTime,\n\t\tconnectionRelaxedTimeout:   connectionRelaxedTimeout,\n\t\tconnectionHandoff:          connectionHandoff,\n\t\tclientErrors:               clientErrors,\n\t\tmaintenanceNotifications:   maintenanceNotifications,\n\t\tconnectionWaitTime:         connectionWaitTime,\n\t\tconnectionClosed:           connectionClosed,\n\t\tconnectionPendingReqsGauge: connectionPendingReqsGauge,\n\t\tpubsubMessages:             pubsubMessages,\n\t\tstreamLag:                  streamLag,\n\t\tcfg:                        &cfg,\n\t\tpools:                      make([]poolInfo, 0),\n\t}\n\n\t// Register async callbacks for ObservableGauges\n\t// These callbacks will pull stats from registered pools\n\tif err := o.registerAsyncCallbacks(meter, recorder); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to register async callbacks: %w\", err)\n\t}\n\n\treturn recorder, nil\n}\n\n// parsePoolName extracts server address, port, and database index from a pool name.\n// Pool name format: \"host:port/db\" or \"host/db\" or \"host:port\" or \"host\"\n// Returns: (serverAddr, serverPort, dbIndex)\nfunc parsePoolName(poolName string) (string, string, string) {\n\t// Handle special pool names\n\tif poolName == PoolNameMain || poolName == PoolNamePubSub {\n\t\treturn \"\", \"\", \"\"\n\t}\n\n\tparts := strings.Split(poolName, \"/\")\n\taddrPart := parts[0]\n\tdbIndex := \"\"\n\tif len(parts) > 1 {\n\t\tdbIndex = parts[1]\n\t}\n\thost, port := parseAddr(addrPart)\n\treturn host, port, dbIndex\n}\n\nfunc (o *ObservabilityInstance) registerAsyncCallbacks(meter metric.Meter, recorder *metricsRecorder) error {\n\t// Register connection count gauge callback\n\tif recorder.connectionCountGauge != nil {\n\t\t_, err := meter.RegisterCallback(\n\t\t\tfunc(ctx context.Context, observer metric.Observer) error {\n\t\t\t\trecorder.poolsMu.RLock()\n\t\t\t\tpools := recorder.pools\n\t\t\t\tpubsubPools := recorder.pubsubPools\n\t\t\t\trecorder.poolsMu.RUnlock()\n\n\t\t\t\t// Iterate over all registered main pools\n\t\t\t\tfor _, poolInfo := range pools {\n\t\t\t\t\tstats := poolInfo.pool.PoolStats()\n\t\t\t\t\tif stats == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// Extract server info from pool name\n\t\t\t\t\tserverAddr, serverPort, _ := parsePoolName(poolInfo.name)\n\n\t\t\t\t\t// Build base attributes\n\t\t\t\t\tbaseAttrs := []attribute.KeyValue{\n\t\t\t\t\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\t\t\t\t\tgetLibraryVersionAttr(),\n\t\t\t\t\t}\n\t\t\t\t\tif serverAddr != \"\" {\n\t\t\t\t\t\tbaseAttrs = append(baseAttrs, attribute.String(AttrServerAddress, serverAddr))\n\t\t\t\t\t}\n\t\t\t\t\tif serverPort != \"\" && serverPort != \"6379\" {\n\t\t\t\t\t\tbaseAttrs = append(baseAttrs, attribute.String(AttrServerPort, serverPort))\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add pool name\n\t\t\t\t\tbaseAttrs = append(baseAttrs, attribute.String(AttrDBClientConnectionPoolName, poolInfo.name))\n\n\t\t\t\t\t// Observe idle connections\n\t\t\t\t\tidleAttrs := append([]attribute.KeyValue{}, baseAttrs...)\n\t\t\t\t\tidleAttrs = append(idleAttrs,\n\t\t\t\t\t\tattribute.String(AttrDBClientConnectionState, ConnectionStateIdle),\n\t\t\t\t\t\tattribute.Bool(AttrRedisClientConnectionPubSub, false),\n\t\t\t\t\t)\n\t\t\t\t\tobserver.ObserveInt64(recorder.connectionCountGauge, int64(stats.IdleConns),\n\t\t\t\t\t\tmetric.WithAttributes(idleAttrs...))\n\n\t\t\t\t\t// Observe used connections\n\t\t\t\t\tusedConns := stats.TotalConns - stats.IdleConns\n\t\t\t\t\tusedAttrs := append([]attribute.KeyValue{}, baseAttrs...)\n\t\t\t\t\tusedAttrs = append(usedAttrs,\n\t\t\t\t\t\tattribute.String(AttrDBClientConnectionState, ConnectionStateUsed),\n\t\t\t\t\t\tattribute.Bool(AttrRedisClientConnectionPubSub, false),\n\t\t\t\t\t)\n\t\t\t\t\tobserver.ObserveInt64(recorder.connectionCountGauge, int64(usedConns),\n\t\t\t\t\t\tmetric.WithAttributes(usedAttrs...))\n\t\t\t\t}\n\n\t\t\t\tfor _, pubsubPoolInfo := range pubsubPools {\n\t\t\t\t\tstats := pubsubPoolInfo.pool.Stats()\n\t\t\t\t\tif stats == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// Build base attributes\n\t\t\t\t\tbaseAttrs := []attribute.KeyValue{\n\t\t\t\t\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\t\t\t\t\tgetLibraryVersionAttr(),\n\t\t\t\t\t\tattribute.String(AttrDBClientConnectionPoolName, pubsubPoolInfo.name),\n\t\t\t\t\t}\n\n\t\t\t\t\t// PubSub pools report Active connections (not idle/used split\n\t\t\t\t\t// We'll report Active as \"used\" and 0 as \"idle\"\n\t\t\t\t\tidleAttrs := append([]attribute.KeyValue{}, baseAttrs...)\n\t\t\t\t\tidleAttrs = append(idleAttrs,\n\t\t\t\t\t\tattribute.String(AttrDBClientConnectionState, ConnectionStateIdle),\n\t\t\t\t\t\tattribute.Bool(AttrRedisClientConnectionPubSub, true),\n\t\t\t\t\t)\n\t\t\t\t\tobserver.ObserveInt64(recorder.connectionCountGauge, 0,\n\t\t\t\t\t\tmetric.WithAttributes(idleAttrs...))\n\n\t\t\t\t\tusedAttrs := append([]attribute.KeyValue{}, baseAttrs...)\n\t\t\t\t\tusedAttrs = append(usedAttrs,\n\t\t\t\t\t\tattribute.String(AttrDBClientConnectionState, ConnectionStateUsed),\n\t\t\t\t\t\tattribute.Bool(AttrRedisClientConnectionPubSub, true),\n\t\t\t\t\t)\n\t\t\t\t\tobserver.ObserveInt64(recorder.connectionCountGauge, int64(stats.Active),\n\t\t\t\t\t\tmetric.WithAttributes(usedAttrs...))\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\trecorder.connectionCountGauge,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to register connection count callback: %w\", err)\n\t\t}\n\t}\n\n\t// Register pending requests gauge callback\n\tif recorder.connectionPendingReqsGauge != nil {\n\t\t_, err := meter.RegisterCallback(\n\t\t\tfunc(ctx context.Context, observer metric.Observer) error {\n\t\t\t\trecorder.poolsMu.RLock()\n\t\t\t\tpools := recorder.pools\n\t\t\t\trecorder.poolsMu.RUnlock()\n\n\t\t\t\t// Iterate over all registered pools\n\t\t\t\tfor _, poolInfo := range pools {\n\t\t\t\t\tstats := poolInfo.pool.PoolStats()\n\t\t\t\t\tif stats == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// Extract server info from pool name\n\t\t\t\t\tserverAddr, serverPort, _ := parsePoolName(poolInfo.name)\n\n\t\t\t\t\t// Build base attributes\n\t\t\t\t\tbaseAttrs := []attribute.KeyValue{\n\t\t\t\t\t\tattribute.String(AttrDBSystemName, DBSystemRedis),\n\t\t\t\t\t\tgetLibraryVersionAttr(),\n\t\t\t\t\t}\n\t\t\t\t\tif serverAddr != \"\" {\n\t\t\t\t\t\tbaseAttrs = append(baseAttrs, attribute.String(AttrServerAddress, serverAddr))\n\t\t\t\t\t}\n\t\t\t\t\tif serverPort != \"\" && serverPort != \"6379\" {\n\t\t\t\t\t\tbaseAttrs = append(baseAttrs, attribute.String(AttrServerPort, serverPort))\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add pool name\n\t\t\t\t\tbaseAttrs = append(baseAttrs, attribute.String(AttrDBClientConnectionPoolName, poolInfo.name))\n\n\t\t\t\t\t// Observe pending requests count\n\t\t\t\t\tobserver.ObserveInt64(recorder.connectionPendingReqsGauge, int64(stats.PendingRequests),\n\t\t\t\t\t\tmetric.WithAttributes(baseAttrs...))\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\trecorder.connectionPendingReqsGauge,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to register pending requests callback: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "extra/redisprometheus/README.md",
    "content": "# Prometheus Metric Collector\n\nThis package implements a [`prometheus.Collector`](https://pkg.go.dev/github.com/prometheus/client_golang@v1.12.2/prometheus#Collector)\nfor collecting metrics about the connection pool used by the various redis clients.\nSupported clients are `redis.Client`, `redis.ClusterClient`, `redis.Ring` and `redis.UniversalClient`.\n\n### Example\n\n```go\nclient := redis.NewClient(options)\ncollector := redisprometheus.NewCollector(namespace, subsystem, client)\nprometheus.MustRegister(collector)\n```\n\n### Metrics\n\n| Name                      | Type           | Description                                                                 |\n|---------------------------|----------------|-----------------------------------------------------------------------------|\n| `pool_hit_total`          | Counter metric | number of times a connection was found in the pool                          |\n| `pool_miss_total`         | Counter metric | number of times a connection was not found in the pool                      |\n| `pool_timeout_total`      | Counter metric | number of times a timeout occurred when getting a connection from the pool  |\n| `pool_conn_total_current` | Gauge metric   | current number of connections in the pool                                   |\n| `pool_conn_idle_current`  | Gauge metric   | current number of idle connections in the pool                              |\n| `pool_conn_stale_total`   | Counter metric | number of times a connection was removed from the pool because it was stale |\n\n\n"
  },
  {
    "path": "extra/redisprometheus/collector.go",
    "content": "package redisprometheus\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// StatGetter provides a method to get pool statistics.\ntype StatGetter interface {\n\tPoolStats() *redis.PoolStats\n}\n\n// Collector collects statistics from a redis client.\n// It implements the prometheus.Collector interface.\ntype Collector struct {\n\tgetter      StatGetter\n\thitDesc     *prometheus.Desc\n\tmissDesc    *prometheus.Desc\n\ttimeoutDesc *prometheus.Desc\n\ttotalDesc   *prometheus.Desc\n\tidleDesc    *prometheus.Desc\n\tstaleDesc   *prometheus.Desc\n}\n\nvar _ prometheus.Collector = (*Collector)(nil)\n\n// NewCollector returns a new Collector based on the provided StatGetter.\n// The given namespace and subsystem are used to build the fully qualified metric name,\n// i.e. \"{namespace}_{subsystem}_{metric}\".\n// The provided metrics are:\n//   - pool_hit_total\n//   - pool_miss_total\n//   - pool_timeout_total\n//   - pool_conn_total_current\n//   - pool_conn_idle_current\n//   - pool_conn_stale_total\nfunc NewCollector(namespace, subsystem string, getter StatGetter) *Collector {\n\treturn &Collector{\n\t\tgetter: getter,\n\t\thitDesc: prometheus.NewDesc(\n\t\t\tprometheus.BuildFQName(namespace, subsystem, \"pool_hit_total\"),\n\t\t\t\"Number of times a connection was found in the pool\",\n\t\t\tnil, nil,\n\t\t),\n\t\tmissDesc: prometheus.NewDesc(\n\t\t\tprometheus.BuildFQName(namespace, subsystem, \"pool_miss_total\"),\n\t\t\t\"Number of times a connection was not found in the pool\",\n\t\t\tnil, nil,\n\t\t),\n\t\ttimeoutDesc: prometheus.NewDesc(\n\t\t\tprometheus.BuildFQName(namespace, subsystem, \"pool_timeout_total\"),\n\t\t\t\"Number of times a timeout occurred when looking for a connection in the pool\",\n\t\t\tnil, nil,\n\t\t),\n\t\ttotalDesc: prometheus.NewDesc(\n\t\t\tprometheus.BuildFQName(namespace, subsystem, \"pool_conn_total_current\"),\n\t\t\t\"Current number of connections in the pool\",\n\t\t\tnil, nil,\n\t\t),\n\t\tidleDesc: prometheus.NewDesc(\n\t\t\tprometheus.BuildFQName(namespace, subsystem, \"pool_conn_idle_current\"),\n\t\t\t\"Current number of idle connections in the pool\",\n\t\t\tnil, nil,\n\t\t),\n\t\tstaleDesc: prometheus.NewDesc(\n\t\t\tprometheus.BuildFQName(namespace, subsystem, \"pool_conn_stale_total\"),\n\t\t\t\"Number of times a connection was removed from the pool because it was stale\",\n\t\t\tnil, nil,\n\t\t),\n\t}\n}\n\n// Describe implements the prometheus.Collector interface.\nfunc (s *Collector) Describe(descs chan<- *prometheus.Desc) {\n\tdescs <- s.hitDesc\n\tdescs <- s.missDesc\n\tdescs <- s.timeoutDesc\n\tdescs <- s.totalDesc\n\tdescs <- s.idleDesc\n\tdescs <- s.staleDesc\n}\n\n// Collect implements the prometheus.Collector interface.\nfunc (s *Collector) Collect(metrics chan<- prometheus.Metric) {\n\tstats := s.getter.PoolStats()\n\tmetrics <- prometheus.MustNewConstMetric(\n\t\ts.hitDesc,\n\t\tprometheus.CounterValue,\n\t\tfloat64(stats.Hits),\n\t)\n\tmetrics <- prometheus.MustNewConstMetric(\n\t\ts.missDesc,\n\t\tprometheus.CounterValue,\n\t\tfloat64(stats.Misses),\n\t)\n\tmetrics <- prometheus.MustNewConstMetric(\n\t\ts.timeoutDesc,\n\t\tprometheus.CounterValue,\n\t\tfloat64(stats.Timeouts),\n\t)\n\tmetrics <- prometheus.MustNewConstMetric(\n\t\ts.totalDesc,\n\t\tprometheus.GaugeValue,\n\t\tfloat64(stats.TotalConns),\n\t)\n\tmetrics <- prometheus.MustNewConstMetric(\n\t\ts.idleDesc,\n\t\tprometheus.GaugeValue,\n\t\tfloat64(stats.IdleConns),\n\t)\n\tmetrics <- prometheus.MustNewConstMetric(\n\t\ts.staleDesc,\n\t\tprometheus.CounterValue,\n\t\tfloat64(stats.StaleConns),\n\t)\n}\n"
  },
  {
    "path": "extra/redisprometheus/go.mod",
    "content": "module github.com/redis/go-redis/extra/redisprometheus/v9\n\ngo 1.24\n\nreplace github.com/redis/go-redis/v9 => ../..\n\nrequire (\n\tgithub.com/prometheus/client_golang v1.14.0\n\tgithub.com/redis/go-redis/v9 v9.18.0\n)\n\nrequire (\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect\n\tgithub.com/prometheus/client_model v0.3.0 // indirect\n\tgithub.com/prometheus/common v0.39.0 // indirect\n\tgithub.com/prometheus/procfs v0.9.0 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n\tgoogle.golang.org/protobuf v1.33.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nretract (\n\tv9.7.2 // This version was accidentally released. Please use version 9.7.3 instead.\n\tv9.5.3 // This version was accidentally released. Please use version 9.6.0 instead.\n)\n"
  },
  {
    "path": "extra/redisprometheus/go.sum",
    "content": "github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=\ngithub.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=\ngithub.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=\ngithub.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=\ngithub.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=\ngithub.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=\ngithub.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=\ngithub.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=\ngithub.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "fuzz/fuzz.go",
    "content": "//go:build gofuzz\n// +build gofuzz\n\npackage fuzz\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tminDataLength     = 4\n\tredisAddr         = \":6379\"\n\tdialTimeout       = 10 * time.Second\n\treadTimeout       = 10 * time.Second\n\twriteTimeout      = 10 * time.Second\n\tpoolSize          = 10\n\tpoolTimeout       = 10 * time.Second\n\tscanCount         = 10\n\tmaxIterPercentage = 256 // Use first byte as percentage of data length\n)\n\nvar (\n\tctx = context.Background()\n\trdb *redis.Client\n)\n\ntype redisOperation func(key, value string)\n\nfunc init() {\n\trdb = redis.NewClient(&redis.Options{\n\t\tAddr:         redisAddr,\n\t\tDialTimeout:  dialTimeout,\n\t\tReadTimeout:  readTimeout,\n\t\tWriteTimeout: writeTimeout,\n\t\tPoolSize:     poolSize,\n\t\tPoolTimeout:  poolTimeout,\n\t})\n}\n\nfunc Fuzz(data []byte) int {\n\tif len(data) < minDataLength {\n\t\treturn -1\n\t}\n\n\tmaxIter := (int(data[0]) * len(data)) / maxIterPercentage\n\tif maxIter == 0 {\n\t\tmaxIter = 1 // Ensure at least one iteration\n\t}\n\n\toperations := []redisOperation{\n\t\tfunc(key, value string) { rdb.Set(ctx, key, value, 0).Err() },\n\t\tfunc(key, value string) { rdb.Get(ctx, key).Result() },\n\t\tfunc(key, value string) { rdb.Incr(ctx, key).Result() },\n\t\tfunc(key, value string) {\n\t\t\tvar cursor uint64\n\t\t\trdb.Scan(ctx, cursor, key, scanCount).Result()\n\t\t},\n\t}\n\n\tdataStr := string(data)\n\n\tfor i := 0; i < maxIter && i < len(data); i++ {\n\t\tstart := i % len(data)\n\t\tend := (i + 1) % len(data)\n\t\tif end <= start {\n\t\t\tend = len(data)\n\t\t}\n\n\t\tkey := dataStr[start:end]\n\t\topIndex := i % len(operations)\n\n\t\toperations[opIndex](key, dataStr)\n\t}\n\n\treturn 1\n}\n"
  },
  {
    "path": "generic_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n)\n\ntype GenericCmdable interface {\n\tDel(ctx context.Context, keys ...string) *IntCmd\n\tDump(ctx context.Context, key string) *StringCmd\n\tExists(ctx context.Context, keys ...string) *IntCmd\n\tExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd\n\tExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd\n\tExpireTime(ctx context.Context, key string) *DurationCmd\n\tExpireNX(ctx context.Context, key string, expiration time.Duration) *BoolCmd\n\tExpireXX(ctx context.Context, key string, expiration time.Duration) *BoolCmd\n\tExpireGT(ctx context.Context, key string, expiration time.Duration) *BoolCmd\n\tExpireLT(ctx context.Context, key string, expiration time.Duration) *BoolCmd\n\tKeys(ctx context.Context, pattern string) *StringSliceCmd\n\tMigrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd\n\tMove(ctx context.Context, key string, db int) *BoolCmd\n\tObjectFreq(ctx context.Context, key string) *IntCmd\n\tObjectRefCount(ctx context.Context, key string) *IntCmd\n\tObjectEncoding(ctx context.Context, key string) *StringCmd\n\tObjectIdleTime(ctx context.Context, key string) *DurationCmd\n\tPersist(ctx context.Context, key string) *BoolCmd\n\tPExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd\n\tPExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd\n\tPExpireTime(ctx context.Context, key string) *DurationCmd\n\tPTTL(ctx context.Context, key string) *DurationCmd\n\tRandomKey(ctx context.Context) *StringCmd\n\tRename(ctx context.Context, key, newkey string) *StatusCmd\n\tRenameNX(ctx context.Context, key, newkey string) *BoolCmd\n\tRestore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd\n\tRestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd\n\tSort(ctx context.Context, key string, sort *Sort) *StringSliceCmd\n\tSortRO(ctx context.Context, key string, sort *Sort) *StringSliceCmd\n\tSortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd\n\tSortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd\n\tTouch(ctx context.Context, keys ...string) *IntCmd\n\tTTL(ctx context.Context, key string) *DurationCmd\n\tType(ctx context.Context, key string) *StatusCmd\n\tCopy(ctx context.Context, sourceKey string, destKey string, db int, replace bool) *IntCmd\n\n\tScan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd\n\tScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *ScanCmd\n}\n\nfunc (c cmdable) Del(ctx context.Context, keys ...string) *IntCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"del\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Unlink(ctx context.Context, keys ...string) *IntCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"unlink\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Dump(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"dump\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Exists(ctx context.Context, keys ...string) *IntCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"exists\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Expire(ctx context.Context, key string, expiration time.Duration) *BoolCmd {\n\treturn c.expire(ctx, key, expiration, \"\")\n}\n\nfunc (c cmdable) ExpireNX(ctx context.Context, key string, expiration time.Duration) *BoolCmd {\n\treturn c.expire(ctx, key, expiration, \"NX\")\n}\n\nfunc (c cmdable) ExpireXX(ctx context.Context, key string, expiration time.Duration) *BoolCmd {\n\treturn c.expire(ctx, key, expiration, \"XX\")\n}\n\nfunc (c cmdable) ExpireGT(ctx context.Context, key string, expiration time.Duration) *BoolCmd {\n\treturn c.expire(ctx, key, expiration, \"GT\")\n}\n\nfunc (c cmdable) ExpireLT(ctx context.Context, key string, expiration time.Duration) *BoolCmd {\n\treturn c.expire(ctx, key, expiration, \"LT\")\n}\n\nfunc (c cmdable) expire(\n\tctx context.Context, key string, expiration time.Duration, mode string,\n) *BoolCmd {\n\targs := make([]interface{}, 3, 4)\n\targs[0] = \"expire\"\n\targs[1] = key\n\targs[2] = formatSec(ctx, expiration)\n\tif mode != \"\" {\n\t\targs = append(args, mode)\n\t}\n\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"expireat\", key, tm.Unix())\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ExpireTime(ctx context.Context, key string) *DurationCmd {\n\tcmd := NewDurationCmd(ctx, time.Second, \"expiretime\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Keys(ctx context.Context, pattern string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"keys\", pattern)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd {\n\tcmd := NewStatusCmd(\n\t\tctx,\n\t\t\"migrate\",\n\t\thost,\n\t\tport,\n\t\tkey,\n\t\tdb,\n\t\tformatMs(ctx, timeout),\n\t)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Move(ctx context.Context, key string, db int) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"move\", key, db)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ObjectFreq(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"object\", \"freq\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ObjectRefCount(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"object\", \"refcount\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ObjectEncoding(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"object\", \"encoding\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ObjectIdleTime(ctx context.Context, key string) *DurationCmd {\n\tcmd := NewDurationCmd(ctx, time.Second, \"object\", \"idletime\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Persist(ctx context.Context, key string) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"persist\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"pexpire\", key, formatMs(ctx, expiration))\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd {\n\tcmd := NewBoolCmd(\n\t\tctx,\n\t\t\"pexpireat\",\n\t\tkey,\n\t\ttm.UnixNano()/int64(time.Millisecond),\n\t)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PExpireTime(ctx context.Context, key string) *DurationCmd {\n\tcmd := NewDurationCmd(ctx, time.Millisecond, \"pexpiretime\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PTTL(ctx context.Context, key string) *DurationCmd {\n\tcmd := NewDurationCmd(ctx, time.Millisecond, \"pttl\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) RandomKey(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"randomkey\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Rename(ctx context.Context, key, newkey string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"rename\", key, newkey)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) RenameNX(ctx context.Context, key, newkey string) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"renamenx\", key, newkey)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Restore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd {\n\tcmd := NewStatusCmd(\n\t\tctx,\n\t\t\"restore\",\n\t\tkey,\n\t\tformatMs(ctx, ttl),\n\t\tvalue,\n\t)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd {\n\tcmd := NewStatusCmd(\n\t\tctx,\n\t\t\"restore\",\n\t\tkey,\n\t\tformatMs(ctx, ttl),\n\t\tvalue,\n\t\t\"replace\",\n\t)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype Sort struct {\n\tBy            string\n\tOffset, Count int64\n\tGet           []string\n\tOrder         string\n\tAlpha         bool\n}\n\nfunc (sort *Sort) args(command, key string) []interface{} {\n\targs := []interface{}{command, key}\n\n\tif sort.By != \"\" {\n\t\targs = append(args, \"by\", sort.By)\n\t}\n\tif sort.Offset != 0 || sort.Count != 0 {\n\t\targs = append(args, \"limit\", sort.Offset, sort.Count)\n\t}\n\tfor _, get := range sort.Get {\n\t\targs = append(args, \"get\", get)\n\t}\n\tif sort.Order != \"\" {\n\t\targs = append(args, sort.Order)\n\t}\n\tif sort.Alpha {\n\t\targs = append(args, \"alpha\")\n\t}\n\treturn args\n}\n\nfunc (c cmdable) SortRO(ctx context.Context, key string, sort *Sort) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, sort.args(\"sort_ro\", key)...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Sort(ctx context.Context, key string, sort *Sort) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, sort.args(\"sort\", key)...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) SortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd {\n\targs := sort.args(\"sort\", key)\n\tif store != \"\" {\n\t\targs = append(args, \"store\", store)\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) SortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd {\n\tcmd := NewSliceCmd(ctx, sort.args(\"sort\", key)...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Touch(ctx context.Context, keys ...string) *IntCmd {\n\targs := make([]interface{}, len(keys)+1)\n\targs[0] = \"touch\"\n\tfor i, key := range keys {\n\t\targs[i+1] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) TTL(ctx context.Context, key string) *DurationCmd {\n\tcmd := NewDurationCmd(ctx, time.Second, \"ttl\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Type(ctx context.Context, key string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"type\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Copy(ctx context.Context, sourceKey, destKey string, db int, replace bool) *IntCmd {\n\targs := []interface{}{\"copy\", sourceKey, destKey, \"DB\", db}\n\tif replace {\n\t\targs = append(args, \"REPLACE\")\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n//------------------------------------------------------------------------------\n\nfunc (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd {\n\targs := []interface{}{\"scan\", cursor}\n\tif match != \"\" {\n\t\targs = append(args, \"match\", match)\n\t}\n\tif count > 0 {\n\t\targs = append(args, \"count\", count)\n\t}\n\tcmd := NewScanCmd(ctx, c, args...)\n\tif hashtag.Present(match) {\n\t\tcmd.SetFirstKeyPos(3)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *ScanCmd {\n\targs := []interface{}{\"scan\", cursor}\n\tif match != \"\" {\n\t\targs = append(args, \"match\", match)\n\t}\n\tif count > 0 {\n\t\targs = append(args, \"count\", count)\n\t}\n\tif keyType != \"\" {\n\t\targs = append(args, \"type\", keyType)\n\t}\n\tcmd := NewScanCmd(ctx, c, args...)\n\tif hashtag.Present(match) {\n\t\tcmd.SetFirstKeyPos(3)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "geo_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\ntype GeoCmdable interface {\n\tGeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd\n\tGeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd\n\tGeoRadius(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd\n\tGeoRadiusStore(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *IntCmd\n\tGeoRadiusByMember(ctx context.Context, key, member string, query *GeoRadiusQuery) *GeoLocationCmd\n\tGeoRadiusByMemberStore(ctx context.Context, key, member string, query *GeoRadiusQuery) *IntCmd\n\tGeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd\n\tGeoSearchLocation(ctx context.Context, key string, q *GeoSearchLocationQuery) *GeoSearchLocationCmd\n\tGeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd\n\tGeoDist(ctx context.Context, key string, member1, member2, unit string) *FloatCmd\n\tGeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd\n}\n\nfunc (c cmdable) GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd {\n\targs := make([]interface{}, 2+3*len(geoLocation))\n\targs[0] = \"geoadd\"\n\targs[1] = key\n\tfor i, eachLoc := range geoLocation {\n\t\targs[2+3*i] = eachLoc.Longitude\n\t\targs[2+3*i+1] = eachLoc.Latitude\n\t\targs[2+3*i+2] = eachLoc.Name\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// GeoRadius queries a geospatial index for members within a distance from a coordinate.\n// This is a read-only variant that does not support Store or StoreDist options.\n//\n// Deprecated: Use GeoSearch with BYRADIUS argument instead as of Redis 6.2.0.\nfunc (c cmdable) GeoRadius(\n\tctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery,\n) *GeoLocationCmd {\n\tcmd := NewGeoLocationCmd(ctx, query, \"georadius_ro\", key, longitude, latitude)\n\tif query.Store != \"\" || query.StoreDist != \"\" {\n\t\tcmd.SetErr(errors.New(\"GeoRadius does not support Store or StoreDist\"))\n\t\treturn cmd\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// GeoRadiusStore is a writing GEORADIUS command.\nfunc (c cmdable) GeoRadiusStore(\n\tctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery,\n) *IntCmd {\n\targs := geoLocationArgs(query, \"georadius\", key, longitude, latitude)\n\tcmd := NewIntCmd(ctx, args...)\n\tif query.Store == \"\" && query.StoreDist == \"\" {\n\t\tcmd.SetErr(errors.New(\"GeoRadiusStore requires Store or StoreDist\"))\n\t\treturn cmd\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// GeoRadiusByMember queries a geospatial index for members within a distance from a member.\n// This is a read-only variant that does not support Store or StoreDist options.\n//\n// Deprecated: Use GeoSearch with BYRADIUS and FROMMEMBER arguments instead as of Redis 6.2.0.\nfunc (c cmdable) GeoRadiusByMember(\n\tctx context.Context, key, member string, query *GeoRadiusQuery,\n) *GeoLocationCmd {\n\tcmd := NewGeoLocationCmd(ctx, query, \"georadiusbymember_ro\", key, member)\n\tif query.Store != \"\" || query.StoreDist != \"\" {\n\t\tcmd.SetErr(errors.New(\"GeoRadiusByMember does not support Store or StoreDist\"))\n\t\treturn cmd\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// GeoRadiusByMemberStore is a writing GEORADIUSBYMEMBER command.\nfunc (c cmdable) GeoRadiusByMemberStore(\n\tctx context.Context, key, member string, query *GeoRadiusQuery,\n) *IntCmd {\n\targs := geoLocationArgs(query, \"georadiusbymember\", key, member)\n\tcmd := NewIntCmd(ctx, args...)\n\tif query.Store == \"\" && query.StoreDist == \"\" {\n\t\tcmd.SetErr(errors.New(\"GeoRadiusByMemberStore requires Store or StoreDist\"))\n\t\treturn cmd\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) GeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd {\n\targs := make([]interface{}, 0, 13)\n\targs = append(args, \"geosearch\", key)\n\targs = geoSearchArgs(q, args)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) GeoSearchLocation(\n\tctx context.Context, key string, q *GeoSearchLocationQuery,\n) *GeoSearchLocationCmd {\n\targs := make([]interface{}, 0, 16)\n\targs = append(args, \"geosearch\", key)\n\targs = geoSearchLocationArgs(q, args)\n\tcmd := NewGeoSearchLocationCmd(ctx, q, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) GeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd {\n\targs := make([]interface{}, 0, 15)\n\targs = append(args, \"geosearchstore\", store, key)\n\targs = geoSearchArgs(&q.GeoSearchQuery, args)\n\tif q.StoreDist {\n\t\targs = append(args, \"storedist\")\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) GeoDist(\n\tctx context.Context, key string, member1, member2, unit string,\n) *FloatCmd {\n\tif unit == \"\" {\n\t\tunit = \"km\"\n\t}\n\tcmd := NewFloatCmd(ctx, \"geodist\", key, member1, member2, unit)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd {\n\targs := make([]interface{}, 2+len(members))\n\targs[0] = \"geohash\"\n\targs[1] = key\n\tfor i, member := range members {\n\t\targs[2+i] = member\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd {\n\targs := make([]interface{}, 2+len(members))\n\targs[0] = \"geopos\"\n\targs[1] = key\n\tfor i, member := range members {\n\t\targs[2+i] = member\n\t}\n\tcmd := NewGeoPosCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/redis/go-redis/v9\n\ngo 1.24\n\nrequire (\n\tgithub.com/bsm/ginkgo/v2 v2.12.0\n\tgithub.com/bsm/gomega v1.27.10\n\tgithub.com/cespare/xxhash/v2 v2.3.0\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f\n\tgithub.com/zeebo/xxh3 v1.1.0\n)\n\nrequire go.uber.org/atomic v1.11.0\n\nrequire (\n\tgithub.com/klauspost/cpuid/v2 v2.2.10 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n)\n\nretract (\n\tv9.15.1 // This version is used to retract v9.15.0\n\tv9.15.0 // This version was accidentally released. It is identical to 9.15.0-beta.2\n\tv9.7.2 // This version was accidentally released. Please use version 9.7.3 instead.\n\tv9.5.4 // This version was accidentally released. Please use version 9.6.0 instead.\n\tv9.5.3 // This version was accidentally released. Please use version 9.6.0 instead.\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=\ngithub.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\n"
  },
  {
    "path": "hash_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n)\n\ntype HashCmdable interface {\n\tHDel(ctx context.Context, key string, fields ...string) *IntCmd\n\tHExists(ctx context.Context, key, field string) *BoolCmd\n\tHGet(ctx context.Context, key, field string) *StringCmd\n\tHGetAll(ctx context.Context, key string) *MapStringStringCmd\n\tHGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd\n\tHGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd\n\tHGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd\n\tHIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd\n\tHIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd\n\tHKeys(ctx context.Context, key string) *StringSliceCmd\n\tHLen(ctx context.Context, key string) *IntCmd\n\tHMGet(ctx context.Context, key string, fields ...string) *SliceCmd\n\tHSet(ctx context.Context, key string, values ...interface{}) *IntCmd\n\tHMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd\n\tHSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd\n\tHSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd\n\tHSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd\n\tHScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd\n\tHScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd\n\tHVals(ctx context.Context, key string) *StringSliceCmd\n\tHRandField(ctx context.Context, key string, count int) *StringSliceCmd\n\tHRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd\n\tHStrLen(ctx context.Context, key, field string) *IntCmd\n\tHExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd\n\tHExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd\n\tHPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd\n\tHPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd\n\tHExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd\n\tHExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd\n\tHPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd\n\tHPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd\n\tHPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd\n\tHExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd\n\tHPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd\n\tHTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd\n\tHPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd\n}\n\nfunc (c cmdable) HDel(ctx context.Context, key string, fields ...string) *IntCmd {\n\targs := make([]interface{}, 2+len(fields))\n\targs[0] = \"hdel\"\n\targs[1] = key\n\tfor i, field := range fields {\n\t\targs[2+i] = field\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HExists(ctx context.Context, key, field string) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"hexists\", key, field)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HGet(ctx context.Context, key, field string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"hget\", key, field)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HGetAll(ctx context.Context, key string) *MapStringStringCmd {\n\tcmd := NewMapStringStringCmd(ctx, \"hgetall\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"hincrby\", key, field, incr)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd {\n\tcmd := NewFloatCmd(ctx, \"hincrbyfloat\", key, field, incr)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HKeys(ctx context.Context, key string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"hkeys\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HLen(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"hlen\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HMGet returns the values for the specified fields in the hash stored at key.\n// It returns an interface{} to distinguish between empty string and nil value.\nfunc (c cmdable) HMGet(ctx context.Context, key string, fields ...string) *SliceCmd {\n\targs := make([]interface{}, 2+len(fields))\n\targs[0] = \"hmget\"\n\targs[1] = key\n\tfor i, field := range fields {\n\t\targs[2+i] = field\n\t}\n\tcmd := NewSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HSet accepts values in following formats:\n//\n//   - HSet(ctx, \"myhash\", \"key1\", \"value1\", \"key2\", \"value2\")\n//\n//   - HSet(ctx, \"myhash\", []string{\"key1\", \"value1\", \"key2\", \"value2\"})\n//\n//   - HSet(ctx, \"myhash\", map[string]interface{}{\"key1\": \"value1\", \"key2\": \"value2\"})\n//\n//     Playing struct With \"redis\" tag.\n//     type MyHash struct { Key1 string `redis:\"key1\"`; Key2 int `redis:\"key2\"` }\n//\n//   - HSet(ctx, \"myhash\", MyHash{\"value1\", \"value2\"}) Warn: redis-server >= 4.0\n//\n//     For struct, can be a structure pointer type, we only parse the field whose tag is redis.\n//     if you don't want the field to be read, you can use the `redis:\"-\"` flag to ignore it,\n//     or you don't need to set the redis tag.\n//     For the type of structure field, we only support simple data types:\n//     string, int/uint(8,16,32,64), float(32,64), time.Time(to RFC3339Nano), time.Duration(to Nanoseconds ),\n//     if you are other more complex or custom data types, please implement the encoding.BinaryMarshaler interface.\n//\n// Note that in older versions of Redis server(redis-server < 4.0), HSet only supports a single key-value pair.\n// redis-docs: https://redis.io/commands/hset (Starting with Redis version 4.0.0: Accepts multiple field and value arguments.)\n// If you are using a Struct type and the number of fields is greater than one,\n// you will receive an error similar to \"ERR wrong number of arguments\", you can use HMSet as a substitute.\nfunc (c cmdable) HSet(ctx context.Context, key string, values ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"hset\"\n\targs[1] = key\n\targs = appendArgs(args, values)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HMSet is a deprecated version of HSet left for compatibility with Redis 3.\nfunc (c cmdable) HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"hmset\"\n\targs[1] = key\n\targs = appendArgs(args, values)\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"hsetnx\", key, field, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HVals(ctx context.Context, key string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"hvals\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HRandField redis-server version >= 6.2.0.\nfunc (c cmdable) HRandField(ctx context.Context, key string, count int) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"hrandfield\", key, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HRandFieldWithValues redis-server version >= 6.2.0.\nfunc (c cmdable) HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd {\n\tcmd := NewKeyValueSliceCmd(ctx, \"hrandfield\", key, count, \"withvalues\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {\n\targs := []interface{}{\"hscan\", key, cursor}\n\tif match != \"\" {\n\t\targs = append(args, \"match\", match)\n\t}\n\tif count > 0 {\n\t\targs = append(args, \"count\", count)\n\t}\n\tcmd := NewScanCmd(ctx, c, args...)\n\tif hashtag.Present(match) {\n\t\tcmd.SetFirstKeyPos(4)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HStrLen(ctx context.Context, key, field string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"hstrlen\", key, field)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\nfunc (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {\n\targs := []interface{}{\"hscan\", key, cursor}\n\tif match != \"\" {\n\t\targs = append(args, \"match\", match)\n\t}\n\tif count > 0 {\n\t\targs = append(args, \"count\", count)\n\t}\n\targs = append(args, \"novalues\")\n\tcmd := NewScanCmd(ctx, c, args...)\n\tif hashtag.Present(match) {\n\t\tcmd.SetFirstKeyPos(4)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype HExpireArgs struct {\n\tNX bool\n\tXX bool\n\tGT bool\n\tLT bool\n}\n\n// HExpire - Sets the expiration time for specified fields in a hash in seconds.\n// The command constructs an argument list starting with \"HEXPIRE\", followed by the key, duration, any conditional flags, and the specified fields.\n// Available since Redis 7.4 CE.\n// For more information refer to [HEXPIRE Documentation].\n//\n// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/\nfunc (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HEXPIRE\", key, formatSec(ctx, expiration), \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds.\n// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.\n// The command constructs an argument list starting with \"HEXPIRE\", followed by the key, duration, any conditional flags, and the specified fields.\n// Available since Redis 7.4 CE.\n// For more information refer to [HEXPIRE Documentation].\n//\n// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/\nfunc (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HEXPIRE\", key, formatSec(ctx, expiration)}\n\n\t// only if one argument is true, we can add it to the args\n\t// if more than one argument is true, it will cause an error\n\tif expirationArgs.NX {\n\t\targs = append(args, \"NX\")\n\t} else if expirationArgs.XX {\n\t\targs = append(args, \"XX\")\n\t} else if expirationArgs.GT {\n\t\targs = append(args, \"GT\")\n\t} else if expirationArgs.LT {\n\t\targs = append(args, \"LT\")\n\t}\n\n\targs = append(args, \"FIELDS\", len(fields))\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HPExpire - Sets the expiration time for specified fields in a hash in milliseconds.\n// Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields.\n// The command modifies the standard time.Duration to milliseconds for the Redis command.\n// Available since Redis 7.4 CE.\n// For more information refer to [HPEXPIRE Documentation].\n//\n// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/\nfunc (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HPEXPIRE\", key, formatMs(ctx, expiration), \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HPExpireWithArgs - Sets the expiration time for specified fields in a hash in milliseconds.\n// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.\n// The command constructs an argument list starting with \"HPEXPIRE\", followed by the key, duration, any conditional flags, and the specified fields.\n// Available since Redis 7.4 CE.\n// For more information refer to [HPEXPIRE Documentation].\n//\n// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/\nfunc (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HPEXPIRE\", key, formatMs(ctx, expiration)}\n\n\t// only if one argument is true, we can add it to the args\n\t// if more than one argument is true, it will cause an error\n\tif expirationArgs.NX {\n\t\targs = append(args, \"NX\")\n\t} else if expirationArgs.XX {\n\t\targs = append(args, \"XX\")\n\t} else if expirationArgs.GT {\n\t\targs = append(args, \"GT\")\n\t} else if expirationArgs.LT {\n\t\targs = append(args, \"LT\")\n\t}\n\n\targs = append(args, \"FIELDS\", len(fields))\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds.\n// Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields.\n// The command sets absolute expiration times based on the UNIX timestamp provided.\n// Available since Redis 7.4 CE.\n// For more information refer to [HExpireAt Documentation].\n//\n// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/\nfunc (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {\n\n\targs := []interface{}{\"HEXPIREAT\", key, tm.Unix(), \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HEXPIREAT\", key, tm.Unix()}\n\n\t// only if one argument is true, we can add it to the args\n\t// if more than one argument is true, it will cause an error\n\tif expirationArgs.NX {\n\t\targs = append(args, \"NX\")\n\t} else if expirationArgs.XX {\n\t\targs = append(args, \"XX\")\n\t} else if expirationArgs.GT {\n\t\targs = append(args, \"GT\")\n\t} else if expirationArgs.LT {\n\t\targs = append(args, \"LT\")\n\t}\n\n\targs = append(args, \"FIELDS\", len(fields))\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds.\n// Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds.\n// Available since Redis 7.4 CE.\n// For more information refer to [HExpireAt Documentation].\n//\n// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/\nfunc (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HPEXPIREAT\", key, tm.UnixNano() / int64(time.Millisecond), \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HPEXPIREAT\", key, tm.UnixNano() / int64(time.Millisecond)}\n\n\t// only if one argument is true, we can add it to the args\n\t// if more than one argument is true, it will cause an error\n\tif expirationArgs.NX {\n\t\targs = append(args, \"NX\")\n\t} else if expirationArgs.XX {\n\t\targs = append(args, \"XX\")\n\t} else if expirationArgs.GT {\n\t\targs = append(args, \"GT\")\n\t} else if expirationArgs.LT {\n\t\targs = append(args, \"LT\")\n\t}\n\n\targs = append(args, \"FIELDS\", len(fields))\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HPersist - Removes the expiration time from specified fields in a hash.\n// Accepts a key and the fields themselves.\n// This command ensures that each field specified will have its expiration removed if present.\n// Available since Redis 7.4 CE.\n// For more information refer to [HPersist Documentation].\n//\n// [HPersist Documentation]: https://redis.io/commands/hpersist/\nfunc (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HPERSIST\", key, \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds.\n// Requires a key and the fields themselves to fetch their expiration timestamps.\n// This command returns the expiration times for each field or error/status codes for each field as specified.\n// Available since Redis 7.4 CE.\n// For more information refer to [HExpireTime Documentation].\n//\n// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/\n// For more information - https://redis.io/commands/hexpiretime/\nfunc (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HEXPIRETIME\", key, \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds.\n// Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters.\n// Provides the expiration timestamp for each field in milliseconds.\n// Available since Redis 7.4 CE.\n// For more information refer to [HExpireTime Documentation].\n//\n// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/\n// For more information - https://redis.io/commands/hexpiretime/\nfunc (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HPEXPIRETIME\", key, \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds.\n// Requires a key and the fields themselves. It returns the TTL for each specified field.\n// This command fetches the TTL in seconds for each field or returns error/status codes as appropriate.\n// Available since Redis 7.4 CE.\n// For more information refer to [HTTL Documentation].\n//\n// [HTTL Documentation]: https://redis.io/commands/httl/\nfunc (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HTTL\", key, \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds.\n// Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields.\n// This command provides the TTL in milliseconds for each field or returns error/status codes as needed.\n// Available since Redis 7.4 CE.\n// For more information refer to [HPTTL Documentation].\n//\n// [HPTTL Documentation]: https://redis.io/commands/hpttl/\n// For more information - https://redis.io/commands/hpttl/\nfunc (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {\n\targs := []interface{}{\"HPTTL\", key, \"FIELDS\", len(fields)}\n\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd {\n\targs := []interface{}{\"HGETDEL\", key, \"FIELDS\", len(fields)}\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd {\n\targs := []interface{}{\"HGETEX\", key, \"FIELDS\", len(fields)}\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// HGetEXExpirationType represents an expiration option for the HGETEX command.\ntype HGetEXExpirationType string\n\nconst (\n\tHGetEXExpirationEX      HGetEXExpirationType = \"EX\"\n\tHGetEXExpirationPX      HGetEXExpirationType = \"PX\"\n\tHGetEXExpirationEXAT    HGetEXExpirationType = \"EXAT\"\n\tHGetEXExpirationPXAT    HGetEXExpirationType = \"PXAT\"\n\tHGetEXExpirationPERSIST HGetEXExpirationType = \"PERSIST\"\n)\n\ntype HGetEXOptions struct {\n\tExpirationType HGetEXExpirationType\n\tExpirationVal  int64\n}\n\nfunc (c cmdable) HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd {\n\targs := []interface{}{\"HGETEX\", key}\n\tif options.ExpirationType != \"\" {\n\t\targs = append(args, string(options.ExpirationType))\n\t\tif options.ExpirationType != HGetEXExpirationPERSIST {\n\t\t\targs = append(args, options.ExpirationVal)\n\t\t}\n\t}\n\n\targs = append(args, \"FIELDS\", len(fields))\n\tfor _, field := range fields {\n\t\targs = append(args, field)\n\t}\n\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype HSetEXCondition string\n\nconst (\n\tHSetEXFNX HSetEXCondition = \"FNX\" // Only set the fields if none of them already exist.\n\tHSetEXFXX HSetEXCondition = \"FXX\" // Only set the fields if all already exist.\n)\n\ntype HSetEXExpirationType string\n\nconst (\n\tHSetEXExpirationEX      HSetEXExpirationType = \"EX\"\n\tHSetEXExpirationPX      HSetEXExpirationType = \"PX\"\n\tHSetEXExpirationEXAT    HSetEXExpirationType = \"EXAT\"\n\tHSetEXExpirationPXAT    HSetEXExpirationType = \"PXAT\"\n\tHSetEXExpirationKEEPTTL HSetEXExpirationType = \"KEEPTTL\"\n)\n\ntype HSetEXOptions struct {\n\tCondition      HSetEXCondition\n\tExpirationType HSetEXExpirationType\n\tExpirationVal  int64\n}\n\nfunc (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd {\n\targs := []interface{}{\"HSETEX\", key, \"FIELDS\", len(fieldsAndValues) / 2}\n\tfor _, field := range fieldsAndValues {\n\t\targs = append(args, field)\n\t}\n\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd {\n\targs := []interface{}{\"HSETEX\", key}\n\tif options.Condition != \"\" {\n\t\targs = append(args, string(options.Condition))\n\t}\n\tif options.ExpirationType != \"\" {\n\t\targs = append(args, string(options.ExpirationType))\n\t\tif options.ExpirationType != HSetEXExpirationKEEPTTL {\n\t\t\targs = append(args, options.ExpirationVal)\n\t\t}\n\t}\n\targs = append(args, \"FIELDS\", len(fieldsAndValues)/2)\n\tfor _, field := range fieldsAndValues {\n\t\targs = append(args, field)\n\t}\n\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "helper/helper.go",
    "content": "package helper\n\nimport (\n\t\"github.com/redis/go-redis/v9/internal/util\"\n\t\"github.com/zeebo/xxh3\"\n)\n\nfunc ParseFloat(s string) (float64, error) {\n\treturn util.ParseStringToFloat(s)\n}\n\nfunc MustParseFloat(s string) float64 {\n\treturn util.MustParseFloat(s)\n}\n\n// DigestBytes computes the xxh3 hash of the given byte slice.\n// This produces the same hash as the Redis DIGEST command, allowing you to\n// calculate digests client-side without making a Redis call.\n// This is useful for optimistic locking with SetIFDEQ, SetIFDNE, and DelExArgs.\nfunc DigestBytes(data []byte) uint64 {\n\treturn xxh3.Hash(data)\n}\n\n// DigestString computes the xxh3 hash of the given string.\n// This produces the same hash as the Redis DIGEST command, allowing you to\n// calculate digests client-side without making a Redis call.\n// This is useful for optimistic locking with SetIFDEQ, SetIFDNE, and DelExArgs.\nfunc DigestString(s string) uint64 {\n\treturn xxh3.HashString(s)\n}\n"
  },
  {
    "path": "helper/helper_test.go",
    "content": "package helper\n\nimport \"testing\"\n\n// Golden values from Redis DIGEST command:\n// redis-cli SET testkey myvalue && redis-cli DIGEST testkey\n// Returns: \"5a32b091fa5dafe7\" (hex) = 6499451353266237415 (decimal)\n//\n// Redis source (t_string.c):\n//\n//\thash = XXH3_64bits(o->ptr, sdslen(o->ptr));  // No seed parameter!\nconst (\n\tgoldenTestValue   = \"myvalue\"\n\tgoldenRedisDigest = uint64(0x5a32b091fa5dafe7) // 6499451353266237415\n)\n\nfunc TestDigestString_RedisCompatibility(t *testing.T) {\n\tdigest := DigestString(goldenTestValue)\n\n\tif digest != goldenRedisDigest {\n\t\tt.Errorf(\"DigestString(%q) = 0x%016x, want 0x%016x (Redis DIGEST)\",\n\t\t\tgoldenTestValue, digest, goldenRedisDigest)\n\t}\n}\n\nfunc TestDigestBytes_RedisCompatibility(t *testing.T) {\n\tdigest := DigestBytes([]byte(goldenTestValue))\n\n\tif digest != goldenRedisDigest {\n\t\tt.Errorf(\"DigestBytes(%q) = 0x%016x, want 0x%016x (Redis DIGEST)\",\n\t\t\tgoldenTestValue, digest, goldenRedisDigest)\n\t}\n}\n\nfunc TestDigestString_EqualsDigestBytes(t *testing.T) {\n\ttestCases := []string{\n\t\t\"\",\n\t\t\"a\",\n\t\t\"hello\",\n\t\t\"hello world\",\n\t\t\"The quick brown fox jumps over the lazy dog\",\n\t\tstring(make([]byte, 1024)),\n\t}\n\n\tfor _, s := range testCases {\n\t\tstringDigest := DigestString(s)\n\t\tbytesDigest := DigestBytes([]byte(s))\n\n\t\tif stringDigest != bytesDigest {\n\t\t\tt.Errorf(\"DigestString(%q) = 0x%016x, DigestBytes = 0x%016x, mismatch!\",\n\t\t\t\ts, stringDigest, bytesDigest)\n\t\t}\n\t}\n}\n\n// Benchmark to verify performance characteristics\nfunc BenchmarkDigestString(b *testing.B) {\n\tsizes := []struct {\n\t\tname string\n\t\tdata string\n\t}{\n\t\t{\"8B\", \"12345678\"},\n\t\t{\"64B\", \"0123456789012345678901234567890123456789012345678901234567890123\"},\n\t\t{\"1KB\", string(make([]byte, 1024))},\n\t}\n\n\tfor _, tc := range sizes {\n\t\tb.Run(tc.name, func(b *testing.B) {\n\t\t\tb.SetBytes(int64(len(tc.data)))\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_ = DigestString(tc.data)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkDigestBytes(b *testing.B) {\n\tsizes := []struct {\n\t\tname string\n\t\tdata []byte\n\t}{\n\t\t{\"8B\", make([]byte, 8)},\n\t\t{\"64B\", make([]byte, 64)},\n\t\t{\"1KB\", make([]byte, 1024)},\n\t}\n\n\tfor _, tc := range sizes {\n\t\tb.Run(tc.name, func(b *testing.B) {\n\t\t\tb.SetBytes(int64(len(tc.data)))\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_ = DigestBytes(tc.data)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "hotkeys_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n)\n\n// HOTKEYS commands are only available on standalone *Client instances.\n// They are NOT available on ClusterClient, Ring, or UniversalClient because\n// HOTKEYS is a stateful command requiring session affinity - all operations\n// (START, GET, STOP, RESET) must be sent to the same Redis node.\n//\n// If you are using UniversalClient and need HOTKEYS functionality, you must\n// type assert to *Client first:\n//\n//\tif client, ok := universalClient.(*redis.Client); ok {\n//\t    result, err := client.HotKeysStart(ctx, args)\n//\t    // ...\n//\t}\n\n// HotKeysMetric represents the metrics that can be tracked by the HOTKEYS command.\ntype HotKeysMetric string\n\nconst (\n\t// HotKeysMetricCPU tracks CPU time spent on the key (in microseconds).\n\tHotKeysMetricCPU HotKeysMetric = \"CPU\"\n\t// HotKeysMetricNET tracks network bytes used by the key (ingress + egress + replication).\n\tHotKeysMetricNET HotKeysMetric = \"NET\"\n)\n\n// HotKeysStartArgs contains the arguments for the HOTKEYS START command.\n// This command is only available on standalone clients due to its stateful nature\n// requiring session affinity. It must NOT be used on cluster or pooled clients.\ntype HotKeysStartArgs struct {\n\t// Metrics to track. At least one must be specified.\n\tMetrics []HotKeysMetric\n\t// Count is the number of top keys to report.\n\t// Default: 10, Min: 10, Max: 64\n\tCount uint8\n\t// Duration is the auto-stop tracking after this many seconds.\n\t// Default: 0 (no auto-stop)\n\tDuration int64\n\t// Sample is the sample ratio - track keys with probability 1/sample.\n\t// Default: 1 (track every key), Min: 1\n\tSample int64\n\t// Slots specifies specific hash slots to track (0-16383).\n\t// All specified slots must be hosted by the receiving node.\n\t// If not specified, all slots are tracked.\n\tSlots []uint16\n}\n\n// ErrHotKeysNoMetrics is returned when HotKeysStart is called without any metrics specified.\nvar ErrHotKeysNoMetrics = errors.New(\"redis: at least one metric must be specified for HOTKEYS START\")\n\n// HotKeysStart starts collecting hotkeys data.\n// At least one metric must be specified in args.Metrics.\n// This command is only available on standalone clients.\nfunc (c *Client) HotKeysStart(ctx context.Context, args *HotKeysStartArgs) *StatusCmd {\n\tcmdArgs := make([]interface{}, 0, 16)\n\tcmdArgs = append(cmdArgs, \"hotkeys\", \"start\")\n\n\t// Validate that at least one metric is specified\n\tif len(args.Metrics) == 0 {\n\t\tcmd := NewStatusCmd(ctx, cmdArgs...)\n\t\tcmd.SetErr(ErrHotKeysNoMetrics)\n\t\treturn cmd\n\t}\n\n\tcmdArgs = append(cmdArgs, \"metrics\", len(args.Metrics))\n\tfor _, metric := range args.Metrics {\n\t\tcmdArgs = append(cmdArgs, strings.ToLower(string(metric)))\n\t}\n\n\tif args.Count > 0 {\n\t\tcmdArgs = append(cmdArgs, \"count\", args.Count)\n\t}\n\n\tif args.Duration > 0 {\n\t\tcmdArgs = append(cmdArgs, \"duration\", args.Duration)\n\t}\n\n\tif args.Sample > 0 {\n\t\tcmdArgs = append(cmdArgs, \"sample\", args.Sample)\n\t}\n\n\tif len(args.Slots) > 0 {\n\t\tcmdArgs = append(cmdArgs, \"slots\", len(args.Slots))\n\t\tfor _, slot := range args.Slots {\n\t\t\tcmdArgs = append(cmdArgs, slot)\n\t\t}\n\t}\n\n\tcmd := NewStatusCmd(ctx, cmdArgs...)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// HotKeysStop stops the ongoing hotkeys collection session.\n// This command is only available on standalone clients.\nfunc (c *Client) HotKeysStop(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"hotkeys\", \"stop\")\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// HotKeysReset discards the last hotkeys collection session results.\n// Returns an error if tracking is currently active.\n// This command is only available on standalone clients.\nfunc (c *Client) HotKeysReset(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"hotkeys\", \"reset\")\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// HotKeysGet retrieves the results of the ongoing or last hotkeys collection session.\n// This command is only available on standalone clients.\nfunc (c *Client) HotKeysGet(ctx context.Context) *HotKeysCmd {\n\tcmd := NewHotKeysCmd(ctx, \"hotkeys\", \"get\")\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "hotkeys_commands_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"HotKeys Commands\", func() {\n\tctx := context.TODO()\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tDescribe(\"HOTKEYS\", func() {\n\t\tIt(\"should start, get, stop, and reset hotkeys tracking\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics:  []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},\n\t\t\t\tCount:    10,\n\t\t\t\tDuration: 0,\n\t\t\t\tSample:   1,\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(start.Val()).To(Equal(\"OK\"))\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tclient.Set(ctx, \"hotkey1\", \"value1\", 0)\n\t\t\t\tclient.Get(ctx, \"hotkey1\")\n\t\t\t}\n\t\t\tfor i := 0; i < 50; i++ {\n\t\t\t\tclient.Set(ctx, \"hotkey2\", \"value2\", 0)\n\t\t\t\tclient.Get(ctx, \"hotkey2\")\n\t\t\t}\n\t\t\tfor i := 0; i < 25; i++ {\n\t\t\t\tclient.Set(ctx, \"hotkey3\", \"value3\", 0)\n\t\t\t\tclient.Get(ctx, \"hotkey3\")\n\t\t\t}\n\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\tget := client.HotKeysGet(ctx)\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tresult := get.Val()\n\t\t\tExpect(result).NotTo(BeNil())\n\t\t\tExpect(result.TrackingActive).To(BeTrue())\n\t\t\tExpect(result.SampleRatio).To(Equal(uint8(1)))\n\t\t\t// Verify that collection start time is set (not zero)\n\t\t\tExpect(result.CollectionStartTime.IsZero()).To(BeFalse())\n\t\t\t// Verify that we have some CPU time data after running commands\n\t\t\tExpect(result.AllCommandsAllSlots).To(BeNumerically(\">\", 0))\n\t\t\t// Verify that we have hot keys data (we ran 350 commands on 3 keys)\n\t\t\tExpect(len(result.ByCPUTime)).To(BeNumerically(\">\", 0))\n\t\t\tExpect(len(result.ByNetBytes)).To(BeNumerically(\">\", 0))\n\t\t\t// Verify that the first hot key entry has a non-empty key\n\t\t\tif len(result.ByCPUTime) > 0 {\n\t\t\t\tExpect(result.ByCPUTime[0].Key).NotTo(BeEmpty())\n\t\t\t\tExpect(result.ByCPUTime[0].Value).NotTo(BeNil())\n\t\t\t}\n\n\t\t\tstop := client.HotKeysStop(ctx)\n\t\t\tExpect(stop.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(stop.Val()).To(Equal(\"OK\"))\n\n\t\t\tget2 := client.HotKeysGet(ctx)\n\t\t\tExpect(get2.Err()).NotTo(HaveOccurred())\n\t\t\tresult2 := get2.Val()\n\t\t\tExpect(result2).NotTo(BeNil())\n\t\t\tExpect(result2.TrackingActive).To(BeFalse())\n\t\t\t// After stopping, collection duration should be set\n\t\t\tExpect(result2.CollectionDuration).To(BeNumerically(\">\", 0))\n\n\t\t\treset := client.HotKeysReset(ctx)\n\t\t\tExpect(reset.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(reset.Val()).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should start hotkeys tracking with CPU metric only\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU},\n\t\t\t\tCount:   5,\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(start.Val()).To(Equal(\"OK\"))\n\n\t\t\tstop := client.HotKeysStop(ctx)\n\t\t\tExpect(stop.Err()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should start hotkeys tracking with NET metric only\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics: []redis.HotKeysMetric{redis.HotKeysMetricNET},\n\t\t\t\tCount:   5,\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(start.Val()).To(Equal(\"OK\"))\n\n\t\t\tstop := client.HotKeysStop(ctx)\n\t\t\tExpect(stop.Err()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should start hotkeys tracking with duration\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics:  []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},\n\t\t\t\tCount:    10,\n\t\t\t\tDuration: 2,\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(start.Val()).To(Equal(\"OK\"))\n\n\t\t\ttime.Sleep(3 * time.Second)\n\n\t\t\tget := client.HotKeysGet(ctx)\n\t\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\t\tresult := get.Val()\n\t\t\tExpect(result).NotTo(BeNil())\n\t\t\tExpect(result.TrackingActive).To(BeFalse())\n\t\t})\n\n\t\tIt(\"should start hotkeys tracking with sampling\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},\n\t\t\t\tCount:   10,\n\t\t\t\tSample:  10,\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(start.Val()).To(Equal(\"OK\"))\n\n\t\t\tstop := client.HotKeysStop(ctx)\n\t\t\tExpect(stop.Err()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should error when using slots in non-cluster mode\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},\n\t\t\t\tCount:   10,\n\t\t\t\tSlots:   []uint16{0, 1, 2, 100, 200},\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).To(HaveOccurred())\n\t\t\tExpect(start.Err().Error()).To(ContainSubstring(\"SLOTS parameter cannot be used in non-cluster mode\"))\n\t\t})\n\n\t\tIt(\"should error when no metrics are specified\", func() {\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tCount: 10,\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).To(HaveOccurred())\n\t\t\tExpect(start.Err()).To(Equal(redis.ErrHotKeysNoMetrics))\n\t\t})\n\n\t\tIt(\"should error when starting tracking while already active\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU},\n\t\t\t\tCount:   10,\n\t\t\t}\n\t\t\tstart1 := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start1.Err()).NotTo(HaveOccurred())\n\n\t\t\tstart2 := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start2.Err()).To(HaveOccurred())\n\n\t\t\tstop := client.HotKeysStop(ctx)\n\t\t\tExpect(stop.Err()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should error when resetting while tracking is active\", func() {\n\t\t\tSkipBeforeRedisVersion(8.6, \"HOTKEYS commands require Redis >= 8.6\")\n\n\t\t\tstartArgs := &redis.HotKeysStartArgs{\n\t\t\t\tMetrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU},\n\t\t\t\tCount:   10,\n\t\t\t}\n\t\t\tstart := client.HotKeysStart(ctx, startArgs)\n\t\t\tExpect(start.Err()).NotTo(HaveOccurred())\n\n\t\t\treset := client.HotKeysReset(ctx)\n\t\t\tExpect(reset.Err()).To(HaveOccurred())\n\n\t\t\tstop := client.HotKeysStop(ctx)\n\t\t\tExpect(stop.Err()).NotTo(HaveOccurred())\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "hset_benchmark_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// HSET Benchmark Tests\n//\n// This file contains benchmark tests for Redis HSET operations with different scales:\n// 1, 10, 100, 1000, 10000, 100000 operations\n//\n// Prerequisites:\n// - Redis server running on localhost:6379\n// - No authentication required\n//\n// Usage:\n//   go test -bench=BenchmarkHSET -v ./hset_benchmark_test.go\n//   go test -bench=BenchmarkHSETPipelined -v ./hset_benchmark_test.go\n//   go test -bench=. -v ./hset_benchmark_test.go  # Run all benchmarks\n//\n// Example output:\n//   BenchmarkHSET/HSET_1_operations-8         \t    5000\t    250000 ns/op\t1000000.00 ops/sec\n//   BenchmarkHSET/HSET_100_operations-8       \t     100\t  10000000 ns/op\t 100000.00 ops/sec\n//\n// The benchmarks test three different approaches:\n// 1. Individual HSET commands (BenchmarkHSET)\n// 2. Pipelined HSET commands (BenchmarkHSETPipelined)\n\n// BenchmarkHSET benchmarks HSET operations with different scales\nfunc BenchmarkHSET(b *testing.B) {\n\tctx := context.Background()\n\n\t// Setup Redis client\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t\tDB:   0,\n\t})\n\tdefer rdb.Close()\n\n\t// Test connection\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\tb.Skipf(\"Redis server not available: %v\", err)\n\t}\n\n\t// Clean up before and after tests\n\tdefer func() {\n\t\trdb.FlushDB(ctx)\n\t}()\n\n\tscales := []int{1, 10, 100, 1000, 10000, 100000}\n\n\tfor _, scale := range scales {\n\t\tb.Run(fmt.Sprintf(\"HSET_%d_operations\", scale), func(b *testing.B) {\n\t\t\tbenchmarkHSETOperations(b, rdb, ctx, scale)\n\t\t})\n\t}\n}\n\n// benchmarkHSETOperations performs the actual HSET benchmark for a given scale\nfunc benchmarkHSETOperations(b *testing.B, rdb *redis.Client, ctx context.Context, operations int) {\n\thashKey := fmt.Sprintf(\"benchmark_hash_%d\", operations)\n\n\tb.ResetTimer()\n\tb.StartTimer()\n\ttotalTimes := []time.Duration{}\n\n\tfor i := 0; i < b.N; i++ {\n\t\tb.StopTimer()\n\t\t// Clean up the hash before each iteration\n\t\trdb.Del(ctx, hashKey)\n\t\tb.StartTimer()\n\n\t\tstartTime := time.Now()\n\t\t// Perform the specified number of HSET operations\n\t\tfor j := 0; j < operations; j++ {\n\t\t\tfield := fmt.Sprintf(\"field_%d\", j)\n\t\t\tvalue := fmt.Sprintf(\"value_%d\", j)\n\n\t\t\terr := rdb.HSet(ctx, hashKey, field, value).Err()\n\t\t\tif err != nil {\n\t\t\t\tb.Fatalf(\"HSET operation failed: %v\", err)\n\t\t\t}\n\t\t}\n\t\ttotalTimes = append(totalTimes, time.Since(startTime))\n\t}\n\n\t// Stop the timer to calculate metrics\n\tb.StopTimer()\n\n\t// Report operations per second\n\topsPerSec := float64(operations*b.N) / b.Elapsed().Seconds()\n\tb.ReportMetric(opsPerSec, \"ops/sec\")\n\n\t// Report average time per operation\n\tavgTimePerOp := b.Elapsed().Nanoseconds() / int64(operations*b.N)\n\tb.ReportMetric(float64(avgTimePerOp), \"ns/op\")\n\t// report average time in milliseconds from totalTimes\n\tsumTime := time.Duration(0)\n\tfor _, t := range totalTimes {\n\t\tsumTime += t\n\t}\n\tavgTimePerOpMs := sumTime.Milliseconds() / int64(len(totalTimes))\n\tb.ReportMetric(float64(avgTimePerOpMs), \"ms\")\n}\n\n// benchmarkHSETOperationsConcurrent performs the actual HSET benchmark for a given scale\nfunc benchmarkHSETOperationsConcurrent(b *testing.B, rdb *redis.Client, ctx context.Context, operations int) {\n\thashKey := fmt.Sprintf(\"benchmark_hash_%d\", operations)\n\n\tb.ResetTimer()\n\tb.StartTimer()\n\ttotalTimes := []time.Duration{}\n\n\tfor i := 0; i < b.N; i++ {\n\t\tb.StopTimer()\n\t\t// Clean up the hash before each iteration\n\t\trdb.Del(ctx, hashKey)\n\t\tb.StartTimer()\n\n\t\tstartTime := time.Now()\n\t\t// Perform the specified number of HSET operations\n\n\t\twg := sync.WaitGroup{}\n\t\ttimesCh := make(chan time.Duration, operations)\n\t\terrCh := make(chan error, operations)\n\n\t\tfor j := 0; j < operations; j++ {\n\t\t\twg.Add(1)\n\t\t\tgo func(j int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tfield := fmt.Sprintf(\"field_%d\", j)\n\t\t\t\tvalue := fmt.Sprintf(\"value_%d\", j)\n\n\t\t\t\terr := rdb.HSet(ctx, hashKey, field, value).Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttimesCh <- time.Since(startTime)\n\t\t\t}(j)\n\t\t}\n\n\t\twg.Wait()\n\t\tclose(timesCh)\n\t\tclose(errCh)\n\n\t\t// Check for errors\n\t\tfor err := range errCh {\n\t\t\tb.Errorf(\"HSET operation failed: %v\", err)\n\t\t}\n\n\t\tfor d := range timesCh {\n\t\t\ttotalTimes = append(totalTimes, d)\n\t\t}\n\t}\n\n\t// Stop the timer to calculate metrics\n\tb.StopTimer()\n\n\t// Report operations per second\n\topsPerSec := float64(operations*b.N) / b.Elapsed().Seconds()\n\tb.ReportMetric(opsPerSec, \"ops/sec\")\n\n\t// Report average time per operation\n\tavgTimePerOp := b.Elapsed().Nanoseconds() / int64(operations*b.N)\n\tb.ReportMetric(float64(avgTimePerOp), \"ns/op\")\n\t// report average time in milliseconds from totalTimes\n\n\tsumTime := time.Duration(0)\n\tfor _, t := range totalTimes {\n\t\tsumTime += t\n\t}\n\tavgTimePerOpMs := sumTime.Milliseconds() / int64(len(totalTimes))\n\tb.ReportMetric(float64(avgTimePerOpMs), \"ms\")\n}\n\n// BenchmarkHSETPipelined benchmarks HSET operations using pipelining for better performance\nfunc BenchmarkHSETPipelined(b *testing.B) {\n\tctx := context.Background()\n\n\t// Setup Redis client\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t\tDB:   0,\n\t})\n\tdefer rdb.Close()\n\n\t// Test connection\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\tb.Skipf(\"Redis server not available: %v\", err)\n\t}\n\n\t// Clean up before and after tests\n\tdefer func() {\n\t\trdb.FlushDB(ctx)\n\t}()\n\n\tscales := []int{1, 10, 100, 1000, 10000, 100000}\n\n\tfor _, scale := range scales {\n\t\tb.Run(fmt.Sprintf(\"HSET_Pipelined_%d_operations\", scale), func(b *testing.B) {\n\t\t\tbenchmarkHSETPipelined(b, rdb, ctx, scale)\n\t\t})\n\t}\n}\n\nfunc BenchmarkHSET_Concurrent(b *testing.B) {\n\tctx := context.Background()\n\n\t// Setup Redis client\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tDB:       0,\n\t\tPoolSize: 100,\n\t})\n\tdefer rdb.Close()\n\n\t// Test connection\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\tb.Skipf(\"Redis server not available: %v\", err)\n\t}\n\n\t// Clean up before and after tests\n\tdefer func() {\n\t\trdb.FlushDB(ctx)\n\t}()\n\n\t// Reduced scales to avoid overwhelming the system with too many concurrent goroutines\n\tscales := []int{1, 10, 100, 1000}\n\n\tfor _, scale := range scales {\n\t\tb.Run(fmt.Sprintf(\"HSET_%d_operations_concurrent\", scale), func(b *testing.B) {\n\t\t\tbenchmarkHSETOperationsConcurrent(b, rdb, ctx, scale)\n\t\t})\n\t}\n}\n\n// benchmarkHSETPipelined performs HSET benchmark using pipelining\nfunc benchmarkHSETPipelined(b *testing.B, rdb *redis.Client, ctx context.Context, operations int) {\n\thashKey := fmt.Sprintf(\"benchmark_hash_pipelined_%d\", operations)\n\n\tb.ResetTimer()\n\tb.StartTimer()\n\ttotalTimes := []time.Duration{}\n\n\tfor i := 0; i < b.N; i++ {\n\t\tb.StopTimer()\n\t\t// Clean up the hash before each iteration\n\t\trdb.Del(ctx, hashKey)\n\t\tb.StartTimer()\n\n\t\tstartTime := time.Now()\n\t\t// Use pipelining for better performance\n\t\tpipe := rdb.Pipeline()\n\n\t\t// Add all HSET operations to the pipeline\n\t\tfor j := 0; j < operations; j++ {\n\t\t\tfield := fmt.Sprintf(\"field_%d\", j)\n\t\t\tvalue := fmt.Sprintf(\"value_%d\", j)\n\t\t\tpipe.HSet(ctx, hashKey, field, value)\n\t\t}\n\n\t\t// Execute all operations at once\n\t\t_, err := pipe.Exec(ctx)\n\t\tif err != nil {\n\t\t\tb.Fatalf(\"Pipeline execution failed: %v\", err)\n\t\t}\n\t\ttotalTimes = append(totalTimes, time.Since(startTime))\n\t}\n\n\tb.StopTimer()\n\n\t// Report operations per second\n\topsPerSec := float64(operations*b.N) / b.Elapsed().Seconds()\n\tb.ReportMetric(opsPerSec, \"ops/sec\")\n\n\t// Report average time per operation\n\tavgTimePerOp := b.Elapsed().Nanoseconds() / int64(operations*b.N)\n\tb.ReportMetric(float64(avgTimePerOp), \"ns/op\")\n\t// report average time in milliseconds from totalTimes\n\tsumTime := time.Duration(0)\n\tfor _, t := range totalTimes {\n\t\tsumTime += t\n\t}\n\tavgTimePerOpMs := sumTime.Milliseconds() / int64(len(totalTimes))\n\tb.ReportMetric(float64(avgTimePerOpMs), \"ms\")\n}\n\n// add same tests but with RESP2\nfunc BenchmarkHSET_RESP2(b *testing.B) {\n\tctx := context.Background()\n\n\t// Setup Redis client\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\tdefer rdb.Close()\n\n\t// Test connection\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\tb.Skipf(\"Redis server not available: %v\", err)\n\t}\n\n\t// Clean up before and after tests\n\tdefer func() {\n\t\trdb.FlushDB(ctx)\n\t}()\n\n\tscales := []int{1, 10, 100, 1000, 10000, 100000}\n\n\tfor _, scale := range scales {\n\t\tb.Run(fmt.Sprintf(\"HSET_RESP2_%d_operations\", scale), func(b *testing.B) {\n\t\t\tbenchmarkHSETOperations(b, rdb, ctx, scale)\n\t\t})\n\t}\n}\n\nfunc BenchmarkHSETPipelined_RESP2(b *testing.B) {\n\tctx := context.Background()\n\n\t// Setup Redis client\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr:     \"localhost:6379\",\n\t\tPassword: \"\", // no password docs\n\t\tDB:       0,  // use default DB\n\t\tProtocol: 2,\n\t})\n\tdefer rdb.Close()\n\n\t// Test connection\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\tb.Skipf(\"Redis server not available: %v\", err)\n\t}\n\n\t// Clean up before and after tests\n\tdefer func() {\n\t\trdb.FlushDB(ctx)\n\t}()\n\n\tscales := []int{1, 10, 100, 1000, 10000, 100000}\n\n\tfor _, scale := range scales {\n\t\tb.Run(fmt.Sprintf(\"HSET_Pipelined_RESP2_%d_operations\", scale), func(b *testing.B) {\n\t\t\tbenchmarkHSETPipelined(b, rdb, ctx, scale)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "hyperloglog_commands.go",
    "content": "package redis\n\nimport \"context\"\n\ntype HyperLogLogCmdable interface {\n\tPFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd\n\tPFCount(ctx context.Context, keys ...string) *IntCmd\n\tPFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd\n}\n\nfunc (c cmdable) PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(els))\n\targs[0] = \"pfadd\"\n\targs[1] = key\n\targs = appendArgs(args, els)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PFCount(ctx context.Context, keys ...string) *IntCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"pfcount\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd {\n\targs := make([]interface{}, 2+len(keys))\n\targs[0] = \"pfmerge\"\n\targs[1] = dest\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/arg.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\nfunc AppendArg(b []byte, v interface{}) []byte {\n\tswitch v := v.(type) {\n\tcase nil:\n\t\treturn append(b, \"<nil>\"...)\n\tcase string:\n\t\treturn appendUTF8String(b, util.StringToBytes(v))\n\tcase []byte:\n\t\treturn appendUTF8String(b, v)\n\tcase int:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int8:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int16:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int32:\n\t\treturn strconv.AppendInt(b, int64(v), 10)\n\tcase int64:\n\t\treturn strconv.AppendInt(b, v, 10)\n\tcase uint:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint8:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint16:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint32:\n\t\treturn strconv.AppendUint(b, uint64(v), 10)\n\tcase uint64:\n\t\treturn strconv.AppendUint(b, v, 10)\n\tcase float32:\n\t\treturn strconv.AppendFloat(b, float64(v), 'f', -1, 64)\n\tcase float64:\n\t\treturn strconv.AppendFloat(b, v, 'f', -1, 64)\n\tcase bool:\n\t\tif v {\n\t\t\treturn append(b, \"true\"...)\n\t\t}\n\t\treturn append(b, \"false\"...)\n\tcase time.Time:\n\t\treturn v.AppendFormat(b, time.RFC3339Nano)\n\tdefault:\n\t\treturn append(b, fmt.Sprint(v)...)\n\t}\n}\n\nfunc appendUTF8String(dst []byte, src []byte) []byte {\n\tdst = append(dst, src...)\n\treturn dst\n}\n"
  },
  {
    "path": "internal/auth/streaming/conn_reauth_credentials_listener.go",
    "content": "package streaming\n\nimport (\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// ConnReAuthCredentialsListener is a credentials listener for a specific connection\n// that triggers re-authentication when credentials change.\n//\n// This listener implements the auth.CredentialsListener interface and is subscribed\n// to a StreamingCredentialsProvider. When new credentials are received via OnNext,\n// it marks the connection for re-authentication through the manager.\n//\n// The re-authentication is always performed asynchronously to avoid blocking the\n// credentials provider and to prevent potential deadlocks with the pool semaphore.\n// The actual re-auth happens when the connection is returned to the pool in an idle state.\n//\n// Lifecycle:\n//   - Created during connection initialization via Manager.Listener()\n//   - Subscribed to the StreamingCredentialsProvider\n//   - Receives credential updates via OnNext()\n//   - Cleaned up when connection is removed from pool via Manager.RemoveListener()\ntype ConnReAuthCredentialsListener struct {\n\t// reAuth is the function to re-authenticate the connection with new credentials\n\treAuth func(conn *pool.Conn, credentials auth.Credentials) error\n\n\t// onErr is the function to call when re-authentication or acquisition fails\n\tonErr func(conn *pool.Conn, err error)\n\n\t// conn is the connection this listener is associated with\n\tconn *pool.Conn\n\n\t// manager is the streaming credentials manager for coordinating re-auth\n\tmanager *Manager\n}\n\n// OnNext is called when new credentials are received from the StreamingCredentialsProvider.\n//\n// This method marks the connection for asynchronous re-authentication. The actual\n// re-authentication happens in the background when the connection is returned to the\n// pool and is in an idle state.\n//\n// Asynchronous re-auth is used to:\n//   - Avoid blocking the credentials provider's notification goroutine\n//   - Prevent deadlocks with the pool's semaphore (especially with small pool sizes)\n//   - Ensure re-auth happens when the connection is safe to use (not processing commands)\n//\n// The reAuthFn callback receives:\n//   - nil if the connection was successfully acquired for re-auth\n//   - error if acquisition timed out or failed\n//\n// Thread-safe: Called by the credentials provider's notification goroutine.\nfunc (c *ConnReAuthCredentialsListener) OnNext(credentials auth.Credentials) {\n\tif c.conn == nil || c.conn.IsClosed() || c.manager == nil || c.reAuth == nil {\n\t\treturn\n\t}\n\n\t// Always use async reauth to avoid complex pool semaphore issues\n\t// The synchronous path can cause deadlocks in the pool's semaphore mechanism\n\t// when called from the Subscribe goroutine, especially with small pool sizes.\n\t// The connection pool hook will re-authenticate the connection when it is\n\t// returned to the pool in a clean, idle state.\n\tc.manager.MarkForReAuth(c.conn, func(err error) {\n\t\t// err is from connection acquisition (timeout, etc.)\n\t\tif err != nil {\n\t\t\t// Log the error\n\t\t\tc.OnError(err)\n\t\t\treturn\n\t\t}\n\t\t// err is from reauth command execution\n\t\terr = c.reAuth(c.conn, credentials)\n\t\tif err != nil {\n\t\t\t// Log the error\n\t\t\tc.OnError(err)\n\t\t\treturn\n\t\t}\n\t})\n}\n\n// OnError is called when an error occurs during credential streaming or re-authentication.\n//\n// This method can be called from:\n//   - The StreamingCredentialsProvider when there's an error in the credentials stream\n//   - The re-auth process when connection acquisition times out\n//   - The re-auth process when the AUTH command fails\n//\n// The error is delegated to the onErr callback provided during listener creation.\n//\n// Thread-safe: Can be called from multiple goroutines (provider, re-auth worker).\nfunc (c *ConnReAuthCredentialsListener) OnError(err error) {\n\tif c.onErr == nil {\n\t\treturn\n\t}\n\n\tc.onErr(c.conn, err)\n}\n\n// Ensure ConnReAuthCredentialsListener implements the CredentialsListener interface.\nvar _ auth.CredentialsListener = (*ConnReAuthCredentialsListener)(nil)\n"
  },
  {
    "path": "internal/auth/streaming/cred_listeners.go",
    "content": "package streaming\n\nimport (\n\t\"sync\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n)\n\n// CredentialsListeners is a thread-safe collection of credentials listeners\n// indexed by connection ID.\n//\n// This collection is used by the Manager to maintain a registry of listeners\n// for each connection in the pool. Listeners are reused when connections are\n// reinitialized (e.g., after a handoff) to avoid creating duplicate subscriptions\n// to the StreamingCredentialsProvider.\n//\n// The collection supports concurrent access from multiple goroutines during\n// connection initialization, credential updates, and connection removal.\ntype CredentialsListeners struct {\n\t// listeners maps connection ID to credentials listener\n\tlisteners map[uint64]auth.CredentialsListener\n\n\t// lock protects concurrent access to the listeners map\n\tlock sync.RWMutex\n}\n\n// NewCredentialsListeners creates a new thread-safe credentials listeners collection.\nfunc NewCredentialsListeners() *CredentialsListeners {\n\treturn &CredentialsListeners{\n\t\tlisteners: make(map[uint64]auth.CredentialsListener),\n\t}\n}\n\n// Add adds or updates a credentials listener for a connection.\n//\n// If a listener already exists for the connection ID, it is replaced.\n// This is safe because the old listener should have been unsubscribed\n// before the connection was reinitialized.\n//\n// Thread-safe: Can be called concurrently from multiple goroutines.\nfunc (c *CredentialsListeners) Add(connID uint64, listener auth.CredentialsListener) {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tif c.listeners == nil {\n\t\tc.listeners = make(map[uint64]auth.CredentialsListener)\n\t}\n\tc.listeners[connID] = listener\n}\n\n// Get retrieves the credentials listener for a connection.\n//\n// Returns:\n//   - listener: The credentials listener for the connection, or nil if not found\n//   - ok: true if a listener exists for the connection ID, false otherwise\n//\n// Thread-safe: Can be called concurrently from multiple goroutines.\nfunc (c *CredentialsListeners) Get(connID uint64) (auth.CredentialsListener, bool) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tif len(c.listeners) == 0 {\n\t\treturn nil, false\n\t}\n\tlistener, ok := c.listeners[connID]\n\treturn listener, ok\n}\n\n// Remove removes the credentials listener for a connection.\n//\n// This is called when a connection is removed from the pool to prevent\n// memory leaks. If no listener exists for the connection ID, this is a no-op.\n//\n// Thread-safe: Can be called concurrently from multiple goroutines.\nfunc (c *CredentialsListeners) Remove(connID uint64) {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tdelete(c.listeners, connID)\n}\n"
  },
  {
    "path": "internal/auth/streaming/manager.go",
    "content": "package streaming\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// Manager coordinates streaming credentials and re-authentication for a connection pool.\n//\n// The manager is responsible for:\n//   - Creating and managing per-connection credentials listeners\n//   - Providing the pool hook for re-authentication\n//   - Coordinating between credentials updates and pool operations\n//\n// When credentials change via a StreamingCredentialsProvider:\n//  1. The credentials listener (ConnReAuthCredentialsListener) receives the update\n//  2. It calls MarkForReAuth on the manager\n//  3. The manager delegates to the pool hook\n//  4. The pool hook schedules background re-authentication\n//\n// The manager maintains a registry of credentials listeners indexed by connection ID,\n// allowing listener reuse when connections are reinitialized (e.g., after handoff).\ntype Manager struct {\n\t// credentialsListeners maps connection ID to credentials listener\n\tcredentialsListeners *CredentialsListeners\n\n\t// pool is the connection pool being managed\n\tpool pool.Pooler\n\n\t// poolHookRef is the re-authentication pool hook\n\tpoolHookRef *ReAuthPoolHook\n}\n\n// NewManager creates a new streaming credentials manager.\n//\n// Parameters:\n//   - pl: The connection pool to manage\n//   - reAuthTimeout: Maximum time to wait for acquiring a connection for re-authentication\n//\n// The manager creates a ReAuthPoolHook sized to match the pool size, ensuring that\n// re-auth operations don't exhaust the connection pool.\nfunc NewManager(pl pool.Pooler, reAuthTimeout time.Duration) *Manager {\n\tm := &Manager{\n\t\tpool:                 pl,\n\t\tpoolHookRef:          NewReAuthPoolHook(pl.Size(), reAuthTimeout),\n\t\tcredentialsListeners: NewCredentialsListeners(),\n\t}\n\tm.poolHookRef.manager = m\n\treturn m\n}\n\n// PoolHook returns the pool hook for re-authentication.\n//\n// This hook should be registered with the connection pool to enable\n// automatic re-authentication when credentials change.\nfunc (m *Manager) PoolHook() pool.PoolHook {\n\treturn m.poolHookRef\n}\n\n// Listener returns or creates a credentials listener for a connection.\n//\n// This method is called during connection initialization to set up the\n// credentials listener. If a listener already exists for the connection ID\n// (e.g., after a handoff), it is reused.\n//\n// Parameters:\n//   - poolCn: The connection to create/get a listener for\n//   - reAuth: Function to re-authenticate the connection with new credentials\n//   - onErr: Function to call when re-authentication fails\n//\n// Returns:\n//   - auth.CredentialsListener: The listener to subscribe to the credentials provider\n//   - error: Non-nil if poolCn is nil\n//\n// Note: The reAuth and onErr callbacks are captured once when the listener is\n// created and reused for the connection's lifetime. They should not change.\n//\n// Thread-safe: Can be called concurrently during connection initialization.\nfunc (m *Manager) Listener(\n\tpoolCn *pool.Conn,\n\treAuth func(*pool.Conn, auth.Credentials) error,\n\tonErr func(*pool.Conn, error),\n) (auth.CredentialsListener, error) {\n\tif poolCn == nil {\n\t\treturn nil, errors.New(\"poolCn cannot be nil\")\n\t}\n\tconnID := poolCn.GetID()\n\t// if we reconnect the underlying network connection, the streaming credentials listener will continue to work\n\t// so we can get the old listener from the cache and use it.\n\t// subscribing the same (an already subscribed) listener for a StreamingCredentialsProvider SHOULD be a no-op\n\tlistener, ok := m.credentialsListeners.Get(connID)\n\tif !ok || listener == nil {\n\t\t// Create new listener for this connection\n\t\t// Note: Callbacks (reAuth, onErr) are captured once and reused for the connection's lifetime\n\t\tnewCredListener := &ConnReAuthCredentialsListener{\n\t\t\tconn:    poolCn,\n\t\t\treAuth:  reAuth,\n\t\t\tonErr:   onErr,\n\t\t\tmanager: m,\n\t\t}\n\n\t\tm.credentialsListeners.Add(connID, newCredListener)\n\t\tlistener = newCredListener\n\t}\n\treturn listener, nil\n}\n\n// MarkForReAuth marks a connection for re-authentication.\n//\n// This method is called by the credentials listener when new credentials are\n// received. It delegates to the pool hook to schedule background re-authentication.\n//\n// Parameters:\n//   - poolCn: The connection to re-authenticate\n//   - reAuthFn: Function to call for re-authentication, receives error if acquisition fails\n//\n// Thread-safe: Called by credentials listeners when credentials change.\nfunc (m *Manager) MarkForReAuth(poolCn *pool.Conn, reAuthFn func(error)) {\n\tconnID := poolCn.GetID()\n\tm.poolHookRef.MarkForReAuth(connID, reAuthFn)\n}\n\n// RemoveListener removes the credentials listener for a connection.\n//\n// This method is called by the pool hook's OnRemove to clean up listeners\n// when connections are removed from the pool.\n//\n// Parameters:\n//   - connID: The connection ID whose listener should be removed\n//\n// Thread-safe: Called during connection removal.\nfunc (m *Manager) RemoveListener(connID uint64) {\n\tm.credentialsListeners.Remove(connID)\n}\n"
  },
  {
    "path": "internal/auth/streaming/manager_test.go",
    "content": "package streaming\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// Test that Listener returns the newly created listener, not nil\nfunc TestManager_Listener_ReturnsNewListener(t *testing.T) {\n\t// Create a mock pool\n\tmockPool := &mockPooler{}\n\n\t// Create manager\n\tmanager := NewManager(mockPool, time.Second)\n\n\t// Create a mock connection\n\tconn := &pool.Conn{}\n\n\t// Mock functions\n\treAuth := func(cn *pool.Conn, creds auth.Credentials) error {\n\t\treturn nil\n\t}\n\n\tonErr := func(cn *pool.Conn, err error) {\n\t}\n\n\t// Get listener - this should create a new one\n\tlistener, err := manager.Listener(conn, reAuth, onErr)\n\n\t// Verify no error\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got: %v\", err)\n\t}\n\n\t// Verify listener is not nil (this was the bug!)\n\tif listener == nil {\n\t\tt.Fatal(\"Expected listener to be non-nil, but got nil\")\n\t}\n\n\t// Verify it's the correct type\n\tif _, ok := listener.(*ConnReAuthCredentialsListener); !ok {\n\t\tt.Fatalf(\"Expected listener to be *ConnReAuthCredentialsListener, got %T\", listener)\n\t}\n\n\t// Get the same listener again - should return the existing one\n\tlistener2, err := manager.Listener(conn, reAuth, onErr)\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error on second call, got: %v\", err)\n\t}\n\n\tif listener2 == nil {\n\t\tt.Fatal(\"Expected listener2 to be non-nil\")\n\t}\n\n\t// Should be the same instance\n\tif listener != listener2 {\n\t\tt.Error(\"Expected to get the same listener instance on second call\")\n\t}\n}\n\n// Test that Listener returns error when conn is nil\nfunc TestManager_Listener_NilConn(t *testing.T) {\n\tmockPool := &mockPooler{}\n\tmanager := NewManager(mockPool, time.Second)\n\n\tlistener, err := manager.Listener(nil, nil, nil)\n\n\tif err == nil {\n\t\tt.Fatal(\"Expected error when conn is nil, got nil\")\n\t}\n\n\tif listener != nil {\n\t\tt.Error(\"Expected listener to be nil when error occurs\")\n\t}\n\n\texpectedErr := \"poolCn cannot be nil\"\n\tif err.Error() != expectedErr {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expectedErr, err.Error())\n\t}\n}\n\n// Mock pooler for testing\ntype mockPooler struct{}\n\nfunc (m *mockPooler) NewConn(ctx context.Context) (*pool.Conn, error)                             { return nil, nil }\nfunc (m *mockPooler) CloseConn(context.Context, *pool.Conn, string, string) error                 { return nil }\nfunc (m *mockPooler) Get(ctx context.Context) (*pool.Conn, error)                          { return nil, nil }\nfunc (m *mockPooler) Put(ctx context.Context, conn *pool.Conn)                             {}\nfunc (m *mockPooler) Remove(ctx context.Context, conn *pool.Conn, reason error)            {}\nfunc (m *mockPooler) RemoveWithoutTurn(ctx context.Context, conn *pool.Conn, reason error) {}\nfunc (m *mockPooler) Len() int                                                             { return 0 }\nfunc (m *mockPooler) IdleLen() int                                                         { return 0 }\nfunc (m *mockPooler) Stats() *pool.Stats                                                   { return &pool.Stats{} }\nfunc (m *mockPooler) Size() int                                                            { return 10 }\nfunc (m *mockPooler) AddPoolHook(hook pool.PoolHook)                                       {}\nfunc (m *mockPooler) RemovePoolHook(hook pool.PoolHook)                                    {}\nfunc (m *mockPooler) Close() error                                                         { return nil }\n"
  },
  {
    "path": "internal/auth/streaming/pool_hook.go",
    "content": "package streaming\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// ReAuthPoolHook is a pool hook that manages background re-authentication of connections\n// when credentials change via a streaming credentials provider.\n//\n// The hook uses a semaphore-based worker pool to limit concurrent re-authentication\n// operations and prevent pool exhaustion. When credentials change, connections are\n// marked for re-authentication and processed asynchronously in the background.\n//\n// The re-authentication process:\n//  1. OnPut: When a connection is returned to the pool, check if it needs re-auth\n//  2. If yes, schedule it for background processing (move from shouldReAuth to scheduledReAuth)\n//  3. A worker goroutine acquires the connection (waits until it's not in use)\n//  4. Executes the re-auth function while holding the connection\n//  5. Releases the connection back to the pool\n//\n// The hook ensures that:\n//   - Only one re-auth operation runs per connection at a time\n//   - Connections are not used for commands during re-authentication\n//   - Re-auth operations timeout if they can't acquire the connection\n//   - Resources are properly cleaned up on connection removal\ntype ReAuthPoolHook struct {\n\t// shouldReAuth maps connection ID to re-auth function\n\t// Connections in this map need re-authentication but haven't been scheduled yet\n\tshouldReAuth     map[uint64]func(error)\n\tshouldReAuthLock sync.RWMutex\n\n\t// workers is a semaphore limiting concurrent re-auth operations\n\t// Initialized with poolSize tokens to prevent pool exhaustion\n\t// Uses FastSemaphore for better performance with eventual fairness\n\tworkers *internal.FastSemaphore\n\n\t// reAuthTimeout is the maximum time to wait for acquiring a connection for re-auth\n\treAuthTimeout time.Duration\n\n\t// scheduledReAuth maps connection ID to scheduled status\n\t// Connections in this map have a background worker attempting re-authentication\n\tscheduledReAuth map[uint64]bool\n\tscheduledLock   sync.RWMutex\n\n\t// manager is a back-reference for cleanup operations\n\tmanager *Manager\n}\n\n// NewReAuthPoolHook creates a new re-authentication pool hook.\n//\n// Parameters:\n//   - poolSize: Maximum number of concurrent re-auth operations (typically matches pool size)\n//   - reAuthTimeout: Maximum time to wait for acquiring a connection for re-authentication\n//\n// The poolSize parameter is used to initialize the worker semaphore, ensuring that\n// re-auth operations don't exhaust the connection pool.\nfunc NewReAuthPoolHook(poolSize int, reAuthTimeout time.Duration) *ReAuthPoolHook {\n\treturn &ReAuthPoolHook{\n\t\tshouldReAuth:    make(map[uint64]func(error)),\n\t\tscheduledReAuth: make(map[uint64]bool),\n\t\tworkers:         internal.NewFastSemaphore(int32(poolSize)),\n\t\treAuthTimeout:   reAuthTimeout,\n\t}\n}\n\n// MarkForReAuth marks a connection for re-authentication.\n//\n// This method is called when credentials change and a connection needs to be\n// re-authenticated. The actual re-authentication happens asynchronously when\n// the connection is returned to the pool (in OnPut).\n//\n// Parameters:\n//   - connID: The connection ID to mark for re-authentication\n//   - reAuthFn: Function to call for re-authentication, receives error if acquisition fails\n//\n// Thread-safe: Can be called concurrently from multiple goroutines.\nfunc (r *ReAuthPoolHook) MarkForReAuth(connID uint64, reAuthFn func(error)) {\n\tr.shouldReAuthLock.Lock()\n\tdefer r.shouldReAuthLock.Unlock()\n\tr.shouldReAuth[connID] = reAuthFn\n}\n\n// OnGet is called when a connection is retrieved from the pool.\n//\n// This hook checks if the connection needs re-authentication or has a scheduled\n// re-auth operation. If so, it rejects the connection (returns accept=false),\n// causing the pool to try another connection.\n//\n// Returns:\n//   - accept: false if connection needs re-auth, true otherwise\n//   - err: always nil (errors are not used in this hook)\n//\n// Thread-safe: Called concurrently by multiple goroutines getting connections.\nfunc (r *ReAuthPoolHook) OnGet(_ context.Context, conn *pool.Conn, _ bool) (accept bool, err error) {\n\tconnID := conn.GetID()\n\tr.shouldReAuthLock.RLock()\n\t_, shouldReAuth := r.shouldReAuth[connID]\n\tr.shouldReAuthLock.RUnlock()\n\t// This connection was marked for reauth while in the pool,\n\t// reject the connection\n\tif shouldReAuth {\n\t\t// simply reject the connection, it will be re-authenticated in OnPut\n\t\treturn false, nil\n\t}\n\tr.scheduledLock.RLock()\n\t_, hasScheduled := r.scheduledReAuth[connID]\n\tr.scheduledLock.RUnlock()\n\t// has scheduled reauth, reject the connection\n\tif hasScheduled {\n\t\t// simply reject the connection, it currently has a reauth scheduled\n\t\t// and the worker is waiting for slot to execute the reauth\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n\n// OnPut is called when a connection is returned to the pool.\n//\n// This hook checks if the connection needs re-authentication. If so, it schedules\n// a background goroutine to perform the re-auth asynchronously. The goroutine:\n//  1. Waits for a worker slot (semaphore)\n//  2. Acquires the connection (waits until not in use)\n//  3. Executes the re-auth function\n//  4. Releases the connection and worker slot\n//\n// The connection is always pooled (not removed) since re-auth happens in background.\n//\n// Returns:\n//   - shouldPool: always true (connection stays in pool during background re-auth)\n//   - shouldRemove: always false\n//   - err: always nil\n//\n// Thread-safe: Called concurrently by multiple goroutines returning connections.\nfunc (r *ReAuthPoolHook) OnPut(_ context.Context, conn *pool.Conn) (bool, bool, error) {\n\tif conn == nil {\n\t\t// noop\n\t\treturn true, false, nil\n\t}\n\tconnID := conn.GetID()\n\t// Check if reauth is needed and get the function with proper locking\n\tr.shouldReAuthLock.RLock()\n\treAuthFn, ok := r.shouldReAuth[connID]\n\tr.shouldReAuthLock.RUnlock()\n\n\tif ok {\n\t\t// Acquire both locks to atomically move from shouldReAuth to scheduledReAuth\n\t\t// This prevents race conditions where OnGet might miss the transition\n\t\tr.shouldReAuthLock.Lock()\n\t\tr.scheduledLock.Lock()\n\t\tr.scheduledReAuth[connID] = true\n\t\tdelete(r.shouldReAuth, connID)\n\t\tr.scheduledLock.Unlock()\n\t\tr.shouldReAuthLock.Unlock()\n\t\tgo func() {\n\t\t\tr.workers.AcquireBlocking()\n\t\t\t// safety first\n\t\t\tif conn == nil || (conn != nil && conn.IsClosed()) {\n\t\t\t\tr.workers.Release()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\tif rec := recover(); rec != nil {\n\t\t\t\t\t// once again - safety first\n\t\t\t\t\tinternal.Logger.Printf(context.Background(), \"panic in reauth worker: %v\", rec)\n\t\t\t\t}\n\t\t\t\tr.scheduledLock.Lock()\n\t\t\t\tdelete(r.scheduledReAuth, connID)\n\t\t\t\tr.scheduledLock.Unlock()\n\t\t\t\tr.workers.Release()\n\t\t\t}()\n\n\t\t\t// Create timeout context for connection acquisition\n\t\t\t// This prevents indefinite waiting if the connection is stuck\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), r.reAuthTimeout)\n\t\t\tdefer cancel()\n\n\t\t\t// Try to acquire the connection for re-authentication\n\t\t\t// We need to ensure the connection is IDLE (not IN_USE) before transitioning to UNUSABLE\n\t\t\t// This prevents re-authentication from interfering with active commands\n\t\t\t// Use AwaitAndTransition to wait for the connection to become IDLE\n\t\t\tstateMachine := conn.GetStateMachine()\n\t\t\tif stateMachine == nil {\n\t\t\t\t// No state machine - should not happen, but handle gracefully\n\t\t\t\treAuthFn(pool.ErrConnUnusableTimeout)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Use predefined slice to avoid allocation\n\t\t\t_, err := stateMachine.AwaitAndTransition(ctx, pool.ValidFromIdle(), pool.StateUnusable)\n\t\t\tif err != nil {\n\t\t\t\t// Timeout or other error occurred, cannot acquire connection\n\t\t\t\treAuthFn(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// safety first\n\t\t\tif !conn.IsClosed() {\n\t\t\t\t// Successfully acquired the connection, perform reauth\n\t\t\t\treAuthFn(nil)\n\t\t\t}\n\n\t\t\t// Release the connection: transition from UNUSABLE back to IDLE\n\t\t\tstateMachine.Transition(pool.StateIdle)\n\t\t}()\n\t}\n\n\t// the reauth will happen in background, as far as the pool is concerned:\n\t// pool the connection, don't remove it, no error\n\treturn true, false, nil\n}\n\n// OnRemove is called when a connection is removed from the pool.\n//\n// This hook cleans up all state associated with the connection:\n//   - Removes from shouldReAuth map (pending re-auth)\n//   - Removes from scheduledReAuth map (active re-auth)\n//   - Removes credentials listener from manager\n//\n// This prevents memory leaks and ensures that removed connections don't have\n// lingering re-auth operations or listeners.\n//\n// Thread-safe: Called when connections are removed due to errors, timeouts, or pool closure.\nfunc (r *ReAuthPoolHook) OnRemove(_ context.Context, conn *pool.Conn, _ error) {\n\tconnID := conn.GetID()\n\tr.shouldReAuthLock.Lock()\n\tr.scheduledLock.Lock()\n\tdelete(r.scheduledReAuth, connID)\n\tdelete(r.shouldReAuth, connID)\n\tr.scheduledLock.Unlock()\n\tr.shouldReAuthLock.Unlock()\n\tif r.manager != nil {\n\t\tr.manager.RemoveListener(connID)\n\t}\n}\n\nvar _ pool.PoolHook = (*ReAuthPoolHook)(nil)\n"
  },
  {
    "path": "internal/auth/streaming/pool_hook_state_test.go",
    "content": "package streaming\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// TestReAuthOnlyWhenIdle verifies that re-authentication only happens when\n// a connection is in IDLE state, not when it's IN_USE.\nfunc TestReAuthOnlyWhenIdle(t *testing.T) {\n\t// Create a connection\n\tcn := pool.NewConn(nil)\n\n\t// Initialize to IDLE state\n\tcn.GetStateMachine().Transition(pool.StateInitializing)\n\tcn.GetStateMachine().Transition(pool.StateIdle)\n\n\t// Simulate connection being acquired (IDLE → IN_USE)\n\tif !cn.CompareAndSwapUsed(false, true) {\n\t\tt.Fatal(\"Failed to acquire connection\")\n\t}\n\n\t// Verify state is IN_USE\n\tif state := cn.GetStateMachine().GetState(); state != pool.StateInUse {\n\t\tt.Errorf(\"Expected state IN_USE, got %s\", state)\n\t}\n\n\t// Try to transition to UNUSABLE (for reauth) - should fail\n\t_, err := cn.GetStateMachine().TryTransition([]pool.ConnState{pool.StateIdle}, pool.StateUnusable)\n\tif err == nil {\n\t\tt.Error(\"Expected error when trying to transition IN_USE → UNUSABLE, but got none\")\n\t}\n\n\t// Verify state is still IN_USE\n\tif state := cn.GetStateMachine().GetState(); state != pool.StateInUse {\n\t\tt.Errorf(\"Expected state to remain IN_USE, got %s\", state)\n\t}\n\n\t// Release connection (IN_USE → IDLE)\n\tif !cn.CompareAndSwapUsed(true, false) {\n\t\tt.Fatal(\"Failed to release connection\")\n\t}\n\n\t// Verify state is IDLE\n\tif state := cn.GetStateMachine().GetState(); state != pool.StateIdle {\n\t\tt.Errorf(\"Expected state IDLE, got %s\", state)\n\t}\n\n\t// Now try to transition to UNUSABLE - should succeed\n\t_, err = cn.GetStateMachine().TryTransition([]pool.ConnState{pool.StateIdle}, pool.StateUnusable)\n\tif err != nil {\n\t\tt.Errorf(\"Failed to transition IDLE → UNUSABLE: %v\", err)\n\t}\n\n\t// Verify state is UNUSABLE\n\tif state := cn.GetStateMachine().GetState(); state != pool.StateUnusable {\n\t\tt.Errorf(\"Expected state UNUSABLE, got %s\", state)\n\t}\n}\n\n// TestReAuthWaitsForConnectionToBeIdle verifies that the re-auth worker\n// waits for a connection to become IDLE before performing re-authentication.\nfunc TestReAuthWaitsForConnectionToBeIdle(t *testing.T) {\n\t// Create a connection\n\tcn := pool.NewConn(nil)\n\n\t// Initialize to IDLE state\n\tcn.GetStateMachine().Transition(pool.StateInitializing)\n\tcn.GetStateMachine().Transition(pool.StateIdle)\n\n\t// Simulate connection being acquired (IDLE → IN_USE)\n\tif !cn.CompareAndSwapUsed(false, true) {\n\t\tt.Fatal(\"Failed to acquire connection\")\n\t}\n\n\t// Track re-auth attempts\n\tvar reAuthAttempts atomic.Int32\n\tvar reAuthSucceeded atomic.Bool\n\n\t// Start a goroutine that tries to acquire the connection for re-auth\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\t// Try to acquire for re-auth with timeout\n\t\ttimeout := time.After(2 * time.Second)\n\t\tacquired := false\n\n\t\tfor !acquired {\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\tt.Error(\"Timeout waiting to acquire connection for re-auth\")\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\treAuthAttempts.Add(1)\n\t\t\t\t// Try to atomically transition from IDLE to UNUSABLE\n\t\t\t\t_, err := cn.GetStateMachine().TryTransition([]pool.ConnState{pool.StateIdle}, pool.StateUnusable)\n\t\t\t\tif err == nil {\n\t\t\t\t\t// Successfully acquired\n\t\t\t\t\tacquired = true\n\t\t\t\t\treAuthSucceeded.Store(true)\n\t\t\t\t} else {\n\t\t\t\t\t// Connection is still IN_USE, wait a bit\n\t\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Release the connection\n\t\tcn.GetStateMachine().Transition(pool.StateIdle)\n\t}()\n\n\t// Keep connection IN_USE for 500ms\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Verify re-auth hasn't succeeded yet (connection is still IN_USE)\n\tif reAuthSucceeded.Load() {\n\t\tt.Error(\"Re-auth succeeded while connection was IN_USE\")\n\t}\n\n\t// Verify there were multiple attempts\n\tattempts := reAuthAttempts.Load()\n\tif attempts < 2 {\n\t\tt.Errorf(\"Expected multiple re-auth attempts, got %d\", attempts)\n\t}\n\n\t// Release connection (IN_USE → IDLE)\n\tif !cn.CompareAndSwapUsed(true, false) {\n\t\tt.Fatal(\"Failed to release connection\")\n\t}\n\n\t// Wait for re-auth to complete\n\twg.Wait()\n\n\t// Verify re-auth succeeded after connection became IDLE\n\tif !reAuthSucceeded.Load() {\n\t\tt.Error(\"Re-auth did not succeed after connection became IDLE\")\n\t}\n\n\t// Verify final state is IDLE\n\tif state := cn.GetStateMachine().GetState(); state != pool.StateIdle {\n\t\tt.Errorf(\"Expected final state IDLE, got %s\", state)\n\t}\n}\n\n// TestConcurrentReAuthAndUsage verifies that re-auth and normal usage\n// don't interfere with each other.\nfunc TestConcurrentReAuthAndUsage(t *testing.T) {\n\t// Create a connection\n\tcn := pool.NewConn(nil)\n\n\t// Initialize to IDLE state\n\tcn.GetStateMachine().Transition(pool.StateInitializing)\n\tcn.GetStateMachine().Transition(pool.StateIdle)\n\n\tvar wg sync.WaitGroup\n\tvar usageCount atomic.Int32\n\tvar reAuthCount atomic.Int32\n\n\t// Goroutine 1: Simulate normal usage (acquire/release)\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < 100; i++ {\n\t\t\t// Try to acquire\n\t\t\tif cn.CompareAndSwapUsed(false, true) {\n\t\t\t\tusageCount.Add(1)\n\t\t\t\t// Simulate work\n\t\t\t\ttime.Sleep(1 * time.Millisecond)\n\t\t\t\t// Release\n\t\t\t\tcn.CompareAndSwapUsed(true, false)\n\t\t\t}\n\t\t\ttime.Sleep(1 * time.Millisecond)\n\t\t}\n\t}()\n\n\t// Goroutine 2: Simulate re-auth attempts\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < 50; i++ {\n\t\t\t// Try to acquire for re-auth\n\t\t\t_, err := cn.GetStateMachine().TryTransition([]pool.ConnState{pool.StateIdle}, pool.StateUnusable)\n\t\t\tif err == nil {\n\t\t\t\treAuthCount.Add(1)\n\t\t\t\t// Simulate re-auth work\n\t\t\t\ttime.Sleep(2 * time.Millisecond)\n\t\t\t\t// Release\n\t\t\t\tcn.GetStateMachine().Transition(pool.StateIdle)\n\t\t\t}\n\t\t\ttime.Sleep(2 * time.Millisecond)\n\t\t}\n\t}()\n\n\twg.Wait()\n\n\t// Verify both operations happened\n\tif usageCount.Load() == 0 {\n\t\tt.Error(\"No successful usage operations\")\n\t}\n\tif reAuthCount.Load() == 0 {\n\t\tt.Error(\"No successful re-auth operations\")\n\t}\n\n\tt.Logf(\"Usage operations: %d, Re-auth operations: %d\", usageCount.Load(), reAuthCount.Load())\n\n\t// Verify final state is IDLE\n\tif state := cn.GetStateMachine().GetState(); state != pool.StateIdle {\n\t\tt.Errorf(\"Expected final state IDLE, got %s\", state)\n\t}\n}\n\n// TestReAuthRespectsClosed verifies that re-auth doesn't happen on closed connections.\nfunc TestReAuthRespectsClosed(t *testing.T) {\n\t// Create a connection\n\tcn := pool.NewConn(nil)\n\n\t// Initialize to IDLE state\n\tcn.GetStateMachine().Transition(pool.StateInitializing)\n\tcn.GetStateMachine().Transition(pool.StateIdle)\n\n\t// Close the connection\n\tcn.GetStateMachine().Transition(pool.StateClosed)\n\n\t// Try to transition to UNUSABLE - should fail\n\t_, err := cn.GetStateMachine().TryTransition([]pool.ConnState{pool.StateIdle}, pool.StateUnusable)\n\tif err == nil {\n\t\tt.Error(\"Expected error when trying to transition CLOSED → UNUSABLE, but got none\")\n\t}\n\n\t// Verify state is still CLOSED\n\tif state := cn.GetStateMachine().GetState(); state != pool.StateClosed {\n\t\tt.Errorf(\"Expected state to remain CLOSED, got %s\", state)\n\t}\n}\n"
  },
  {
    "path": "internal/customvet/.gitignore",
    "content": "/customvet\n"
  },
  {
    "path": "internal/customvet/checks/setval/setval.go",
    "content": "package setval\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"go/types\"\n\n\t\"golang.org/x/tools/go/analysis\"\n)\n\nvar Analyzer = &analysis.Analyzer{\n\tName: \"setval\",\n\tDoc:  \"find Cmder types that are missing a SetVal method\",\n\n\tRun: func(pass *analysis.Pass) (interface{}, error) {\n\t\tcmderTypes := make(map[string]token.Pos)\n\t\ttypesWithSetValMethod := make(map[string]bool)\n\n\t\tfor _, file := range pass.Files {\n\t\t\tfor _, decl := range file.Decls {\n\t\t\t\tfuncName, receiverType := parseFuncDecl(decl, pass.TypesInfo)\n\n\t\t\t\tswitch funcName {\n\t\t\t\tcase \"Result\":\n\t\t\t\t\tcmderTypes[receiverType] = decl.Pos()\n\t\t\t\tcase \"SetVal\":\n\t\t\t\t\ttypesWithSetValMethod[receiverType] = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor cmder, pos := range cmderTypes {\n\t\t\tif !typesWithSetValMethod[cmder] {\n\t\t\t\tpass.Reportf(pos, \"%s is missing a SetVal method\", cmder)\n\t\t\t}\n\t\t}\n\n\t\treturn nil, nil\n\t},\n}\n\nfunc parseFuncDecl(decl ast.Decl, typesInfo *types.Info) (funcName, receiverType string) {\n\tfuncDecl, ok := decl.(*ast.FuncDecl)\n\tif !ok {\n\t\treturn \"\", \"\" // Not a function declaration.\n\t}\n\n\tif funcDecl.Recv == nil {\n\t\treturn \"\", \"\" // Not a method.\n\t}\n\n\tif len(funcDecl.Recv.List) != 1 {\n\t\treturn \"\", \"\" // Unexpected number of receiver arguments. (Can this happen?)\n\t}\n\n\treceiverTypeObj := typesInfo.TypeOf(funcDecl.Recv.List[0].Type)\n\tif receiverTypeObj == nil {\n\t\treturn \"\", \"\" // Unable to determine the receiver type.\n\t}\n\n\treturn funcDecl.Name.Name, receiverTypeObj.String()\n}\n"
  },
  {
    "path": "internal/customvet/checks/setval/setval_test.go",
    "content": "package setval_test\n\nimport (\n\t\"testing\"\n\n\t\"golang.org/x/tools/go/analysis/analysistest\"\n\n\t\"github.com/redis/go-redis/internal/customvet/checks/setval\"\n)\n\nfunc Test(t *testing.T) {\n\ttestdata := analysistest.TestData()\n\tanalysistest.Run(t, testdata, setval.Analyzer, \"a\")\n}\n"
  },
  {
    "path": "internal/customvet/checks/setval/testdata/src/a/a.go",
    "content": "package a\n\ntype GoodCmd struct {\n\tval int\n}\n\nfunc (c *GoodCmd) SetVal(val int) {\n\tc.val = val\n}\n\nfunc (c *GoodCmd) Result() (int, error) {\n\treturn c.val, nil\n}\n\ntype BadCmd struct {\n\tval int\n}\n\nfunc (c *BadCmd) Result() (int, error) { // want \"\\\\*a.BadCmd is missing a SetVal method\"\n\treturn c.val, nil\n}\n\ntype NotACmd struct {\n\tval int\n}\n\nfunc (c *NotACmd) Val() int {\n\treturn c.val\n}\n"
  },
  {
    "path": "internal/customvet/go.mod",
    "content": "module github.com/redis/go-redis/internal/customvet\n\ngo 1.24\n\nrequire golang.org/x/tools v0.5.0\n\nrequire (\n\tgolang.org/x/mod v0.7.0 // indirect\n\tgolang.org/x/sys v0.4.0 // indirect\n)\n"
  },
  {
    "path": "internal/customvet/go.sum",
    "content": "golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=\ngolang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=\ngolang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=\ngolang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=\n"
  },
  {
    "path": "internal/customvet/main.go",
    "content": "package main\n\nimport (\n\t\"golang.org/x/tools/go/analysis/multichecker\"\n\n\t\"github.com/redis/go-redis/internal/customvet/checks/setval\"\n)\n\nfunc main() {\n\tmultichecker.Main(\n\t\tsetval.Analyzer,\n\t)\n}\n"
  },
  {
    "path": "internal/hashtag/hashtag.go",
    "content": "package hashtag\n\nimport (\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9/internal/rand\"\n)\n\nconst slotNumber = 16384\n\n// CRC16 implementation according to CCITT standards.\n// Copyright 2001-2010 Georges Menie (www.menie.org)\n// Copyright 2013 The Go Authors. All rights reserved.\n// https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c.\nvar crc16tab = [256]uint16{\n\t0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,\n\t0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,\n\t0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,\n\t0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,\n\t0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,\n\t0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,\n\t0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,\n\t0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,\n\t0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,\n\t0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,\n\t0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,\n\t0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,\n\t0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,\n\t0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,\n\t0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,\n\t0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,\n\t0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,\n\t0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,\n\t0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,\n\t0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,\n\t0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,\n\t0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,\n\t0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,\n\t0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,\n\t0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,\n\t0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,\n\t0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,\n\t0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,\n\t0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,\n\t0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,\n\t0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,\n\t0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,\n}\n\nfunc Key(key string) string {\n\tif s := strings.IndexByte(key, '{'); s > -1 {\n\t\tif e := strings.IndexByte(key[s+1:], '}'); e > 0 {\n\t\t\treturn key[s+1 : s+e+1]\n\t\t}\n\t}\n\treturn key\n}\n\nfunc Present(key string) bool {\n\tif key == \"\" {\n\t\treturn false\n\t}\n\tif s := strings.IndexByte(key, '{'); s > -1 {\n\t\tif e := strings.IndexByte(key[s+1:], '}'); e > 0 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc RandomSlot() int {\n\treturn rand.Intn(slotNumber)\n}\n\n// Slot returns a consistent slot number between 0 and 16383\n// for any given string key.\nfunc Slot(key string) int {\n\tif key == \"\" {\n\t\treturn RandomSlot()\n\t}\n\tkey = Key(key)\n\treturn int(crc16sum(key)) % slotNumber\n}\n\nfunc crc16sum(key string) (crc uint16) {\n\tfor i := 0; i < len(key); i++ {\n\t\tcrc = (crc << 8) ^ crc16tab[(byte(crc>>8)^key[i])&0x00ff]\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/hashtag/hashtag_test.go",
    "content": "package hashtag\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9/internal/rand\"\n)\n\nfunc TestGinkgoSuite(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"hashtag\")\n}\n\nvar _ = Describe(\"CRC16\", func() {\n\t// https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec#key-distribution-model.\n\tIt(\"should calculate CRC16\", func() {\n\t\ttests := []struct {\n\t\t\ts string\n\t\t\tn uint16\n\t\t}{\n\t\t\t{\"123456789\", 0x31C3},\n\t\t\t{string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), 21847},\n\t\t}\n\n\t\tfor _, test := range tests {\n\t\t\tExpect(crc16sum(test.s)).To(Equal(test.n), \"for %s\", test.s)\n\t\t}\n\t})\n})\n\nvar _ = Describe(\"HashSlot\", func() {\n\tIt(\"should calculate hash slots\", func() {\n\t\ttests := []struct {\n\t\t\tkey  string\n\t\t\tslot int\n\t\t}{\n\t\t\t{\"123456789\", 12739},\n\t\t\t{\"{}foo\", 9500},\n\t\t\t{\"foo{}\", 5542},\n\t\t\t{\"foo{}{bar}\", 8363},\n\t\t\t{\"\", 10503},\n\t\t\t{\"\", 5176},\n\t\t\t{string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), 5463},\n\t\t}\n\t\t// Empty keys receive random slot.\n\t\trand.Seed(100)\n\n\t\tfor _, test := range tests {\n\t\t\tExpect(Slot(test.key)).To(Equal(test.slot), \"for %s\", test.key)\n\t\t}\n\t})\n\n\tIt(\"should extract keys from tags\", func() {\n\t\ttests := []struct {\n\t\t\tone, two string\n\t\t}{\n\t\t\t{\"foo{bar}\", \"bar\"},\n\t\t\t{\"{foo}bar\", \"foo\"},\n\t\t\t{\"{user1000}.following\", \"{user1000}.followers\"},\n\t\t\t{\"foo{{bar}}zap\", \"{bar\"},\n\t\t\t{\"foo{bar}{zap}\", \"bar\"},\n\t\t}\n\n\t\tfor _, test := range tests {\n\t\t\tExpect(Slot(test.one)).To(Equal(Slot(test.two)), \"for %s <-> %s\", test.one, test.two)\n\t\t}\n\t})\n})\n\nvar _ = Describe(\"Present\", func() {\n\tIt(\"should calculate hash slots\", func() {\n\t\ttests := []struct {\n\t\t\tkey     string\n\t\t\tpresent bool\n\t\t}{\n\t\t\t{\"123456789\", false},\n\t\t\t{\"{}foo\", false},\n\t\t\t{\"foo{}\", false},\n\t\t\t{\"foo{}{bar}\", false},\n\t\t\t{\"\", false},\n\t\t\t{string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), false},\n\t\t\t{\"foo{bar}\", true},\n\t\t\t{\"{foo}bar\", true},\n\t\t\t{\"{user1000}.following\", true},\n\t\t\t{\"foo{{bar}}zap\", true},\n\t\t\t{\"foo{bar}{zap}\", true},\n\t\t}\n\n\t\tfor _, test := range tests {\n\t\t\tExpect(Present(test.key)).To(Equal(test.present), \"for %s\", test.key)\n\t\t}\n\t})\n})\n"
  },
  {
    "path": "internal/hscan/hscan.go",
    "content": "package hscan\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n)\n\n// decoderFunc represents decoding functions for default built-in types.\ntype decoderFunc func(reflect.Value, string) error\n\n// Scanner is the interface implemented by themselves,\n// which will override the decoding behavior of decoderFunc.\ntype Scanner interface {\n\tScanRedis(s string) error\n}\n\nvar (\n\t// List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1).\n\tdecoders = []decoderFunc{\n\t\treflect.Bool:          decodeBool,\n\t\treflect.Int:           decodeInt,\n\t\treflect.Int8:          decodeInt8,\n\t\treflect.Int16:         decodeInt16,\n\t\treflect.Int32:         decodeInt32,\n\t\treflect.Int64:         decodeInt64,\n\t\treflect.Uint:          decodeUint,\n\t\treflect.Uint8:         decodeUint8,\n\t\treflect.Uint16:        decodeUint16,\n\t\treflect.Uint32:        decodeUint32,\n\t\treflect.Uint64:        decodeUint64,\n\t\treflect.Float32:       decodeFloat32,\n\t\treflect.Float64:       decodeFloat64,\n\t\treflect.Complex64:     decodeUnsupported,\n\t\treflect.Complex128:    decodeUnsupported,\n\t\treflect.Array:         decodeUnsupported,\n\t\treflect.Chan:          decodeUnsupported,\n\t\treflect.Func:          decodeUnsupported,\n\t\treflect.Interface:     decodeUnsupported,\n\t\treflect.Map:           decodeUnsupported,\n\t\treflect.Ptr:           decodeUnsupported,\n\t\treflect.Slice:         decodeSlice,\n\t\treflect.String:        decodeString,\n\t\treflect.Struct:        decodeUnsupported,\n\t\treflect.UnsafePointer: decodeUnsupported,\n\t}\n\n\t// Global map of struct field specs that is populated once for every new\n\t// struct type that is scanned. This caches the field types and the corresponding\n\t// decoder functions to avoid iterating through struct fields on subsequent scans.\n\tglobalStructMap = newStructMap()\n)\n\nfunc Struct(dst interface{}) (StructValue, error) {\n\tv := reflect.ValueOf(dst)\n\n\t// The destination to scan into should be a struct pointer.\n\tif v.Kind() != reflect.Ptr || v.IsNil() {\n\t\treturn StructValue{}, fmt.Errorf(\"redis.Scan(non-pointer %T)\", dst)\n\t}\n\n\tv = v.Elem()\n\tif v.Kind() != reflect.Struct {\n\t\treturn StructValue{}, fmt.Errorf(\"redis.Scan(non-struct %T)\", dst)\n\t}\n\n\treturn StructValue{\n\t\tspec:  globalStructMap.get(v.Type()),\n\t\tvalue: v,\n\t}, nil\n}\n\n// Scan scans the results from a key-value Redis map result set to a destination struct.\n// The Redis keys are matched to the struct's field with the `redis` tag.\nfunc Scan(dst interface{}, keys []interface{}, vals []interface{}) error {\n\tif len(keys) != len(vals) {\n\t\treturn errors.New(\"args should have the same number of keys and vals\")\n\t}\n\n\tstrct, err := Struct(dst)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Iterate through the (key, value) sequence.\n\tfor i := 0; i < len(vals); i++ {\n\t\tkey, ok := keys[i].(string)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tval, ok := vals[i].(string)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := strct.Scan(key, val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc decodeBool(f reflect.Value, s string) error {\n\tb, err := strconv.ParseBool(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf.SetBool(b)\n\treturn nil\n}\n\nfunc decodeInt8(f reflect.Value, s string) error {\n\treturn decodeNumber(f, s, 8)\n}\n\nfunc decodeInt16(f reflect.Value, s string) error {\n\treturn decodeNumber(f, s, 16)\n}\n\nfunc decodeInt32(f reflect.Value, s string) error {\n\treturn decodeNumber(f, s, 32)\n}\n\nfunc decodeInt64(f reflect.Value, s string) error {\n\treturn decodeNumber(f, s, 64)\n}\n\nfunc decodeInt(f reflect.Value, s string) error {\n\treturn decodeNumber(f, s, 0)\n}\n\nfunc decodeNumber(f reflect.Value, s string, bitSize int) error {\n\tv, err := strconv.ParseInt(s, 10, bitSize)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf.SetInt(v)\n\treturn nil\n}\n\nfunc decodeUint8(f reflect.Value, s string) error {\n\treturn decodeUnsignedNumber(f, s, 8)\n}\n\nfunc decodeUint16(f reflect.Value, s string) error {\n\treturn decodeUnsignedNumber(f, s, 16)\n}\n\nfunc decodeUint32(f reflect.Value, s string) error {\n\treturn decodeUnsignedNumber(f, s, 32)\n}\n\nfunc decodeUint64(f reflect.Value, s string) error {\n\treturn decodeUnsignedNumber(f, s, 64)\n}\n\nfunc decodeUint(f reflect.Value, s string) error {\n\treturn decodeUnsignedNumber(f, s, 0)\n}\n\nfunc decodeUnsignedNumber(f reflect.Value, s string, bitSize int) error {\n\tv, err := strconv.ParseUint(s, 10, bitSize)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf.SetUint(v)\n\treturn nil\n}\n\nfunc decodeFloat32(f reflect.Value, s string) error {\n\tv, err := strconv.ParseFloat(s, 32)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf.SetFloat(v)\n\treturn nil\n}\n\n// although the default is float64, but we better define it.\nfunc decodeFloat64(f reflect.Value, s string) error {\n\tv, err := strconv.ParseFloat(s, 64)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf.SetFloat(v)\n\treturn nil\n}\n\nfunc decodeString(f reflect.Value, s string) error {\n\tf.SetString(s)\n\treturn nil\n}\n\nfunc decodeSlice(f reflect.Value, s string) error {\n\t// []byte slice ([]uint8).\n\tif f.Type().Elem().Kind() == reflect.Uint8 {\n\t\tf.SetBytes([]byte(s))\n\t}\n\treturn nil\n}\n\nfunc decodeUnsupported(v reflect.Value, s string) error {\n\treturn fmt.Errorf(\"redis.Scan(unsupported %s)\", v.Type())\n}\n"
  },
  {
    "path": "internal/hscan/hscan_test.go",
    "content": "package hscan\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\ntype data struct {\n\tOmit  string `redis:\"-\"`\n\tEmpty string\n\n\tString  string  `redis:\"string\"`\n\tBytes   []byte  `redis:\"byte\"`\n\tInt     int     `redis:\"int\"`\n\tInt8    int8    `redis:\"int8\"`\n\tInt16   int16   `redis:\"int16\"`\n\tInt32   int32   `redis:\"int32\"`\n\tInt64   int64   `redis:\"int64\"`\n\tUint    uint    `redis:\"uint\"`\n\tUint8   uint8   `redis:\"uint8\"`\n\tUint16  uint16  `redis:\"uint16\"`\n\tUint32  uint32  `redis:\"uint32\"`\n\tUint64  uint64  `redis:\"uint64\"`\n\tFloat   float32 `redis:\"float\"`\n\tFloat64 float64 `redis:\"float64\"`\n\tBool    bool    `redis:\"bool\"`\n\tBoolRef *bool   `redis:\"boolRef\"`\n}\n\ntype TimeRFC3339Nano struct {\n\ttime.Time\n}\n\nfunc (t *TimeRFC3339Nano) ScanRedis(s string) (err error) {\n\tt.Time, err = time.Parse(time.RFC3339Nano, s)\n\treturn\n}\n\ntype TimeData struct {\n\tName string           `redis:\"name\"`\n\tTime *TimeRFC3339Nano `redis:\"login\"`\n}\n\ntype i []interface{}\n\nfunc TestGinkgoSuite(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"hscan\")\n}\n\nvar _ = Describe(\"Scan\", func() {\n\tIt(\"catches bad args\", func() {\n\t\tvar d data\n\n\t\tExpect(Scan(&d, i{}, i{})).NotTo(HaveOccurred())\n\t\tExpect(d).To(Equal(data{}))\n\n\t\tExpect(Scan(&d, i{\"key\"}, i{})).To(HaveOccurred())\n\t\tExpect(Scan(&d, i{\"key\"}, i{\"1\", \"2\"})).To(HaveOccurred())\n\t\tExpect(Scan(nil, i{\"key\", \"1\"}, i{})).To(HaveOccurred())\n\n\t\tvar m map[string]interface{}\n\t\tExpect(Scan(&m, i{\"key\"}, i{\"1\"})).To(HaveOccurred())\n\t\tExpect(Scan(data{}, i{\"key\"}, i{\"1\"})).To(HaveOccurred())\n\t\tExpect(Scan(data{}, i{\"key\", \"string\"}, i{nil, nil})).To(HaveOccurred())\n\t})\n\n\tIt(\"number out of range\", func() {\n\t\tf := func(v uint64) string {\n\t\t\treturn strconv.FormatUint(v, 10) + \"1\"\n\t\t}\n\t\tkeys := i{\"int8\", \"int16\", \"int32\", \"int64\", \"uint8\", \"uint16\", \"uint32\", \"uint64\", \"float\", \"float64\"}\n\t\tvals := i{\n\t\t\tf(math.MaxInt8), f(math.MaxInt16), f(math.MaxInt32), f(math.MaxInt64),\n\t\t\tf(math.MaxUint8), f(math.MaxUint16), f(math.MaxUint32), strconv.FormatUint(math.MaxUint64, 10) + \"1\",\n\t\t\t\"13.4028234663852886e+38\", \"11.79769313486231570e+308\",\n\t\t}\n\t\tfor k, v := range keys {\n\t\t\tvar d data\n\t\t\tExpect(Scan(&d, i{v}, i{vals[k]})).To(HaveOccurred())\n\t\t}\n\n\t\t// success\n\t\tf = func(v uint64) string {\n\t\t\treturn strconv.FormatUint(v, 10)\n\t\t}\n\t\tkeys = i{\"int8\", \"int16\", \"int32\", \"int64\", \"uint8\", \"uint16\", \"uint32\", \"uint64\", \"float\", \"float64\"}\n\t\tvals = i{\n\t\t\tf(math.MaxInt8), f(math.MaxInt16), f(math.MaxInt32), f(math.MaxInt64),\n\t\t\tf(math.MaxUint8), f(math.MaxUint16), f(math.MaxUint32), strconv.FormatUint(math.MaxUint64, 10),\n\t\t\t\"3.40282346638528859811704183484516925440e+38\", \"1.797693134862315708145274237317043567981e+308\",\n\t\t}\n\t\tvar d data\n\t\tExpect(Scan(&d, keys, vals)).NotTo(HaveOccurred())\n\t\tExpect(d).To(Equal(data{\n\t\t\tInt8:    math.MaxInt8,\n\t\t\tInt16:   math.MaxInt16,\n\t\t\tInt32:   math.MaxInt32,\n\t\t\tInt64:   math.MaxInt64,\n\t\t\tUint8:   math.MaxUint8,\n\t\t\tUint16:  math.MaxUint16,\n\t\t\tUint32:  math.MaxUint32,\n\t\t\tUint64:  math.MaxUint64,\n\t\t\tFloat:   math.MaxFloat32,\n\t\t\tFloat64: math.MaxFloat64,\n\t\t}))\n\t})\n\n\tIt(\"scans good values\", func() {\n\t\tvar d data\n\n\t\t// non-tagged fields.\n\t\tExpect(Scan(&d, i{\"key\"}, i{\"value\"})).NotTo(HaveOccurred())\n\t\tExpect(d).To(Equal(data{}))\n\n\t\tkeys := i{\"string\", \"byte\", \"int\", \"int64\", \"uint\", \"uint64\", \"float\", \"float64\", \"bool\", \"boolRef\"}\n\t\tvals := i{\n\t\t\t\"str!\", \"bytes!\", \"123\", \"123456789123456789\", \"456\", \"987654321987654321\",\n\t\t\t\"123.456\", \"123456789123456789.987654321987654321\", \"1\", \"1\",\n\t\t}\n\t\tExpect(Scan(&d, keys, vals)).NotTo(HaveOccurred())\n\t\tExpect(d).To(Equal(data{\n\t\t\tString:  \"str!\",\n\t\t\tBytes:   []byte(\"bytes!\"),\n\t\t\tInt:     123,\n\t\t\tInt64:   123456789123456789,\n\t\t\tUint:    456,\n\t\t\tUint64:  987654321987654321,\n\t\t\tFloat:   123.456,\n\t\t\tFloat64: 1.2345678912345678e+17,\n\t\t\tBool:    true,\n\t\t\tBoolRef: util.ToPtr(true),\n\t\t}))\n\n\t\t// Scan a different type with the same values to test that\n\t\t// the struct spec maps don't conflict.\n\t\ttype data2 struct {\n\t\t\tString string  `redis:\"string\"`\n\t\t\tBytes  []byte  `redis:\"byte\"`\n\t\t\tInt    int     `redis:\"int\"`\n\t\t\tUint   uint    `redis:\"uint\"`\n\t\t\tFloat  float32 `redis:\"float\"`\n\t\t\tBool   bool    `redis:\"bool\"`\n\t\t}\n\t\tvar d2 data2\n\t\tExpect(Scan(&d2, keys, vals)).NotTo(HaveOccurred())\n\t\tExpect(d2).To(Equal(data2{\n\t\t\tString: \"str!\",\n\t\t\tBytes:  []byte(\"bytes!\"),\n\t\t\tInt:    123,\n\t\t\tUint:   456,\n\t\t\tFloat:  123.456,\n\t\t\tBool:   true,\n\t\t}))\n\n\t\tExpect(Scan(&d, i{\"string\", \"float\", \"bool\"}, i{\"\", \"1\", \"t\"})).NotTo(HaveOccurred())\n\t\tExpect(d).To(Equal(data{\n\t\t\tString:  \"\",\n\t\t\tBytes:   []byte(\"bytes!\"),\n\t\t\tInt:     123,\n\t\t\tInt64:   123456789123456789,\n\t\t\tUint:    456,\n\t\t\tUint64:  987654321987654321,\n\t\t\tFloat:   1.0,\n\t\t\tFloat64: 1.2345678912345678e+17,\n\t\t\tBool:    true,\n\t\t\tBoolRef: util.ToPtr(true),\n\t\t}))\n\t})\n\n\tIt(\"omits untagged fields\", func() {\n\t\tvar d data\n\n\t\tExpect(Scan(&d, i{\"empty\", \"omit\", \"string\"}, i{\"value\", \"value\", \"str!\"})).NotTo(HaveOccurred())\n\t\tExpect(d).To(Equal(data{\n\t\t\tString: \"str!\",\n\t\t}))\n\t})\n\n\tIt(\"catches bad values\", func() {\n\t\tvar d data\n\n\t\tExpect(Scan(&d, i{\"int\"}, i{\"a\"})).To(HaveOccurred())\n\t\tExpect(Scan(&d, i{\"uint\"}, i{\"a\"})).To(HaveOccurred())\n\t\tExpect(Scan(&d, i{\"uint\"}, i{\"\"})).To(HaveOccurred())\n\t\tExpect(Scan(&d, i{\"float\"}, i{\"b\"})).To(HaveOccurred())\n\t\tExpect(Scan(&d, i{\"bool\"}, i{\"-1\"})).To(HaveOccurred())\n\t\tExpect(Scan(&d, i{\"bool\"}, i{\"\"})).To(HaveOccurred())\n\t\tExpect(Scan(&d, i{\"bool\"}, i{\"123\"})).To(HaveOccurred())\n\t})\n\n\tIt(\"Implements Scanner\", func() {\n\t\tvar td TimeData\n\n\t\tnow := time.Now()\n\t\tExpect(Scan(&td, i{\"name\", \"login\"}, i{\"hello\", now.Format(time.RFC3339Nano)})).NotTo(HaveOccurred())\n\t\tExpect(td.Name).To(Equal(\"hello\"))\n\t\tExpect(td.Time.UnixNano()).To(Equal(now.UnixNano()))\n\t\tExpect(td.Time.Format(time.RFC3339Nano)).To(Equal(now.Format(time.RFC3339Nano)))\n\t})\n\n\tIt(\"should time.Time RFC3339Nano\", func() {\n\t\ttype TimeTime struct {\n\t\t\tTime time.Time `redis:\"time\"`\n\t\t}\n\n\t\tnow := time.Now()\n\n\t\tvar tt TimeTime\n\t\tExpect(Scan(&tt, i{\"time\"}, i{now.Format(time.RFC3339Nano)})).NotTo(HaveOccurred())\n\t\tExpect(now.Unix()).To(Equal(tt.Time.Unix()))\n\t})\n})\n"
  },
  {
    "path": "internal/hscan/structmap.go",
    "content": "package hscan\n\nimport (\n\t\"encoding\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\n// structMap contains the map of struct fields for target structs\n// indexed by the struct type.\ntype structMap struct {\n\tm sync.Map\n}\n\nfunc newStructMap() *structMap {\n\treturn new(structMap)\n}\n\nfunc (s *structMap) get(t reflect.Type) *structSpec {\n\tif v, ok := s.m.Load(t); ok {\n\t\treturn v.(*structSpec)\n\t}\n\n\tspec := newStructSpec(t, \"redis\")\n\ts.m.Store(t, spec)\n\treturn spec\n}\n\n//------------------------------------------------------------------------------\n\n// structSpec contains the list of all fields in a target struct.\ntype structSpec struct {\n\tm map[string]*structField\n}\n\nfunc (s *structSpec) set(tag string, sf *structField) {\n\ts.m[tag] = sf\n}\n\nfunc newStructSpec(t reflect.Type, fieldTag string) *structSpec {\n\tnumField := t.NumField()\n\tout := &structSpec{\n\t\tm: make(map[string]*structField, numField),\n\t}\n\n\tfor i := 0; i < numField; i++ {\n\t\tf := t.Field(i)\n\n\t\ttag := f.Tag.Get(fieldTag)\n\t\tif tag == \"\" || tag == \"-\" {\n\t\t\tcontinue\n\t\t}\n\n\t\ttag = strings.Split(tag, \",\")[0]\n\t\tif tag == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Use the built-in decoder.\n\t\tkind := f.Type.Kind()\n\t\tif kind == reflect.Pointer {\n\t\t\tkind = f.Type.Elem().Kind()\n\t\t}\n\t\tout.set(tag, &structField{index: i, fn: decoders[kind]})\n\t}\n\n\treturn out\n}\n\n//------------------------------------------------------------------------------\n\n// structField represents a single field in a target struct.\ntype structField struct {\n\tindex int\n\tfn    decoderFunc\n}\n\n//------------------------------------------------------------------------------\n\ntype StructValue struct {\n\tspec  *structSpec\n\tvalue reflect.Value\n}\n\nfunc (s StructValue) Scan(key string, value string) error {\n\tfield, ok := s.spec.m[key]\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tv := s.value.Field(field.index)\n\tisPtr := v.Kind() == reflect.Ptr\n\n\tif isPtr && v.IsNil() {\n\t\tv.Set(reflect.New(v.Type().Elem()))\n\t}\n\tif !isPtr && v.Type().Name() != \"\" && v.CanAddr() {\n\t\tv = v.Addr()\n\t\tisPtr = true\n\t}\n\n\tif isPtr && v.Type().NumMethod() > 0 && v.CanInterface() {\n\t\tswitch scan := v.Interface().(type) {\n\t\tcase Scanner:\n\t\t\treturn scan.ScanRedis(value)\n\t\tcase encoding.TextUnmarshaler:\n\t\t\treturn scan.UnmarshalText(util.StringToBytes(value))\n\t\t}\n\t}\n\n\tif isPtr {\n\t\tv = v.Elem()\n\t}\n\n\tif err := field.fn(v, value); err != nil {\n\t\tt := s.value.Type()\n\t\treturn fmt.Errorf(\"cannot scan redis.result %s into struct field %s.%s of type %s, error-%s\",\n\t\t\tvalue, t.Name(), t.Field(field.index).Name, t.Field(field.index).Type, err.Error())\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/interfaces/interfaces.go",
    "content": "// Package interfaces provides shared interfaces used by both the main redis package\n// and the maintnotifications upgrade package to avoid circular dependencies.\npackage interfaces\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"time\"\n)\n\n// NotificationProcessor is (most probably) a push.NotificationProcessor\n// forward declaration to avoid circular imports\ntype NotificationProcessor interface {\n\tRegisterHandler(pushNotificationName string, handler interface{}, protected bool) error\n\tUnregisterHandler(pushNotificationName string) error\n\tGetHandler(pushNotificationName string) interface{}\n}\n\n// ClientInterface defines the interface that clients must implement for maintnotifications upgrades.\ntype ClientInterface interface {\n\t// GetOptions returns the client options.\n\tGetOptions() OptionsInterface\n\n\t// GetPushProcessor returns the client's push notification processor.\n\tGetPushProcessor() NotificationProcessor\n}\n\n// OptionsInterface defines the interface for client options.\n// Uses an adapter pattern to avoid circular dependencies.\ntype OptionsInterface interface {\n\t// GetReadTimeout returns the read timeout.\n\tGetReadTimeout() time.Duration\n\n\t// GetWriteTimeout returns the write timeout.\n\tGetWriteTimeout() time.Duration\n\n\t// GetNetwork returns the network type.\n\tGetNetwork() string\n\n\t// GetAddr returns the connection address.\n\tGetAddr() string\n\n\t// GetNodeAddress returns the address of the Redis node as reported by the server.\n\t// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation.\n\t// For standalone clients, this defaults to Addr.\n\tGetNodeAddress() string\n\n\t// IsTLSEnabled returns true if TLS is enabled.\n\tIsTLSEnabled() bool\n\n\t// GetProtocol returns the protocol version.\n\tGetProtocol() int\n\n\t// GetPoolSize returns the connection pool size.\n\tGetPoolSize() int\n\n\t// NewDialer returns a new dialer function for the connection.\n\tNewDialer() func(context.Context) (net.Conn, error)\n}\n"
  },
  {
    "path": "internal/internal.go",
    "content": "package internal\n\nimport (\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/rand\"\n)\n\nfunc RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration {\n\tif retry < 0 {\n\t\tpanic(\"not reached\")\n\t}\n\tif minBackoff == 0 {\n\t\treturn 0\n\t}\n\n\td := minBackoff << uint(retry)\n\tif d < minBackoff {\n\t\treturn maxBackoff\n\t}\n\n\td = minBackoff + time.Duration(rand.Int63n(int64(d)))\n\n\tif d > maxBackoff || d < minBackoff {\n\t\td = maxBackoff\n\t}\n\n\treturn d\n}\n"
  },
  {
    "path": "internal/internal_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/gomega\"\n)\n\nfunc TestRetryBackoff(t *testing.T) {\n\tRegisterTestingT(t)\n\n\tfor i := 0; i <= 16; i++ {\n\t\tbackoff := RetryBackoff(i, time.Millisecond, 512*time.Millisecond)\n\t\tExpect(backoff >= 0).To(BeTrue())\n\t\tExpect(backoff <= 512*time.Millisecond).To(BeTrue())\n\t}\n}\n"
  },
  {
    "path": "internal/log.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n)\n\n// TODO (ned): Revisit logging\n// Add more standardized approach with log levels and configurability\n\ntype Logging interface {\n\tPrintf(ctx context.Context, format string, v ...interface{})\n}\n\ntype DefaultLogger struct {\n\tlog *log.Logger\n}\n\nfunc (l *DefaultLogger) Printf(ctx context.Context, format string, v ...interface{}) {\n\t_ = l.log.Output(2, fmt.Sprintf(format, v...))\n}\n\nfunc NewDefaultLogger() Logging {\n\treturn &DefaultLogger{\n\t\tlog: log.New(os.Stderr, \"redis: \", log.LstdFlags|log.Lshortfile),\n\t}\n}\n\n// Logger calls Output to print to the stderr.\n// Arguments are handled in the manner of fmt.Print.\nvar Logger Logging = NewDefaultLogger()\n\nvar LogLevel LogLevelT = LogLevelError\n\n// LogLevelT represents the logging level\ntype LogLevelT int\n\n// Log level constants for the entire go-redis library\nconst (\n\tLogLevelError LogLevelT = iota // 0 - errors only\n\tLogLevelWarn                   // 1 - warnings and errors\n\tLogLevelInfo                   // 2 - info, warnings, and errors\n\tLogLevelDebug                  // 3 - debug, info, warnings, and errors\n)\n\n// String returns the string representation of the log level\nfunc (l LogLevelT) String() string {\n\tswitch l {\n\tcase LogLevelError:\n\t\treturn \"ERROR\"\n\tcase LogLevelWarn:\n\t\treturn \"WARN\"\n\tcase LogLevelInfo:\n\t\treturn \"INFO\"\n\tcase LogLevelDebug:\n\t\treturn \"DEBUG\"\n\tdefault:\n\t\treturn \"UNKNOWN\"\n\t}\n}\n\n// IsValid returns true if the log level is valid\nfunc (l LogLevelT) IsValid() bool {\n\treturn l >= LogLevelError && l <= LogLevelDebug\n}\n\nfunc (l LogLevelT) WarnOrAbove() bool {\n\treturn l >= LogLevelWarn\n}\n\nfunc (l LogLevelT) InfoOrAbove() bool {\n\treturn l >= LogLevelInfo\n}\n\nfunc (l LogLevelT) DebugOrAbove() bool {\n\treturn l >= LogLevelDebug\n}\n"
  },
  {
    "path": "internal/maintnotifications/logs/log_messages.go",
    "content": "package logs\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n)\n\n// appendJSONIfDebug appends JSON data to a message only if the global log level is Debug\nfunc appendJSONIfDebug(message string, data map[string]interface{}) string {\n\tif internal.LogLevel.DebugOrAbove() {\n\t\tjsonData, _ := json.Marshal(data)\n\t\treturn fmt.Sprintf(\"%s %s\", message, string(jsonData))\n\t}\n\treturn message\n}\n\nconst (\n\t// ========================================\n\t// CIRCUIT_BREAKER.GO - Circuit breaker management\n\t// ========================================\n\tCircuitBreakerTransitioningToHalfOpenMessage = \"circuit breaker transitioning to half-open\"\n\tCircuitBreakerOpenedMessage                  = \"circuit breaker opened\"\n\tCircuitBreakerReopenedMessage                = \"circuit breaker reopened\"\n\tCircuitBreakerClosedMessage                  = \"circuit breaker closed\"\n\tCircuitBreakerCleanupMessage                 = \"circuit breaker cleanup\"\n\tCircuitBreakerOpenMessage                    = \"circuit breaker is open, failing fast\"\n\n\t// ========================================\n\t// CONFIG.GO - Configuration and debug\n\t// ========================================\n\tDebugLoggingEnabledMessage = \"debug logging enabled\"\n\tConfigDebugMessage         = \"config debug\"\n\n\t// ========================================\n\t// ERRORS.GO - Error message constants\n\t// ========================================\n\tInvalidRelaxedTimeoutErrorMessage                 = \"relaxed timeout must be greater than 0\"\n\tInvalidHandoffTimeoutErrorMessage                 = \"handoff timeout must be greater than 0\"\n\tInvalidHandoffWorkersErrorMessage                 = \"MaxWorkers must be greater than or equal to 0\"\n\tInvalidHandoffQueueSizeErrorMessage               = \"handoff queue size must be greater than 0\"\n\tInvalidPostHandoffRelaxedDurationErrorMessage     = \"post-handoff relaxed duration must be greater than or equal to 0\"\n\tInvalidEndpointTypeErrorMessage                   = \"invalid endpoint type\"\n\tInvalidMaintNotificationsErrorMessage             = \"invalid maintenance notifications setting (must be 'disabled', 'enabled', or 'auto')\"\n\tInvalidHandoffRetriesErrorMessage                 = \"MaxHandoffRetries must be between 1 and 10\"\n\tInvalidClientErrorMessage                         = \"invalid client type\"\n\tInvalidNotificationErrorMessage                   = \"invalid notification format\"\n\tMaxHandoffRetriesReachedErrorMessage              = \"max handoff retries reached\"\n\tHandoffQueueFullErrorMessage                      = \"handoff queue is full, cannot queue new handoff requests - consider increasing HandoffQueueSize or MaxWorkers in configuration\"\n\tInvalidCircuitBreakerFailureThresholdErrorMessage = \"circuit breaker failure threshold must be >= 1\"\n\tInvalidCircuitBreakerResetTimeoutErrorMessage     = \"circuit breaker reset timeout must be >= 0\"\n\tInvalidCircuitBreakerMaxRequestsErrorMessage      = \"circuit breaker max requests must be >= 1\"\n\tConnectionMarkedForHandoffErrorMessage            = \"connection marked for handoff\"\n\tConnectionInvalidHandoffStateErrorMessage         = \"connection is in invalid state for handoff\"\n\tShutdownErrorMessage                              = \"shutdown\"\n\tCircuitBreakerOpenErrorMessage                    = \"circuit breaker is open, failing fast\"\n\n\t// ========================================\n\t// EXAMPLE_HOOKS.GO - Example metrics hooks\n\t// ========================================\n\tMetricsHookProcessingNotificationMessage = \"metrics hook processing\"\n\tMetricsHookRecordedErrorMessage          = \"metrics hook recorded error\"\n\n\t// ========================================\n\t// HANDOFF_WORKER.GO - Connection handoff processing\n\t// ========================================\n\tHandoffStartedMessage                            = \"handoff started\"\n\tHandoffFailedMessage                             = \"handoff failed\"\n\tConnectionNotMarkedForHandoffMessage             = \"is not marked for handoff and has no retries\"\n\tConnectionNotMarkedForHandoffErrorMessage        = \"is not marked for handoff\"\n\tHandoffRetryAttemptMessage                       = \"Performing handoff\"\n\tCannotQueueHandoffForRetryMessage                = \"can't queue handoff for retry\"\n\tHandoffQueueFullMessage                          = \"handoff queue is full\"\n\tFailedToDialNewEndpointMessage                   = \"failed to dial new endpoint\"\n\tApplyingRelaxedTimeoutDueToPostHandoffMessage    = \"applying relaxed timeout due to post-handoff\"\n\tHandoffSuccessMessage                            = \"handoff succeeded\"\n\tRemovingConnectionFromPoolMessage                = \"removing connection from pool\"\n\tNoPoolProvidedMessageCannotRemoveMessage         = \"no pool provided, cannot remove connection, closing it\"\n\tWorkerExitingDueToShutdownMessage                = \"worker exiting due to shutdown\"\n\tWorkerExitingDueToShutdownWhileProcessingMessage = \"worker exiting due to shutdown while processing request\"\n\tWorkerPanicRecoveredMessage                      = \"worker panic recovered\"\n\tWorkerExitingDueToInactivityTimeoutMessage       = \"worker exiting due to inactivity timeout\"\n\tReachedMaxHandoffRetriesMessage                  = \"reached max handoff retries\"\n\n\t// ========================================\n\t// MANAGER.GO - Moving operation tracking and handler registration\n\t// ========================================\n\tDuplicateMovingOperationMessage  = \"duplicate MOVING operation ignored\"\n\tTrackingMovingOperationMessage   = \"tracking MOVING operation\"\n\tUntrackingMovingOperationMessage = \"untracking MOVING operation\"\n\tOperationNotTrackedMessage       = \"operation not tracked\"\n\tFailedToRegisterHandlerMessage   = \"failed to register handler\"\n\n\t// ========================================\n\t// HOOKS.GO - Notification processing hooks\n\t// ========================================\n\tProcessingNotificationMessage          = \"processing notification started\"\n\tProcessingNotificationFailedMessage    = \"proccessing notification failed\"\n\tProcessingNotificationSucceededMessage = \"processing notification succeeded\"\n\n\t// ========================================\n\t// POOL_HOOK.GO - Pool connection management\n\t// ========================================\n\tFailedToQueueHandoffMessage = \"failed to queue handoff\"\n\tMarkedForHandoffMessage     = \"connection marked for handoff\"\n\n\t// ========================================\n\t// PUSH_NOTIFICATION_HANDLER.GO - Push notification validation and processing\n\t// ========================================\n\tInvalidNotificationFormatMessage              = \"invalid notification format\"\n\tInvalidNotificationTypeFormatMessage          = \"invalid notification type format\"\n\tInvalidSeqIDInMovingNotificationMessage       = \"invalid seqID in MOVING notification\"\n\tInvalidTimeSInMovingNotificationMessage       = \"invalid timeS in MOVING notification\"\n\tInvalidNewEndpointInMovingNotificationMessage = \"invalid newEndpoint in MOVING notification\"\n\tNoConnectionInHandlerContextMessage           = \"no connection in handler context\"\n\tInvalidConnectionTypeInHandlerContextMessage  = \"invalid connection type in handler context\"\n\tSchedulingHandoffToCurrentEndpointMessage     = \"scheduling handoff to current endpoint\"\n\tRelaxedTimeoutDueToNotificationMessage        = \"applying relaxed timeout due to notification\"\n\tUnrelaxedTimeoutMessage                       = \"clearing relaxed timeout\"\n\tManagerNotInitializedMessage                  = \"manager not initialized\"\n\tFailedToMarkForHandoffMessage                 = \"failed to mark connection for handoff\"\n\tInvalidSeqIDInSMigratingNotificationMessage   = \"invalid SeqID in SMIGRATING notification\"\n\tInvalidSeqIDInSMigratedNotificationMessage    = \"invalid SeqID in SMIGRATED notification\"\n\tTriggeringClusterStateReloadMessage           = \"triggering cluster state reload\"\n\n\t// ========================================\n\t// used in pool/conn\n\t// ========================================\n\tUnrelaxedTimeoutAfterDeadlineMessage = \"clearing relaxed timeout after deadline\"\n)\n\nfunc HandoffStarted(connID uint64, newEndpoint string) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s to %s\", connID, HandoffStartedMessage, newEndpoint)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":   connID,\n\t\t\"endpoint\": newEndpoint,\n\t})\n}\n\nfunc HandoffFailed(connID uint64, newEndpoint string, attempt int, maxAttempts int, err error) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s to %s (attempt %d/%d): %v\", connID, HandoffFailedMessage, newEndpoint, attempt, maxAttempts, err)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":      connID,\n\t\t\"endpoint\":    newEndpoint,\n\t\t\"attempt\":     attempt,\n\t\t\"maxAttempts\": maxAttempts,\n\t\t\"error\":       err.Error(),\n\t})\n}\n\nfunc HandoffSucceeded(connID uint64, newEndpoint string) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s to %s\", connID, HandoffSuccessMessage, newEndpoint)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":   connID,\n\t\t\"endpoint\": newEndpoint,\n\t})\n}\n\n// Timeout-related log functions\nfunc RelaxedTimeoutDueToNotification(connID uint64, notificationType string, timeout interface{}) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s %s (%v)\", connID, RelaxedTimeoutDueToNotificationMessage, notificationType, timeout)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":           connID,\n\t\t\"notificationType\": notificationType,\n\t\t\"timeout\":          fmt.Sprintf(\"%v\", timeout),\n\t})\n}\n\nfunc UnrelaxedTimeout(connID uint64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s\", connID, UnrelaxedTimeoutMessage)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t})\n}\n\nfunc UnrelaxedTimeoutAfterDeadline(connID uint64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s\", connID, UnrelaxedTimeoutAfterDeadlineMessage)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t})\n}\n\n// Handoff queue and marking functions\nfunc HandoffQueueFull(queueLen, queueCap int) string {\n\tmessage := fmt.Sprintf(\"%s (%d/%d), cannot queue new handoff requests - consider increasing HandoffQueueSize or MaxWorkers in configuration\", HandoffQueueFullMessage, queueLen, queueCap)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"queueLen\": queueLen,\n\t\t\"queueCap\": queueCap,\n\t})\n}\n\nfunc FailedToQueueHandoff(connID uint64, err error) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s: %v\", connID, FailedToQueueHandoffMessage, err)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t\t\"error\":  err.Error(),\n\t})\n}\n\nfunc FailedToMarkForHandoff(connID uint64, err error) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s: %v\", connID, FailedToMarkForHandoffMessage, err)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t\t\"error\":  err.Error(),\n\t})\n}\n\nfunc FailedToDialNewEndpoint(connID uint64, endpoint string, err error) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s %s: %v\", connID, FailedToDialNewEndpointMessage, endpoint, err)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":   connID,\n\t\t\"endpoint\": endpoint,\n\t\t\"error\":    err.Error(),\n\t})\n}\n\nfunc ReachedMaxHandoffRetries(connID uint64, endpoint string, maxRetries int) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s to %s (max retries: %d)\", connID, ReachedMaxHandoffRetriesMessage, endpoint, maxRetries)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":     connID,\n\t\t\"endpoint\":   endpoint,\n\t\t\"maxRetries\": maxRetries,\n\t})\n}\n\n// Notification processing functions\nfunc ProcessingNotification(connID uint64, seqID int64, notificationType string, notification interface{}) string {\n\tmessage := fmt.Sprintf(\"conn[%d] seqID[%d] %s %s: %v\", connID, seqID, ProcessingNotificationMessage, notificationType, notification)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":           connID,\n\t\t\"seqID\":            seqID,\n\t\t\"notificationType\": notificationType,\n\t\t\"notification\":     fmt.Sprintf(\"%v\", notification),\n\t})\n}\n\nfunc ProcessingNotificationFailed(connID uint64, notificationType string, err error, notification interface{}) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s %s: %v - %v\", connID, ProcessingNotificationFailedMessage, notificationType, err, notification)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":           connID,\n\t\t\"notificationType\": notificationType,\n\t\t\"error\":            err.Error(),\n\t\t\"notification\":     fmt.Sprintf(\"%v\", notification),\n\t})\n}\n\nfunc ProcessingNotificationSucceeded(connID uint64, notificationType string) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s %s\", connID, ProcessingNotificationSucceededMessage, notificationType)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":           connID,\n\t\t\"notificationType\": notificationType,\n\t})\n}\n\n// Moving operation tracking functions\nfunc DuplicateMovingOperation(connID uint64, endpoint string, seqID int64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s for %s seqID[%d]\", connID, DuplicateMovingOperationMessage, endpoint, seqID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":   connID,\n\t\t\"endpoint\": endpoint,\n\t\t\"seqID\":    seqID,\n\t})\n}\n\nfunc TrackingMovingOperation(connID uint64, endpoint string, seqID int64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s for %s seqID[%d]\", connID, TrackingMovingOperationMessage, endpoint, seqID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":   connID,\n\t\t\"endpoint\": endpoint,\n\t\t\"seqID\":    seqID,\n\t})\n}\n\nfunc UntrackingMovingOperation(connID uint64, seqID int64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s seqID[%d]\", connID, UntrackingMovingOperationMessage, seqID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t\t\"seqID\":  seqID,\n\t})\n}\n\nfunc OperationNotTracked(connID uint64, seqID int64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s seqID[%d]\", connID, OperationNotTrackedMessage, seqID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t\t\"seqID\":  seqID,\n\t})\n}\n\n// Connection pool functions\nfunc RemovingConnectionFromPool(connID uint64, reason error) string {\n\tmetadata := map[string]interface{}{\n\t\t\"connID\": connID,\n\t\t\"reason\": \"unknown\", // this will be overwritten if reason is not nil\n\t}\n\tif reason != nil {\n\t\tmetadata[\"reason\"] = reason.Error()\n\t}\n\n\tmessage := fmt.Sprintf(\"conn[%d] %s due to: %v\", connID, RemovingConnectionFromPoolMessage, reason)\n\treturn appendJSONIfDebug(message, metadata)\n}\n\nfunc NoPoolProvidedCannotRemove(connID uint64, reason error) string {\n\tmetadata := map[string]interface{}{\n\t\t\"connID\": connID,\n\t\t\"reason\": \"unknown\", // this will be overwritten if reason is not nil\n\t}\n\tif reason != nil {\n\t\tmetadata[\"reason\"] = reason.Error()\n\t}\n\n\tmessage := fmt.Sprintf(\"conn[%d] %s due to: %v\", connID, NoPoolProvidedMessageCannotRemoveMessage, reason)\n\treturn appendJSONIfDebug(message, metadata)\n}\n\n// Circuit breaker functions\nfunc CircuitBreakerOpen(connID uint64, endpoint string) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s for %s\", connID, CircuitBreakerOpenMessage, endpoint)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":   connID,\n\t\t\"endpoint\": endpoint,\n\t})\n}\n\n// Additional handoff functions for specific cases\nfunc ConnectionNotMarkedForHandoff(connID uint64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s\", connID, ConnectionNotMarkedForHandoffMessage)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t})\n}\n\nfunc ConnectionNotMarkedForHandoffError(connID uint64) string {\n\treturn fmt.Sprintf(\"conn[%d] %s\", connID, ConnectionNotMarkedForHandoffErrorMessage)\n}\n\nfunc HandoffRetryAttempt(connID uint64, retries int, newEndpoint string, oldEndpoint string) string {\n\tmessage := fmt.Sprintf(\"conn[%d] Retry %d: %s to %s(was %s)\", connID, retries, HandoffRetryAttemptMessage, newEndpoint, oldEndpoint)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":      connID,\n\t\t\"retries\":     retries,\n\t\t\"newEndpoint\": newEndpoint,\n\t\t\"oldEndpoint\": oldEndpoint,\n\t})\n}\n\nfunc CannotQueueHandoffForRetry(err error) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", CannotQueueHandoffForRetryMessage, err)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"error\": err.Error(),\n\t})\n}\n\n// Validation and error functions\nfunc InvalidNotificationFormat(notification interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", InvalidNotificationFormatMessage, notification)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notification\": fmt.Sprintf(\"%v\", notification),\n\t})\n}\n\nfunc InvalidNotificationTypeFormat(notificationType interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", InvalidNotificationTypeFormatMessage, notificationType)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notificationType\": fmt.Sprintf(\"%v\", notificationType),\n\t})\n}\n\n// InvalidNotification creates a log message for invalid notifications of any type\nfunc InvalidNotification(notificationType string, notification interface{}) string {\n\tmessage := fmt.Sprintf(\"invalid %s notification: %v\", notificationType, notification)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notificationType\": notificationType,\n\t\t\"notification\":     fmt.Sprintf(\"%v\", notification),\n\t})\n}\n\nfunc InvalidSeqIDInMovingNotification(seqID interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", InvalidSeqIDInMovingNotificationMessage, seqID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"seqID\": fmt.Sprintf(\"%v\", seqID),\n\t})\n}\n\nfunc InvalidTimeSInMovingNotification(timeS interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", InvalidTimeSInMovingNotificationMessage, timeS)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"timeS\": fmt.Sprintf(\"%v\", timeS),\n\t})\n}\n\nfunc InvalidNewEndpointInMovingNotification(newEndpoint interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", InvalidNewEndpointInMovingNotificationMessage, newEndpoint)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"newEndpoint\": fmt.Sprintf(\"%v\", newEndpoint),\n\t})\n}\n\nfunc NoConnectionInHandlerContext(notificationType string) string {\n\tmessage := fmt.Sprintf(\"%s for %s notification\", NoConnectionInHandlerContextMessage, notificationType)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notificationType\": notificationType,\n\t})\n}\n\nfunc InvalidConnectionTypeInHandlerContext(notificationType string, conn interface{}, handlerCtx interface{}) string {\n\tmessage := fmt.Sprintf(\"%s for %s notification - %T %#v\", InvalidConnectionTypeInHandlerContextMessage, notificationType, conn, handlerCtx)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notificationType\": notificationType,\n\t\t\"connType\":         fmt.Sprintf(\"%T\", conn),\n\t})\n}\n\nfunc SchedulingHandoffToCurrentEndpoint(connID uint64, seconds float64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s in %v seconds\", connID, SchedulingHandoffToCurrentEndpointMessage, seconds)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":  connID,\n\t\t\"seconds\": seconds,\n\t})\n}\n\nfunc ManagerNotInitialized() string {\n\treturn appendJSONIfDebug(ManagerNotInitializedMessage, map[string]interface{}{})\n}\n\nfunc FailedToRegisterHandler(notificationType string, err error) string {\n\tmessage := fmt.Sprintf(\"%s for %s: %v\", FailedToRegisterHandlerMessage, notificationType, err)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notificationType\": notificationType,\n\t\t\"error\":            err.Error(),\n\t})\n}\n\nfunc ShutdownError() string {\n\treturn appendJSONIfDebug(ShutdownErrorMessage, map[string]interface{}{})\n}\n\n// Configuration validation error functions\nfunc InvalidRelaxedTimeoutError() string {\n\treturn appendJSONIfDebug(InvalidRelaxedTimeoutErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidHandoffTimeoutError() string {\n\treturn appendJSONIfDebug(InvalidHandoffTimeoutErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidHandoffWorkersError() string {\n\treturn appendJSONIfDebug(InvalidHandoffWorkersErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidHandoffQueueSizeError() string {\n\treturn appendJSONIfDebug(InvalidHandoffQueueSizeErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidPostHandoffRelaxedDurationError() string {\n\treturn appendJSONIfDebug(InvalidPostHandoffRelaxedDurationErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidEndpointTypeError() string {\n\treturn appendJSONIfDebug(InvalidEndpointTypeErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidMaintNotificationsError() string {\n\treturn appendJSONIfDebug(InvalidMaintNotificationsErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidHandoffRetriesError() string {\n\treturn appendJSONIfDebug(InvalidHandoffRetriesErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidClientError() string {\n\treturn appendJSONIfDebug(InvalidClientErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidNotificationError() string {\n\treturn appendJSONIfDebug(InvalidNotificationErrorMessage, map[string]interface{}{})\n}\n\nfunc MaxHandoffRetriesReachedError() string {\n\treturn appendJSONIfDebug(MaxHandoffRetriesReachedErrorMessage, map[string]interface{}{})\n}\n\nfunc HandoffQueueFullError() string {\n\treturn appendJSONIfDebug(HandoffQueueFullErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidCircuitBreakerFailureThresholdError() string {\n\treturn appendJSONIfDebug(InvalidCircuitBreakerFailureThresholdErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidCircuitBreakerResetTimeoutError() string {\n\treturn appendJSONIfDebug(InvalidCircuitBreakerResetTimeoutErrorMessage, map[string]interface{}{})\n}\n\nfunc InvalidCircuitBreakerMaxRequestsError() string {\n\treturn appendJSONIfDebug(InvalidCircuitBreakerMaxRequestsErrorMessage, map[string]interface{}{})\n}\n\n// Configuration and debug functions\nfunc DebugLoggingEnabled() string {\n\treturn appendJSONIfDebug(DebugLoggingEnabledMessage, map[string]interface{}{})\n}\n\nfunc ConfigDebug(config interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %+v\", ConfigDebugMessage, config)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"config\": fmt.Sprintf(\"%+v\", config),\n\t})\n}\n\n// Handoff worker functions\nfunc WorkerExitingDueToShutdown() string {\n\treturn appendJSONIfDebug(WorkerExitingDueToShutdownMessage, map[string]interface{}{})\n}\n\nfunc WorkerExitingDueToShutdownWhileProcessing() string {\n\treturn appendJSONIfDebug(WorkerExitingDueToShutdownWhileProcessingMessage, map[string]interface{}{})\n}\n\nfunc WorkerPanicRecovered(panicValue interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", WorkerPanicRecoveredMessage, panicValue)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"panic\": fmt.Sprintf(\"%v\", panicValue),\n\t})\n}\n\nfunc WorkerExitingDueToInactivityTimeout(timeout interface{}) string {\n\tmessage := fmt.Sprintf(\"%s (%v)\", WorkerExitingDueToInactivityTimeoutMessage, timeout)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"timeout\": fmt.Sprintf(\"%v\", timeout),\n\t})\n}\n\nfunc ApplyingRelaxedTimeoutDueToPostHandoff(connID uint64, timeout interface{}, until string) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s (%v) until %s\", connID, ApplyingRelaxedTimeoutDueToPostHandoffMessage, timeout, until)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\":  connID,\n\t\t\"timeout\": fmt.Sprintf(\"%v\", timeout),\n\t\t\"until\":   until,\n\t})\n}\n\n// Example hooks functions\nfunc MetricsHookProcessingNotification(notificationType string, connID uint64) string {\n\tmessage := fmt.Sprintf(\"%s %s notification on conn[%d]\", MetricsHookProcessingNotificationMessage, notificationType, connID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notificationType\": notificationType,\n\t\t\"connID\":           connID,\n\t})\n}\n\nfunc MetricsHookRecordedError(notificationType string, connID uint64, err error) string {\n\tmessage := fmt.Sprintf(\"%s for %s notification on conn[%d]: %v\", MetricsHookRecordedErrorMessage, notificationType, connID, err)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"notificationType\": notificationType,\n\t\t\"connID\":           connID,\n\t\t\"error\":            err.Error(),\n\t})\n}\n\n// Pool hook functions\nfunc MarkedForHandoff(connID uint64) string {\n\tmessage := fmt.Sprintf(\"conn[%d] %s\", connID, MarkedForHandoffMessage)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"connID\": connID,\n\t})\n}\n\n// Circuit breaker additional functions\nfunc CircuitBreakerTransitioningToHalfOpen(endpoint string) string {\n\tmessage := fmt.Sprintf(\"%s for %s\", CircuitBreakerTransitioningToHalfOpenMessage, endpoint)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"endpoint\": endpoint,\n\t})\n}\n\nfunc CircuitBreakerOpened(endpoint string, failures int64) string {\n\tmessage := fmt.Sprintf(\"%s for endpoint %s after %d failures\", CircuitBreakerOpenedMessage, endpoint, failures)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"endpoint\": endpoint,\n\t\t\"failures\": failures,\n\t})\n}\n\nfunc CircuitBreakerReopened(endpoint string) string {\n\tmessage := fmt.Sprintf(\"%s for endpoint %s due to failure in half-open state\", CircuitBreakerReopenedMessage, endpoint)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"endpoint\": endpoint,\n\t})\n}\n\nfunc CircuitBreakerClosed(endpoint string, successes int64) string {\n\tmessage := fmt.Sprintf(\"%s for endpoint %s after %d successful requests\", CircuitBreakerClosedMessage, endpoint, successes)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"endpoint\":  endpoint,\n\t\t\"successes\": successes,\n\t})\n}\n\nfunc CircuitBreakerCleanup(removed int, total int) string {\n\tmessage := fmt.Sprintf(\"%s removed %d/%d entries\", CircuitBreakerCleanupMessage, removed, total)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"removed\": removed,\n\t\t\"total\":   total,\n\t})\n}\n\n// ExtractDataFromLogMessage extracts structured data from maintnotifications log messages\n// Returns a map containing the parsed key-value pairs from the structured data section\n// Example: \"conn[123] handoff started to localhost:6379 {\"connID\":123,\"endpoint\":\"localhost:6379\"}\"\n// Returns: map[string]interface{}{\"connID\": 123, \"endpoint\": \"localhost:6379\"}\nfunc ExtractDataFromLogMessage(logMessage string) map[string]interface{} {\n\tresult := make(map[string]interface{})\n\n\t// Find the JSON data section at the end of the message\n\tre := regexp.MustCompile(`(\\{.*\\})$`)\n\tmatches := re.FindStringSubmatch(logMessage)\n\tif len(matches) < 2 {\n\t\treturn result\n\t}\n\n\tjsonStr := matches[1]\n\tif jsonStr == \"\" {\n\t\treturn result\n\t}\n\n\t// Parse the JSON directly\n\tvar jsonResult map[string]interface{}\n\tif err := json.Unmarshal([]byte(jsonStr), &jsonResult); err == nil {\n\t\treturn jsonResult\n\t}\n\n\t// If JSON parsing fails, return empty map\n\treturn result\n}\n\n// Cluster notification functions\nfunc InvalidSeqIDInSMigratingNotification(seqID interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", InvalidSeqIDInSMigratingNotificationMessage, seqID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"seqID\": fmt.Sprintf(\"%v\", seqID),\n\t})\n}\n\nfunc InvalidSeqIDInSMigratedNotification(seqID interface{}) string {\n\tmessage := fmt.Sprintf(\"%s: %v\", InvalidSeqIDInSMigratedNotificationMessage, seqID)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"seqID\": fmt.Sprintf(\"%v\", seqID),\n\t})\n}\n\n// TriggeringClusterStateReload logs when cluster state reload is triggered (deduplicated, once per seqID)\nfunc TriggeringClusterStateReload(seqID int64, hostPort string, slotRanges []string) string {\n\tmessage := fmt.Sprintf(\"%s seqID=%d host:port=%s slots=%v\", TriggeringClusterStateReloadMessage, seqID, hostPort, slotRanges)\n\treturn appendJSONIfDebug(message, map[string]interface{}{\n\t\t\"seqID\":      seqID,\n\t\t\"hostPort\":   hostPort,\n\t\t\"slotRanges\": slotRanges,\n\t})\n}\n"
  },
  {
    "path": "internal/once.go",
    "content": "/*\nCopyright 2014 The Camlistore Authors\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage internal\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\n// A Once will perform a successful action exactly once.\n//\n// Unlike a sync.Once, this Once's func returns an error\n// and is re-armed on failure.\ntype Once struct {\n\tm    sync.Mutex\n\tdone uint32\n}\n\n// Do calls the function f if and only if Do has not been invoked\n// without error for this instance of Once.  In other words, given\n//\n//\tvar once Once\n//\n// if once.Do(f) is called multiple times, only the first call will\n// invoke f, even if f has a different value in each invocation unless\n// f returns an error.  A new instance of Once is required for each\n// function to execute.\n//\n// Do is intended for initialization that must be run exactly once.  Since f\n// is niladic, it may be necessary to use a function literal to capture the\n// arguments to a function to be invoked by Do:\n//\n//\terr := config.once.Do(func() error { return config.init(filename) })\nfunc (o *Once) Do(f func() error) error {\n\tif atomic.LoadUint32(&o.done) == 1 {\n\t\treturn nil\n\t}\n\t// Slow-path.\n\to.m.Lock()\n\tdefer o.m.Unlock()\n\tvar err error\n\tif o.done == 0 {\n\t\terr = f()\n\t\tif err == nil {\n\t\t\tatomic.StoreUint32(&o.done, 1)\n\t\t}\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "internal/otel/metrics.go",
    "content": "package otel\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// generateUniqueID generates a short unique identifier for pool names.\nfunc generateUniqueID() string {\n\tb := make([]byte, 4)\n\tif _, err := rand.Read(b); err != nil {\n\t\treturn \"\"\n\t}\n\treturn hex.EncodeToString(b)\n}\n\n// Cmder is a minimal interface for command information needed for metrics.\n// This avoids circular dependencies with the main redis package.\ntype Cmder interface {\n\tName() string\n\tFullName() string\n\tArgs() []interface{}\n\tErr() error\n}\n\n// Recorder is the interface for recording metrics.\ntype Recorder interface {\n\t// RecordOperationDuration records the total operation duration (including all retries)\n\t// dbIndex is the Redis database index (0-15)\n\tRecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int)\n\n\t// RecordPipelineOperationDuration records the total pipeline/transaction duration.\n\t// operationName should be \"PIPELINE\" for regular pipelines or \"MULTI\" for transactions.\n\t// cmdCount is the number of commands in the pipeline.\n\t// err is the error from the pipeline execution (can be nil).\n\t// dbIndex is the Redis database index (0-15)\n\tRecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int)\n\n\t// RecordConnectionCreateTime records the time it took to create a new connection\n\tRecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn)\n\n\t// RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed\n\t// delta: +1 for relaxed, -1 for unrelaxed\n\t// poolName: name of the connection pool (e.g., \"main\", \"pubsub\")\n\t// notificationType: the notification type that triggered the timeout relaxation (e.g., \"MOVING\")\n\tRecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string)\n\n\t// RecordConnectionHandoff records when a connection is handed off to another node\n\t// poolName: name of the connection pool (e.g., \"main\", \"pubsub\")\n\tRecordConnectionHandoff(ctx context.Context, cn *pool.Conn, poolName string)\n\n\t// RecordError records client errors (ASK, MOVED, handshake failures, etc.)\n\t// errorType: type of error (e.g., \"ASK\", \"MOVED\", \"HANDSHAKE_FAILED\")\n\t// statusCode: Redis response status code if available (e.g., \"MOVED\", \"ASK\")\n\t// isInternal: whether this is an internal error\n\t// retryAttempts: number of retry attempts made\n\tRecordError(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int)\n\n\t// RecordMaintenanceNotification records when a maintenance notification is received\n\t// notificationType: the type of notification (e.g., \"MOVING\", \"MIGRATING\", etc.)\n\tRecordMaintenanceNotification(ctx context.Context, cn *pool.Conn, notificationType string)\n\n\t// RecordConnectionWaitTime records the time spent waiting for a connection from the pool\n\tRecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn *pool.Conn)\n\n\t// RecordConnectionClosed records when a connection is closed\n\t// reason: reason for closing (e.g., \"idle\", \"max_lifetime\", \"error\", \"pool_closed\")\n\t// err: the error that caused the close (nil for non-error closures)\n\tRecordConnectionClosed(ctx context.Context, cn *pool.Conn, reason string, err error)\n\n\t// RecordPubSubMessage records a Pub/Sub message\n\t// direction: \"sent\" or \"received\"\n\t// channel: channel name (may be hidden for cardinality reduction)\n\t// sharded: true for sharded pub/sub (SPUBLISH/SSUBSCRIBE)\n\tRecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool)\n\n\t// RecordStreamLag records the lag for stream consumer group processing\n\t// lag: time difference between message creation and consumption\n\t// streamName: name of the stream (may be hidden for cardinality reduction)\n\t// consumerGroup: name of the consumer group\n\t// consumerName: name of the consumer\n\tRecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string)\n}\n\ntype PubSubPooler interface {\n\tStats() *pool.PubSubStats\n}\n\ntype PoolRegistrar interface {\n\t// RegisterPool is called when a new client is created with its connection pools.\n\t// poolName: identifier for the pool (e.g., \"main_abc123\")\n\t// pool: the connection pool\n\tRegisterPool(poolName string, pool pool.Pooler)\n\t// UnregisterPool is called when a client is closed to remove its pool from the registry.\n\t// pool: the connection pool to unregister\n\tUnregisterPool(pool pool.Pooler)\n\t// RegisterPubSubPool is called when a new client is created with a PubSub pool.\n\t// poolName: identifier for the pool (e.g., \"main_abc123_pubsub\")\n\t// pool: the PubSub connection pool\n\tRegisterPubSubPool(poolName string, pool PubSubPooler)\n\t// UnregisterPubSubPool is called when a PubSub client is closed to remove its pool.\n\t// pool: the PubSub connection pool to unregister\n\tUnregisterPubSubPool(pool PubSubPooler)\n}\n\nvar (\n\t// recorderMu protects globalRecorder and operation duration callbacks\n\trecorderMu sync.RWMutex\n\n\t// Global recorder instance (initialized by extra/redisotel-native)\n\tglobalRecorder Recorder = noopRecorder{}\n\n\t// Callbacks for operation duration metrics\n\toperationDurationCallback         func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int)\n\tpipelineOperationDurationCallback func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int)\n)\n\n// GetOperationDurationCallback returns the callback for operation duration.\nfunc GetOperationDurationCallback() func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\trecorderMu.RLock()\n\tcb := operationDurationCallback\n\trecorderMu.RUnlock()\n\treturn cb\n}\n\n// GetPipelineOperationDurationCallback returns the callback for pipeline operation duration.\nfunc GetPipelineOperationDurationCallback() func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\trecorderMu.RLock()\n\tcb := pipelineOperationDurationCallback\n\trecorderMu.RUnlock()\n\treturn cb\n}\n\n// getRecorder returns the current global recorder under a read lock.\nfunc getRecorder() Recorder {\n\trecorderMu.RLock()\n\tr := globalRecorder\n\trecorderMu.RUnlock()\n\treturn r\n}\n\n// SetGlobalRecorder sets the global recorder (called by Init() in extra/redisotel-native)\nfunc SetGlobalRecorder(r Recorder) {\n\trecorderMu.Lock()\n\tif r == nil {\n\t\tglobalRecorder = noopRecorder{}\n\t\toperationDurationCallback = nil\n\t\tpipelineOperationDurationCallback = nil\n\t\trecorderMu.Unlock()\n\t\t// Unregister all pool metric callbacks atomically\n\t\tpool.SetAllMetricCallbacks(nil)\n\t\treturn\n\t}\n\tglobalRecorder = r\n\n\t// Register operation duration callbacks\n\t// These capture r directly since we want them to use the specific recorder\n\t// that was set at this point in time\n\toperationDurationCallback = func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\t\tgetRecorder().RecordOperationDuration(ctx, duration, cmd, attempts, err, cn, dbIndex)\n\t}\n\tpipelineOperationDurationCallback = func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\t\tgetRecorder().RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, cn, dbIndex)\n\t}\n\trecorderMu.Unlock()\n\n\t// Register all pool metric callbacks atomically\n\t// These use getRecorder() to safely access the current recorder\n\tpool.SetAllMetricCallbacks(&pool.MetricCallbacks{\n\t\tConnectionCreateTime: func(ctx context.Context, duration time.Duration, cn *pool.Conn) {\n\t\t\tgetRecorder().RecordConnectionCreateTime(ctx, duration, cn)\n\t\t},\n\t\tConnectionRelaxedTimeout: func(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string) {\n\t\t\tgetRecorder().RecordConnectionRelaxedTimeout(ctx, delta, cn, poolName, notificationType)\n\t\t},\n\t\tConnectionHandoff: func(ctx context.Context, cn *pool.Conn, poolName string) {\n\t\t\tgetRecorder().RecordConnectionHandoff(ctx, cn, poolName)\n\t\t},\n\t\tError: func(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int) {\n\t\t\tgetRecorder().RecordError(ctx, errorType, cn, statusCode, isInternal, retryAttempts)\n\t\t},\n\t\tMaintenanceNotification: func(ctx context.Context, cn *pool.Conn, notificationType string) {\n\t\t\tgetRecorder().RecordMaintenanceNotification(ctx, cn, notificationType)\n\t\t},\n\t\tConnectionWaitTime: func(ctx context.Context, duration time.Duration, cn *pool.Conn) {\n\t\t\tgetRecorder().RecordConnectionWaitTime(ctx, duration, cn)\n\t\t},\n\t\tConnectionClosed: func(ctx context.Context, cn *pool.Conn, reason string, err error) {\n\t\t\tgetRecorder().RecordConnectionClosed(ctx, cn, reason, err)\n\t\t},\n\t})\n}\n\n// RecordOperationDuration records the total operation duration.\n// dbIndex is the Redis database index (0-15).\nfunc RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\tgetRecorder().RecordOperationDuration(ctx, duration, cmd, attempts, err, cn, dbIndex)\n}\n\n// RecordPipelineOperationDuration records the total pipeline/transaction duration.\n// This is called from redis.go after pipeline/transaction execution completes.\n// operationName should be \"PIPELINE\" for regular pipelines or \"MULTI\" for transactions.\n// err is the error from the pipeline execution (can be nil).\n// dbIndex is the Redis database index (0-15).\nfunc RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\tgetRecorder().RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, cn, dbIndex)\n}\n\n// RecordConnectionCreateTime records the time it took to create a new connection.\nfunc RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn) {\n\tgetRecorder().RecordConnectionCreateTime(ctx, duration, cn)\n}\n\n// RecordPubSubMessage records a Pub/Sub message sent or received.\nfunc RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool) {\n\tgetRecorder().RecordPubSubMessage(ctx, cn, direction, channel, sharded)\n}\n\n// RecordStreamLag records the lag between message creation and consumption in a stream.\nfunc RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) {\n\tgetRecorder().RecordStreamLag(ctx, lag, cn, streamName, consumerGroup, consumerName)\n}\n\ntype noopRecorder struct{}\n\nfunc (noopRecorder) RecordOperationDuration(context.Context, time.Duration, Cmder, int, error, *pool.Conn, int) {\n}\nfunc (noopRecorder) RecordPipelineOperationDuration(context.Context, time.Duration, string, int, int, error, *pool.Conn, int) {\n}\nfunc (noopRecorder) RecordConnectionCreateTime(context.Context, time.Duration, *pool.Conn) {}\nfunc (noopRecorder) RecordConnectionRelaxedTimeout(context.Context, int, *pool.Conn, string, string) {\n}\nfunc (noopRecorder) RecordConnectionHandoff(context.Context, *pool.Conn, string)        {}\nfunc (noopRecorder) RecordError(context.Context, string, *pool.Conn, string, bool, int) {}\nfunc (noopRecorder) RecordMaintenanceNotification(context.Context, *pool.Conn, string)  {}\n\nfunc (noopRecorder) RecordConnectionWaitTime(context.Context, time.Duration, *pool.Conn) {}\nfunc (noopRecorder) RecordConnectionClosed(context.Context, *pool.Conn, string, error)   {}\n\nfunc (noopRecorder) RecordPubSubMessage(context.Context, *pool.Conn, string, string, bool) {}\n\nfunc (noopRecorder) RecordStreamLag(context.Context, time.Duration, *pool.Conn, string, string, string) {\n}\n\n// RegisterPools registers connection pools with the global recorder.\nfunc RegisterPools(connPool pool.Pooler, pubSubPool PubSubPooler, addr string) {\n\t// Check if the global recorder implements PoolRegistrar\n\tif registrar, ok := globalRecorder.(PoolRegistrar); ok {\n\t\t// Generate a unique ID for this client's pools\n\t\tuniqueID := generateUniqueID()\n\n\t\tif connPool != nil {\n\t\t\tpoolName := addr + \"_\" + uniqueID\n\t\t\tregistrar.RegisterPool(poolName, connPool)\n\t\t}\n\t\tif pubSubPool != nil {\n\t\t\tpoolName := addr + \"_\" + uniqueID + \"_pubsub\"\n\t\t\tregistrar.RegisterPubSubPool(poolName, pubSubPool)\n\t\t}\n\t}\n}\n\n// UnregisterPools removes connection pools from the global recorder\nfunc UnregisterPools(connPool pool.Pooler, pubSubPool PubSubPooler) {\n\t// Check if the global recorder implements PoolRegistrar\n\tif registrar, ok := globalRecorder.(PoolRegistrar); ok {\n\t\tif connPool != nil {\n\t\t\tregistrar.UnregisterPool(connPool)\n\t\t}\n\t\tif pubSubPool != nil {\n\t\t\tregistrar.UnregisterPubSubPool(pubSubPool)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/pool/bench_test.go",
    "content": "package pool_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\ntype poolGetPutBenchmark struct {\n\tpoolSize int\n}\n\nfunc (bm poolGetPutBenchmark) String() string {\n\treturn fmt.Sprintf(\"pool=%d\", bm.poolSize)\n}\n\nfunc BenchmarkPoolGetPut(b *testing.B) {\n\tctx := context.Background()\n\tbenchmarks := []poolGetPutBenchmark{\n\t\t{1},\n\t\t{2},\n\t\t{8},\n\t\t{32},\n\t\t{64},\n\t\t{128},\n\t}\n\tfor _, bm := range benchmarks {\n\t\tb.Run(bm.String(), func(b *testing.B) {\n\t\t\tconnPool := pool.NewConnPool(&pool.Options{\n\t\t\t\tDialer:             dummyDialer,\n\t\t\t\tPoolSize:           int32(bm.poolSize),\n\t\t\t\tMaxConcurrentDials: bm.poolSize,\n\t\t\t\tPoolTimeout:        time.Second,\n\t\t\t\tDialTimeout:        1 * time.Second,\n\t\t\t\tConnMaxIdleTime:    time.Hour,\n\t\t\t})\n\n\t\t\tb.ResetTimer()\n\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tb.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tconnPool.Put(ctx, cn)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\ntype poolGetRemoveBenchmark struct {\n\tpoolSize int\n}\n\nfunc (bm poolGetRemoveBenchmark) String() string {\n\treturn fmt.Sprintf(\"pool=%d\", bm.poolSize)\n}\n\nfunc BenchmarkPoolGetRemove(b *testing.B) {\n\tctx := context.Background()\n\tbenchmarks := []poolGetRemoveBenchmark{\n\t\t{1},\n\t\t{2},\n\t\t{8},\n\t\t{32},\n\t\t{64},\n\t\t{128},\n\t}\n\n\tfor _, bm := range benchmarks {\n\t\tb.Run(bm.String(), func(b *testing.B) {\n\t\t\tconnPool := pool.NewConnPool(&pool.Options{\n\t\t\t\tDialer:             dummyDialer,\n\t\t\t\tPoolSize:           int32(bm.poolSize),\n\t\t\t\tMaxConcurrentDials: bm.poolSize,\n\t\t\t\tPoolTimeout:        time.Second,\n\t\t\t\tDialTimeout:        1 * time.Second,\n\t\t\t\tConnMaxIdleTime:    time.Hour,\n\t\t\t})\n\n\t\t\tb.ResetTimer()\n\t\t\trmvErr := errors.New(\"Bench test remove\")\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tb.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tconnPool.Remove(ctx, cn, rmvErr)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/pool/buffer_size_test.go",
    "content": "package pool_test\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"sync/atomic\"\n\t\"unsafe\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\nvar _ = Describe(\"Buffer Size Configuration\", func() {\n\tvar connPool *pool.ConnPool\n\tctx := context.Background()\n\n\tAfterEach(func() {\n\t\tif connPool != nil {\n\t\t\tconnPool.Close()\n\t\t}\n\t})\n\n\tIt(\"should use default buffer sizes when not specified\", func() {\n\t\tconnPool = pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           int32(1),\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        1000,\n\t\t})\n\n\t\tcn, err := connPool.NewConn(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tdefer connPool.CloseConn(ctx, cn, pool.CloseReasonTest, pool.MetricStateIdle)\n\n\t\t// Check that default buffer sizes are used (32KiB)\n\t\twriterBufSize := getWriterBufSizeUnsafe(cn)\n\t\treaderBufSize := getReaderBufSizeUnsafe(cn)\n\n\t\tExpect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t\tExpect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t})\n\n\tIt(\"should use custom buffer sizes when specified\", func() {\n\t\tcustomReadSize := 32 * 1024  // 32KB\n\t\tcustomWriteSize := 64 * 1024 // 64KB\n\n\t\tconnPool = pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           int32(1),\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        1000,\n\t\t\tReadBufferSize:     customReadSize,\n\t\t\tWriteBufferSize:    customWriteSize,\n\t\t})\n\n\t\tcn, err := connPool.NewConn(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tdefer connPool.CloseConn(ctx, cn, pool.CloseReasonTest, pool.MetricStateIdle)\n\n\t\t// Check that custom buffer sizes are used\n\t\twriterBufSize := getWriterBufSizeUnsafe(cn)\n\t\treaderBufSize := getReaderBufSizeUnsafe(cn)\n\n\t\tExpect(writerBufSize).To(Equal(customWriteSize))\n\t\tExpect(readerBufSize).To(Equal(customReadSize))\n\t})\n\n\tIt(\"should handle zero buffer sizes by using defaults\", func() {\n\t\tconnPool = pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           int32(1),\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        1000,\n\t\t\tReadBufferSize:     0, // Should use default\n\t\t\tWriteBufferSize:    0, // Should use default\n\t\t})\n\n\t\tcn, err := connPool.NewConn(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tdefer connPool.CloseConn(ctx, cn, pool.CloseReasonTest, pool.MetricStateIdle)\n\n\t\t// Check that default buffer sizes are used (32KiB)\n\t\twriterBufSize := getWriterBufSizeUnsafe(cn)\n\t\treaderBufSize := getReaderBufSizeUnsafe(cn)\n\n\t\tExpect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t\tExpect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t})\n\n\tIt(\"should use 32KiB default buffer sizes for standalone NewConn\", func() {\n\t\t// Test that NewConn (without pool) also uses 32KiB buffers\n\t\tnetConn := newDummyConn()\n\t\tcn := pool.NewConn(netConn)\n\t\tdefer cn.Close()\n\n\t\twriterBufSize := getWriterBufSizeUnsafe(cn)\n\t\treaderBufSize := getReaderBufSizeUnsafe(cn)\n\n\t\tExpect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t\tExpect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t})\n\n\tIt(\"should use 32KiB defaults even when pool is created directly without buffer sizes\", func() {\n\t\t// Test the scenario where someone creates a pool directly (like in tests)\n\t\t// without setting ReadBufferSize and WriteBufferSize\n\t\tconnPool = pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           int32(1),\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        1000,\n\t\t\t// ReadBufferSize and WriteBufferSize are not set (will be 0)\n\t\t})\n\n\t\tcn, err := connPool.NewConn(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tdefer connPool.CloseConn(ctx, cn, pool.CloseReasonTest, pool.MetricStateIdle)\n\n\t\t// Should still get 32KiB defaults because NewConnPool sets them\n\t\twriterBufSize := getWriterBufSizeUnsafe(cn)\n\t\treaderBufSize := getReaderBufSizeUnsafe(cn)\n\n\t\tExpect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t\tExpect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 32KiB buffer size\n\t})\n})\n\n// Helper functions to extract buffer sizes using unsafe pointers\n// The struct layout must match pool.Conn exactly to avoid checkptr violations.\n// checkptr is Go's pointer safety checker, which ensures that unsafe pointer\n// conversions are valid. If the struct layouts do not match exactly, this can\n// cause runtime panics or incorrect memory access due to invalid pointer dereferencing.\nfunc getWriterBufSizeUnsafe(cn *pool.Conn) int {\n\tcnPtr := (*struct {\n\t\tid            uint64       // First field in pool.Conn\n\t\tusedAt        atomic.Int64 // Second field (atomic)\n\t\tlastPutAt     atomic.Int64 // Third field (atomic)\n\t\tcheckoutAt    atomic.Int64 // Fourth field (atomic)\n\t\tnetConnAtomic atomic.Value // atomic.Value for net.Conn\n\t\trd            *proto.Reader\n\t\tbw            *bufio.Writer\n\t\twr            *proto.Writer\n\t\t// We only need fields up to bw, so we can stop here\n\t})(unsafe.Pointer(cn))\n\n\tif cnPtr.bw == nil {\n\t\treturn -1\n\t}\n\n\t// bufio.Writer internal structure\n\tbwPtr := (*struct {\n\t\terr error\n\t\tbuf []byte\n\t\tn   int\n\t\twr  interface{}\n\t})(unsafe.Pointer(cnPtr.bw))\n\n\treturn len(bwPtr.buf)\n}\n\nfunc getReaderBufSizeUnsafe(cn *pool.Conn) int {\n\tcnPtr := (*struct {\n\t\tid            uint64       // First field in pool.Conn\n\t\tusedAt        atomic.Int64 // Second field (atomic)\n\t\tlastPutAt     atomic.Int64 // Third field (atomic)\n\t\tcheckoutAt    atomic.Int64 // Fourth field (atomic)\n\t\tnetConnAtomic atomic.Value // atomic.Value for net.Conn\n\t\trd            *proto.Reader\n\t\tbw            *bufio.Writer\n\t\twr            *proto.Writer\n\t\t// We only need fields up to rd, so we can stop here\n\t})(unsafe.Pointer(cn))\n\n\tif cnPtr.rd == nil {\n\t\treturn -1\n\t}\n\n\t// proto.Reader internal structure\n\trdPtr := (*struct {\n\t\trd *bufio.Reader\n\t})(unsafe.Pointer(cnPtr.rd))\n\n\tif rdPtr.rd == nil {\n\t\treturn -1\n\t}\n\n\t// bufio.Reader internal structure\n\tbufReaderPtr := (*struct {\n\t\tbuf          []byte\n\t\trd           interface{}\n\t\tr, w         int\n\t\terr          error\n\t\tlastByte     int\n\t\tlastRuneSize int\n\t})(unsafe.Pointer(rdPtr.rd))\n\n\treturn len(bufReaderPtr.buf)\n}\n"
  },
  {
    "path": "internal/pool/conn.go",
    "content": "// Package pool implements the pool management\npackage pool\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\nvar noDeadline = time.Time{}\n\n// Preallocated errors for hot paths to avoid allocations\nvar (\n\terrAlreadyMarkedForHandoff  = errors.New(\"connection is already marked for handoff\")\n\terrNotMarkedForHandoff      = errors.New(\"connection was not marked for handoff\")\n\terrHandoffStateChanged      = errors.New(\"handoff state changed during marking\")\n\terrConnectionNotAvailable   = errors.New(\"redis: connection not available\")\n\terrConnNotAvailableForWrite = errors.New(\"redis: connection not available for write operation\")\n)\n\n// getCachedTimeNs returns the current time in nanoseconds.\n// This function previously used a global cache updated by a background goroutine,\n// but that caused unnecessary CPU usage when the client was idle (ticker waking up\n// the scheduler every 50ms). We now use time.Now() directly, which is fast enough\n// on modern systems (vDSO on Linux) and only adds ~1-2% overhead in extreme\n// high-concurrency benchmarks while eliminating idle CPU usage.\nfunc getCachedTimeNs() int64 {\n\treturn time.Now().UnixNano()\n}\n\n// GetCachedTimeNs returns the current time in nanoseconds.\n// Exported for use by other packages that need fast time access.\nfunc GetCachedTimeNs() int64 {\n\treturn getCachedTimeNs()\n}\n\n// Global atomic counter for connection IDs\nvar connIDCounter uint64\n\n// HandoffState represents the atomic state for connection handoffs\n// This struct is stored atomically to prevent race conditions between\n// checking handoff status and reading handoff parameters\ntype HandoffState struct {\n\tShouldHandoff bool   // Whether connection should be handed off\n\tEndpoint      string // New endpoint for handoff\n\tSeqID         int64  // Sequence ID from MOVING notification\n}\n\n// atomicNetConn is a wrapper to ensure consistent typing in atomic.Value\ntype atomicNetConn struct {\n\tconn net.Conn\n}\n\n// generateConnID generates a fast unique identifier for a connection with zero allocations\nfunc generateConnID() uint64 {\n\treturn atomic.AddUint64(&connIDCounter, 1)\n}\n\ntype Conn struct {\n\t// Connection identifier for unique tracking\n\tid uint64\n\n\tusedAt      atomic.Int64\n\tlastPutAt   atomic.Int64\n\tdialStartNs atomic.Int64 // Time when dial started (for connection create time metric)\n\n\t// Lock-free netConn access using atomic.Value\n\t// Contains *atomicNetConn wrapper, accessed atomically for better performance\n\tnetConnAtomic atomic.Value // stores *atomicNetConn\n\n\trd *proto.Reader\n\tbw *bufio.Writer\n\twr *proto.Writer\n\n\t// Lightweight mutex to protect reader operations during handoff\n\t// Only used for the brief period during SetNetConn and HasBufferedData/PeekReplyTypeSafe\n\treaderMu sync.RWMutex\n\n\t// State machine for connection state management\n\t// Replaces: usable, Inited, used\n\t// Provides thread-safe state transitions with FIFO waiting queue\n\t// States: CREATED → INITIALIZING → IDLE ⇄ IN_USE\n\t//                                    ↓\n\t//                                UNUSABLE (handoff/reauth)\n\t//                                    ↓\n\t//                                IDLE/CLOSED\n\tstateMachine *ConnStateMachine\n\n\t// Handoff metadata - managed separately from state machine\n\t// These are atomic for lock-free access during handoff operations\n\thandoffStateAtomic   atomic.Value  // stores *HandoffState\n\thandoffRetriesAtomic atomic.Uint32 // retry counter\n\n\tpooled    bool\n\tpubsub    bool\n\tclosed    atomic.Bool\n\tcreatedAt time.Time\n\texpiresAt time.Time\n\tpoolName  string // Name of the pool this connection belongs to (for metrics)\n\n\t// maintenanceNotifications upgrade support: relaxed timeouts during migrations/failovers\n\n\t// Using atomic operations for lock-free access to avoid mutex contention\n\trelaxedReadTimeoutNs  atomic.Int64 // time.Duration as nanoseconds\n\trelaxedWriteTimeoutNs atomic.Int64 // time.Duration as nanoseconds\n\trelaxedDeadlineNs     atomic.Int64 // time.Time as nanoseconds since epoch\n\n\t// Counter to track multiple relaxed timeout setters if we have nested calls\n\t// will be decremented when ClearRelaxedTimeout is called or deadline is reached\n\t// if counter reaches 0, we clear the relaxed timeouts\n\trelaxedCounter atomic.Int32\n\n\t// Connection initialization function for reconnections\n\tinitConnFunc func(context.Context, *Conn) error\n\n\tonClose func() error\n}\n\nfunc NewConn(netConn net.Conn) *Conn {\n\treturn NewConnWithBufferSize(netConn, proto.DefaultBufferSize, proto.DefaultBufferSize)\n}\n\nfunc NewConnWithBufferSize(netConn net.Conn, readBufSize, writeBufSize int) *Conn {\n\tnow := time.Now()\n\tcn := &Conn{\n\t\tcreatedAt:    now,\n\t\tid:           generateConnID(), // Generate unique ID for this connection\n\t\tstateMachine: NewConnStateMachine(),\n\t}\n\n\t// Use specified buffer sizes, or fall back to 32KiB defaults if 0\n\tif readBufSize > 0 {\n\t\tcn.rd = proto.NewReaderSize(netConn, readBufSize)\n\t} else {\n\t\tcn.rd = proto.NewReader(netConn) // Uses 32KiB default\n\t}\n\n\tif writeBufSize > 0 {\n\t\tcn.bw = bufio.NewWriterSize(netConn, writeBufSize)\n\t} else {\n\t\tcn.bw = bufio.NewWriterSize(netConn, proto.DefaultBufferSize)\n\t}\n\n\t// Store netConn atomically for lock-free access using wrapper\n\tcn.netConnAtomic.Store(&atomicNetConn{conn: netConn})\n\n\tcn.wr = proto.NewWriter(cn.bw)\n\tcn.SetUsedAt(now)\n\t// Initialize handoff state atomically\n\tinitialHandoffState := &HandoffState{\n\t\tShouldHandoff: false,\n\t\tEndpoint:      \"\",\n\t\tSeqID:         0,\n\t}\n\tcn.handoffStateAtomic.Store(initialHandoffState)\n\treturn cn\n}\n\nfunc (cn *Conn) UsedAt() time.Time {\n\treturn time.Unix(0, cn.usedAt.Load())\n}\nfunc (cn *Conn) SetUsedAt(tm time.Time) {\n\tcn.usedAt.Store(tm.UnixNano())\n}\n\nfunc (cn *Conn) UsedAtNs() int64 {\n\treturn cn.usedAt.Load()\n}\nfunc (cn *Conn) SetUsedAtNs(ns int64) {\n\tcn.usedAt.Store(ns)\n}\n\nfunc (cn *Conn) LastPutAtNs() int64 {\n\treturn cn.lastPutAt.Load()\n}\nfunc (cn *Conn) SetLastPutAtNs(ns int64) {\n\tcn.lastPutAt.Store(ns)\n}\n\n// GetDialStartNs returns the time when the dial started (in nanoseconds since epoch).\n// This is used to calculate the full connection creation time (TCP + handshake).\nfunc (cn *Conn) GetDialStartNs() int64 {\n\treturn cn.dialStartNs.Load()\n}\n\n// PoolName returns the name of the pool this connection belongs to.\n// This is used for metrics to identify which pool a connection is from.\nfunc (cn *Conn) PoolName() string {\n\treturn cn.poolName\n}\n\n// SetPoolName sets the name of the pool this connection belongs to.\n// This should be called when the connection is added to a pool.\nfunc (cn *Conn) SetPoolName(name string) {\n\tcn.poolName = name\n}\n\n// Backward-compatible wrapper methods for state machine\n// These maintain the existing API while using the new state machine internally\n\n// CompareAndSwapUsable atomically compares and swaps the usable flag (lock-free).\n//\n// This is used by background operations (handoff, re-auth) to acquire exclusive\n// access to a connection. The operation sets usable to false, preventing the pool\n// from returning the connection to clients.\n//\n// Returns true if the swap was successful (old value matched), false otherwise.\n//\n// Implementation note: This is a compatibility wrapper around the state machine.\n// It checks if the current state is \"usable\" (IDLE or IN_USE) and transitions accordingly.\n// Deprecated: Use GetStateMachine().TryTransition() directly for better state management.\nfunc (cn *Conn) CompareAndSwapUsable(old, new bool) bool {\n\tcurrentState := cn.stateMachine.GetState()\n\n\t// Check if current state matches the \"old\" usable value\n\tcurrentUsable := (currentState == StateIdle || currentState == StateInUse)\n\tif currentUsable != old {\n\t\treturn false\n\t}\n\n\t// If we're trying to set to the same value, succeed immediately\n\tif old == new {\n\t\treturn true\n\t}\n\n\t// Transition based on new value\n\tif new {\n\t\t// Trying to make usable - transition from UNUSABLE to IDLE\n\t\t// This should only work from UNUSABLE or INITIALIZING states\n\t\t// Use predefined slice to avoid allocation\n\t\t_, err := cn.stateMachine.TryTransition(\n\t\t\tvalidFromInitializingOrUnusable,\n\t\t\tStateIdle,\n\t\t)\n\t\treturn err == nil\n\t}\n\t// Trying to make unusable - transition from IDLE to UNUSABLE\n\t// This is typically for acquiring the connection for background operations\n\t// Use predefined slice to avoid allocation\n\t_, err := cn.stateMachine.TryTransition(\n\t\tvalidFromIdle,\n\t\tStateUnusable,\n\t)\n\treturn err == nil\n}\n\n// IsUsable returns true if the connection is safe to use for new commands (lock-free).\n//\n// A connection is \"usable\" when it's in a stable state and can be returned to clients.\n// It becomes unusable during:\n//   - Handoff operations (network connection replacement)\n//   - Re-authentication (credential updates)\n//   - Other background operations that need exclusive access\n//\n// Note: CREATED state is considered usable because new connections need to pass OnGet() hook\n// before initialization. The initialization happens after OnGet() in the client code.\nfunc (cn *Conn) IsUsable() bool {\n\tstate := cn.stateMachine.GetState()\n\t// CREATED, IDLE, and IN_USE states are considered usable\n\t// CREATED: new connection, not yet initialized (will be initialized by client)\n\t// IDLE: initialized and ready to be acquired\n\t// IN_USE: usable but currently acquired by someone\n\treturn state == StateCreated || state == StateIdle || state == StateInUse\n}\n\n// SetUsable sets the usable flag for the connection (lock-free).\n//\n// Deprecated: Use GetStateMachine().Transition() directly for better state management.\n// This method is kept for backwards compatibility.\n//\n// This should be called to mark a connection as usable after initialization or\n// to release it after a background operation completes.\n//\n// Prefer CompareAndSwapUsable() when acquiring exclusive access to avoid race conditions.\n// Deprecated: Use GetStateMachine().Transition() directly for better state management.\nfunc (cn *Conn) SetUsable(usable bool) {\n\tif usable {\n\t\t// Transition to IDLE state (ready to be acquired)\n\t\tcn.stateMachine.Transition(StateIdle)\n\t} else {\n\t\t// Transition to UNUSABLE state (for background operations)\n\t\tcn.stateMachine.Transition(StateUnusable)\n\t}\n}\n\n// IsInited returns true if the connection has been initialized.\n// This is a backward-compatible wrapper around the state machine.\nfunc (cn *Conn) IsInited() bool {\n\tstate := cn.stateMachine.GetState()\n\t// Connection is initialized if it's in IDLE or any post-initialization state\n\treturn state != StateCreated && state != StateInitializing && state != StateClosed\n}\n\n// Used - State machine based implementation\n\n// CompareAndSwapUsed atomically compares and swaps the used flag (lock-free).\n// This method is kept for backwards compatibility.\n//\n// This is the preferred method for acquiring a connection from the pool, as it\n// ensures that only one goroutine marks the connection as used.\n//\n// Implementation: Uses state machine transitions IDLE ⇄ IN_USE\n//\n// Returns true if the swap was successful (old value matched), false otherwise.\n// Deprecated: Use GetStateMachine().TryTransition() directly for better state management.\nfunc (cn *Conn) CompareAndSwapUsed(old, new bool) bool {\n\tif old == new {\n\t\t// No change needed\n\t\tcurrentState := cn.stateMachine.GetState()\n\t\tcurrentUsed := (currentState == StateInUse)\n\t\treturn currentUsed == old\n\t}\n\n\tif !old && new {\n\t\t// Acquiring: IDLE → IN_USE\n\t\t// Use predefined slice to avoid allocation\n\t\t_, err := cn.stateMachine.TryTransition(validFromCreatedOrIdle, StateInUse)\n\t\treturn err == nil\n\t} else {\n\t\t// Releasing: IN_USE → IDLE\n\t\t// Use predefined slice to avoid allocation\n\t\t_, err := cn.stateMachine.TryTransition(validFromInUse, StateIdle)\n\t\treturn err == nil\n\t}\n}\n\n// IsUsed returns true if the connection is currently in use (lock-free).\n//\n// Deprecated: Use GetStateMachine().GetState() == StateInUse directly for better clarity.\n// This method is kept for backwards compatibility.\n//\n// A connection is \"used\" when it has been retrieved from the pool and is\n// actively processing a command. Background operations (like re-auth) should\n// wait until the connection is not used before executing commands.\nfunc (cn *Conn) IsUsed() bool {\n\treturn cn.stateMachine.GetState() == StateInUse\n}\n\n// SetUsed sets the used flag for the connection (lock-free).\n//\n// This should be called when returning a connection to the pool (set to false)\n// or when a single-connection pool retrieves its connection (set to true).\n//\n// Prefer CompareAndSwapUsed() when acquiring from a multi-connection pool to\n// avoid race conditions.\n// Deprecated: Use GetStateMachine().Transition() directly for better state management.\nfunc (cn *Conn) SetUsed(val bool) {\n\tif val {\n\t\tcn.stateMachine.Transition(StateInUse)\n\t} else {\n\t\tcn.stateMachine.Transition(StateIdle)\n\t}\n}\n\n// getNetConn returns the current network connection using atomic load (lock-free).\n// This is the fast path for accessing netConn without mutex overhead.\nfunc (cn *Conn) getNetConn() net.Conn {\n\tif v := cn.netConnAtomic.Load(); v != nil {\n\t\tif wrapper, ok := v.(*atomicNetConn); ok {\n\t\t\treturn wrapper.conn\n\t\t}\n\t}\n\treturn nil\n}\n\n// setNetConn stores the network connection atomically (lock-free).\n// This is used for the fast path of connection replacement.\nfunc (cn *Conn) setNetConn(netConn net.Conn) {\n\tcn.netConnAtomic.Store(&atomicNetConn{conn: netConn})\n}\n\n// Handoff state management - atomic access to handoff metadata\n\n// ShouldHandoff returns true if connection needs handoff (lock-free).\nfunc (cn *Conn) ShouldHandoff() bool {\n\tif v := cn.handoffStateAtomic.Load(); v != nil {\n\t\treturn v.(*HandoffState).ShouldHandoff\n\t}\n\treturn false\n}\n\n// GetHandoffEndpoint returns the new endpoint for handoff (lock-free).\nfunc (cn *Conn) GetHandoffEndpoint() string {\n\tif v := cn.handoffStateAtomic.Load(); v != nil {\n\t\treturn v.(*HandoffState).Endpoint\n\t}\n\treturn \"\"\n}\n\n// GetMovingSeqID returns the sequence ID from the MOVING notification (lock-free).\nfunc (cn *Conn) GetMovingSeqID() int64 {\n\tif v := cn.handoffStateAtomic.Load(); v != nil {\n\t\treturn v.(*HandoffState).SeqID\n\t}\n\treturn 0\n}\n\n// GetHandoffInfo returns all handoff information atomically (lock-free).\n// This method prevents race conditions by returning all handoff state in a single atomic operation.\n// Returns (shouldHandoff, endpoint, seqID).\nfunc (cn *Conn) GetHandoffInfo() (bool, string, int64) {\n\tif v := cn.handoffStateAtomic.Load(); v != nil {\n\t\tstate := v.(*HandoffState)\n\t\treturn state.ShouldHandoff, state.Endpoint, state.SeqID\n\t}\n\treturn false, \"\", 0\n}\n\n// HandoffRetries returns the current handoff retry count (lock-free).\nfunc (cn *Conn) HandoffRetries() int {\n\treturn int(cn.handoffRetriesAtomic.Load())\n}\n\n// IncrementAndGetHandoffRetries atomically increments and returns handoff retries (lock-free).\nfunc (cn *Conn) IncrementAndGetHandoffRetries(n int) int {\n\treturn int(cn.handoffRetriesAtomic.Add(uint32(n)))\n}\n\n// IsPooled returns true if the connection is managed by a pool and will be pooled on Put.\nfunc (cn *Conn) IsPooled() bool {\n\treturn cn.pooled\n}\n\n// IsPubSub returns true if the connection is used for PubSub.\nfunc (cn *Conn) IsPubSub() bool {\n\treturn cn.pubsub\n}\n\n// SetRelaxedTimeout sets relaxed timeouts for this connection during maintenanceNotifications upgrades.\n// These timeouts will be used for all subsequent commands until the deadline expires.\n// Uses atomic operations for lock-free access.\n// Note: Metrics should be recorded by the caller (notification handler) which has context about\n// the notification type and pool name.\nfunc (cn *Conn) SetRelaxedTimeout(readTimeout, writeTimeout time.Duration) {\n\tcn.relaxedCounter.Add(1)\n\tcn.relaxedReadTimeoutNs.Store(int64(readTimeout))\n\tcn.relaxedWriteTimeoutNs.Store(int64(writeTimeout))\n}\n\n// SetRelaxedTimeoutWithDeadline sets relaxed timeouts with an expiration deadline.\n// After the deadline, timeouts automatically revert to normal values.\n// Uses atomic operations for lock-free access.\nfunc (cn *Conn) SetRelaxedTimeoutWithDeadline(readTimeout, writeTimeout time.Duration, deadline time.Time) {\n\tcn.SetRelaxedTimeout(readTimeout, writeTimeout)\n\tcn.relaxedDeadlineNs.Store(deadline.UnixNano())\n}\n\n// ClearRelaxedTimeout removes relaxed timeouts, returning to normal timeout behavior.\n// Uses atomic operations for lock-free access.\nfunc (cn *Conn) ClearRelaxedTimeout() {\n\t// Atomically decrement counter and check if we should clear\n\tnewCount := cn.relaxedCounter.Add(-1)\n\tdeadlineNs := cn.relaxedDeadlineNs.Load()\n\tif newCount <= 0 && (deadlineNs == 0 || time.Now().UnixNano() >= deadlineNs) {\n\t\t// Use atomic load to get current value for CAS to avoid stale value race\n\t\tcurrent := cn.relaxedCounter.Load()\n\t\tif current <= 0 && cn.relaxedCounter.CompareAndSwap(current, 0) {\n\t\t\tcn.clearRelaxedTimeout()\n\t\t}\n\t}\n}\n\nfunc (cn *Conn) clearRelaxedTimeout() {\n\tcn.relaxedReadTimeoutNs.Store(0)\n\tcn.relaxedWriteTimeoutNs.Store(0)\n\tcn.relaxedDeadlineNs.Store(0)\n\tcn.relaxedCounter.Store(0)\n\n\t// Note: Metrics for timeout unrelaxing are not recorded here because we don't have\n\t// context about which notification type or pool triggered the relaxation.\n\t// In practice, relaxed timeouts expire automatically via deadline, so explicit\n\t// unrelaxing metrics are less critical than the initial relaxation metrics.\n}\n\n// HasRelaxedTimeout returns true if relaxed timeouts are currently active on this connection.\n// This checks both the timeout values and the deadline (if set).\n// Uses atomic operations for lock-free access.\nfunc (cn *Conn) HasRelaxedTimeout() bool {\n\t// Fast path: no relaxed timeouts are set\n\tif cn.relaxedCounter.Load() <= 0 {\n\t\treturn false\n\t}\n\n\treadTimeoutNs := cn.relaxedReadTimeoutNs.Load()\n\twriteTimeoutNs := cn.relaxedWriteTimeoutNs.Load()\n\n\t// If no relaxed timeouts are set, return false\n\tif readTimeoutNs <= 0 && writeTimeoutNs <= 0 {\n\t\treturn false\n\t}\n\n\tdeadlineNs := cn.relaxedDeadlineNs.Load()\n\t// If no deadline is set, relaxed timeouts are active\n\tif deadlineNs == 0 {\n\t\treturn true\n\t}\n\n\t// If deadline is set, check if it's still in the future\n\treturn time.Now().UnixNano() < deadlineNs\n}\n\n// getEffectiveReadTimeout returns the timeout to use for read operations.\n// If relaxed timeout is set and not expired, it takes precedence over the provided timeout.\n// This method automatically clears expired relaxed timeouts using atomic operations.\nfunc (cn *Conn) getEffectiveReadTimeout(normalTimeout time.Duration) time.Duration {\n\treadTimeoutNs := cn.relaxedReadTimeoutNs.Load()\n\n\t// Fast path: no relaxed timeout set\n\tif readTimeoutNs <= 0 {\n\t\treturn normalTimeout\n\t}\n\n\tdeadlineNs := cn.relaxedDeadlineNs.Load()\n\t// If no deadline is set, use relaxed timeout\n\tif deadlineNs == 0 {\n\t\treturn time.Duration(readTimeoutNs)\n\t}\n\n\t// Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks)\n\tnowNs := getCachedTimeNs()\n\t// Check if deadline has passed\n\tif nowNs < deadlineNs {\n\t\t// Deadline is in the future, use relaxed timeout\n\t\treturn time.Duration(readTimeoutNs)\n\t} else {\n\t\t// Deadline has passed, clear relaxed timeouts atomically and use normal timeout\n\t\tnewCount := cn.relaxedCounter.Add(-1)\n\t\tif newCount <= 0 {\n\t\t\tinternal.Logger.Printf(context.Background(), logs.UnrelaxedTimeoutAfterDeadline(cn.GetID()))\n\t\t\tcn.clearRelaxedTimeout()\n\t\t}\n\t\treturn normalTimeout\n\t}\n}\n\n// getEffectiveWriteTimeout returns the timeout to use for write operations.\n// If relaxed timeout is set and not expired, it takes precedence over the provided timeout.\n// This method automatically clears expired relaxed timeouts using atomic operations.\nfunc (cn *Conn) getEffectiveWriteTimeout(normalTimeout time.Duration) time.Duration {\n\twriteTimeoutNs := cn.relaxedWriteTimeoutNs.Load()\n\n\t// Fast path: no relaxed timeout set\n\tif writeTimeoutNs <= 0 {\n\t\treturn normalTimeout\n\t}\n\n\tdeadlineNs := cn.relaxedDeadlineNs.Load()\n\t// If no deadline is set, use relaxed timeout\n\tif deadlineNs == 0 {\n\t\treturn time.Duration(writeTimeoutNs)\n\t}\n\n\t// Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks)\n\tnowNs := getCachedTimeNs()\n\t// Check if deadline has passed\n\tif nowNs < deadlineNs {\n\t\t// Deadline is in the future, use relaxed timeout\n\t\treturn time.Duration(writeTimeoutNs)\n\t} else {\n\t\t// Deadline has passed, clear relaxed timeouts atomically and use normal timeout\n\t\tnewCount := cn.relaxedCounter.Add(-1)\n\t\tif newCount <= 0 {\n\t\t\tinternal.Logger.Printf(context.Background(), logs.UnrelaxedTimeoutAfterDeadline(cn.GetID()))\n\t\t\tcn.clearRelaxedTimeout()\n\t\t}\n\t\treturn normalTimeout\n\t}\n}\n\nfunc (cn *Conn) SetOnClose(fn func() error) {\n\tcn.onClose = fn\n}\n\n// SetInitConnFunc sets the connection initialization function to be called on reconnections.\nfunc (cn *Conn) SetInitConnFunc(fn func(context.Context, *Conn) error) {\n\tcn.initConnFunc = fn\n}\n\n// ExecuteInitConn runs the stored connection initialization function if available.\nfunc (cn *Conn) ExecuteInitConn(ctx context.Context) error {\n\tif cn.initConnFunc != nil {\n\t\treturn cn.initConnFunc(ctx, cn)\n\t}\n\treturn fmt.Errorf(\"redis: no initConnFunc set for conn[%d]\", cn.GetID())\n}\n\nfunc (cn *Conn) SetNetConn(netConn net.Conn) {\n\t// Store the new connection atomically first (lock-free)\n\tcn.setNetConn(netConn)\n\t// Protect reader reset operations to avoid data races\n\t// Use write lock since we're modifying the reader state\n\tcn.readerMu.Lock()\n\tcn.rd.Reset(netConn)\n\tcn.readerMu.Unlock()\n\n\tcn.bw.Reset(netConn)\n}\n\n// GetNetConn safely returns the current network connection using atomic load (lock-free).\n// This method is used by the pool for health checks and provides better performance.\nfunc (cn *Conn) GetNetConn() net.Conn {\n\treturn cn.getNetConn()\n}\n\n// SetNetConnAndInitConn replaces the underlying connection and executes the initialization.\n// This method ensures only one initialization can happen at a time by using atomic state transitions.\n// If another goroutine is currently initializing, this will wait for it to complete.\nfunc (cn *Conn) SetNetConnAndInitConn(ctx context.Context, netConn net.Conn) error {\n\t// Wait for and transition to INITIALIZING state - this prevents concurrent initializations\n\t// Valid from states: CREATED (first init), IDLE (reconnect), UNUSABLE (handoff/reauth)\n\t// If another goroutine is initializing, we'll wait for it to finish\n\t// if the context has a deadline, use that, otherwise use the connection read (relaxed) timeout\n\t// which should be set during handoff. If it is not set, use a 5 second default\n\tdeadline, ok := ctx.Deadline()\n\tif !ok {\n\t\tdeadline = time.Now().Add(cn.getEffectiveReadTimeout(5 * time.Second))\n\t}\n\twaitCtx, cancel := context.WithDeadline(ctx, deadline)\n\tdefer cancel()\n\t// Use predefined slice to avoid allocation\n\tfinalState, err := cn.stateMachine.AwaitAndTransition(\n\t\twaitCtx,\n\t\tvalidFromCreatedIdleOrUnusable,\n\t\tStateInitializing,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot initialize connection from state %s: %w\", finalState, err)\n\t}\n\n\t// Replace the underlying connection\n\tcn.SetNetConn(netConn)\n\n\t// Execute initialization\n\t// NOTE: ExecuteInitConn (via baseClient.initConn) will transition to IDLE on success\n\t// or CLOSED on failure. We don't need to do it here.\n\t// NOTE: Initconn returns conn in IDLE state\n\tinitErr := cn.ExecuteInitConn(ctx)\n\tif initErr != nil {\n\t\t// ExecuteInitConn already transitioned to CLOSED, just return the error\n\t\treturn initErr\n\t}\n\n\t// ExecuteInitConn already transitioned to IDLE\n\treturn nil\n}\n\n// MarkForHandoff marks the connection for handoff due to MOVING notification.\n// Returns an error if the connection is already marked for handoff.\n// Note: This only sets metadata - the connection state is not changed until OnPut.\n// This allows the current user to finish using the connection before handoff.\nfunc (cn *Conn) MarkForHandoff(newEndpoint string, seqID int64) error {\n\t// Check if already marked for handoff\n\tif cn.ShouldHandoff() {\n\t\treturn errAlreadyMarkedForHandoff\n\t}\n\n\t// Set handoff metadata atomically\n\tcn.handoffStateAtomic.Store(&HandoffState{\n\t\tShouldHandoff: true,\n\t\tEndpoint:      newEndpoint,\n\t\tSeqID:         seqID,\n\t})\n\treturn nil\n}\n\n// MarkQueuedForHandoff marks the connection as queued for handoff processing.\n// This makes the connection unusable until handoff completes.\n// This is called from OnPut hook, where the connection is typically in IN_USE state.\n// The pool will preserve the UNUSABLE state and not overwrite it with IDLE.\nfunc (cn *Conn) MarkQueuedForHandoff() error {\n\t// Get current handoff state\n\tcurrentState := cn.handoffStateAtomic.Load()\n\tif currentState == nil {\n\t\treturn errNotMarkedForHandoff\n\t}\n\n\tstate := currentState.(*HandoffState)\n\tif !state.ShouldHandoff {\n\t\treturn errNotMarkedForHandoff\n\t}\n\n\t// Create new state with ShouldHandoff=false but preserve endpoint and seqID\n\t// This prevents the connection from being queued multiple times while still\n\t// allowing the worker to access the handoff metadata\n\tnewState := &HandoffState{\n\t\tShouldHandoff: false,\n\t\tEndpoint:      state.Endpoint, // Preserve endpoint for handoff processing\n\t\tSeqID:         state.SeqID,    // Preserve seqID for handoff processing\n\t}\n\n\t// Atomic compare-and-swap to update state\n\tif !cn.handoffStateAtomic.CompareAndSwap(currentState, newState) {\n\t\t// State changed between load and CAS - retry or return error\n\t\treturn errHandoffStateChanged\n\t}\n\n\t// Transition to UNUSABLE from IN_USE (normal flow), IDLE (edge cases), or CREATED (tests/uninitialized)\n\t// The connection is typically in IN_USE state when OnPut is called (normal Put flow)\n\t// But in some edge cases or tests, it might be in IDLE or CREATED state\n\t// The pool will detect this state change and preserve it (not overwrite with IDLE)\n\t// Use predefined slice to avoid allocation\n\tfinalState, err := cn.stateMachine.TryTransition(validFromCreatedInUseOrIdle, StateUnusable)\n\tif err != nil {\n\t\t// Check if already in UNUSABLE state (race condition or retry)\n\t\t// ShouldHandoff should be false now, but check just in case\n\t\tif finalState == StateUnusable && !cn.ShouldHandoff() {\n\t\t\t// Already unusable - this is fine, keep the new handoff state\n\t\t\treturn nil\n\t\t}\n\t\t// Restore the original state if transition fails for other reasons\n\t\tcn.handoffStateAtomic.Store(currentState)\n\t\treturn fmt.Errorf(\"failed to mark connection as unusable: %w\", err)\n\t}\n\treturn nil\n}\n\n// GetID returns the unique identifier for this connection.\nfunc (cn *Conn) GetID() uint64 {\n\treturn cn.id\n}\n\n// GetStateMachine returns the connection's state machine for advanced state management.\n// This is primarily used by internal packages like maintnotifications for handoff processing.\nfunc (cn *Conn) GetStateMachine() *ConnStateMachine {\n\treturn cn.stateMachine\n}\n\n// TryAcquire attempts to acquire the connection for use.\n// This is an optimized inline method for the hot path (Get operation).\n//\n// It tries to transition from IDLE -> IN_USE or CREATED -> CREATED.\n// Returns true if the connection was successfully acquired, false otherwise.\n// The CREATED->CREATED is done so we can keep the state correct for later\n// initialization of the connection in initConn.\n//\n// Performance: This is faster than calling GetStateMachine() + TryTransitionFast()\n//\n// NOTE: We directly access cn.stateMachine.state here instead of using the state machine's\n// methods. This breaks encapsulation but is necessary for performance.\n// The IDLE->IN_USE and CREATED->CREATED transitions don't need\n// waiter notification, and benchmarks show 1-3% improvement. If the state machine ever\n// needs to notify waiters on these transitions, update this to use TryTransitionFast().\nfunc (cn *Conn) TryAcquire() bool {\n\t// The || operator short-circuits, so only 1 CAS in the common case\n\treturn cn.stateMachine.state.CompareAndSwap(uint32(StateIdle), uint32(StateInUse)) ||\n\t\tcn.stateMachine.state.CompareAndSwap(uint32(StateCreated), uint32(StateCreated))\n}\n\n// Release releases the connection back to the pool.\n// This is an optimized inline method for the hot path (Put operation).\n//\n// It tries to transition from IN_USE -> IDLE.\n// Returns true if the connection was successfully released, false otherwise.\n//\n// Performance: This is faster than calling GetStateMachine() + TryTransitionFast().\n//\n// NOTE: We directly access cn.stateMachine.state here instead of using the state machine's\n// methods. This breaks encapsulation but is necessary for performance.\n// If the state machine ever needs to notify waiters\n// on this transition, update this to use TryTransitionFast().\nfunc (cn *Conn) Release() bool {\n\t// Inline the hot path - single CAS operation\n\treturn cn.stateMachine.state.CompareAndSwap(uint32(StateInUse), uint32(StateIdle))\n}\n\n// ClearHandoffState clears the handoff state after successful handoff.\n// Makes the connection usable again.\nfunc (cn *Conn) ClearHandoffState() {\n\t// Clear handoff metadata\n\tcn.handoffStateAtomic.Store(&HandoffState{\n\t\tShouldHandoff: false,\n\t\tEndpoint:      \"\",\n\t\tSeqID:         0,\n\t})\n\n\t// Reset retry counter\n\tcn.handoffRetriesAtomic.Store(0)\n\n\t// Mark connection as usable again\n\t// Use state machine directly instead of deprecated SetUsable\n\t// probably done by initConn\n\tcn.stateMachine.Transition(StateIdle)\n}\n\n// HasBufferedData safely checks if the connection has buffered data.\n// This method is used to avoid data races when checking for push notifications.\nfunc (cn *Conn) HasBufferedData() bool {\n\t// Use read lock for concurrent access to reader state\n\tcn.readerMu.RLock()\n\tdefer cn.readerMu.RUnlock()\n\treturn cn.rd.Buffered() > 0\n}\n\n// PeekReplyTypeSafe safely peeks at the reply type.\n// This method is used to avoid data races when checking for push notifications.\nfunc (cn *Conn) PeekReplyTypeSafe() (byte, error) {\n\t// Use read lock for concurrent access to reader state\n\tcn.readerMu.RLock()\n\tdefer cn.readerMu.RUnlock()\n\n\tif cn.rd.Buffered() <= 0 {\n\t\treturn 0, fmt.Errorf(\"redis: can't peek reply type, no data available\")\n\t}\n\treturn cn.rd.PeekReplyType()\n}\n\nfunc (cn *Conn) Write(b []byte) (int, error) {\n\t// Lock-free netConn access for better performance\n\tif netConn := cn.getNetConn(); netConn != nil {\n\t\treturn netConn.Write(b)\n\t}\n\treturn 0, net.ErrClosed\n}\n\nfunc (cn *Conn) RemoteAddr() net.Addr {\n\t// Lock-free netConn access for better performance\n\tif netConn := cn.getNetConn(); netConn != nil {\n\t\treturn netConn.RemoteAddr()\n\t}\n\treturn nil\n}\n\nfunc (cn *Conn) WithReader(\n\tctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error,\n) error {\n\tif timeout >= 0 {\n\t\t// Use relaxed timeout if set, otherwise use provided timeout\n\t\teffectiveTimeout := cn.getEffectiveReadTimeout(timeout)\n\n\t\t// Get the connection directly from atomic storage\n\t\tnetConn := cn.getNetConn()\n\t\tif netConn == nil {\n\t\t\treturn errConnectionNotAvailable\n\t\t}\n\n\t\tif err := netConn.SetReadDeadline(cn.deadline(ctx, effectiveTimeout)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn fn(cn.rd)\n}\n\nfunc (cn *Conn) WithWriter(\n\tctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error,\n) error {\n\tif timeout >= 0 {\n\t\t// Use relaxed timeout if set, otherwise use provided timeout\n\t\teffectiveTimeout := cn.getEffectiveWriteTimeout(timeout)\n\n\t\t// Set write deadline on the connection\n\t\tif netConn := cn.getNetConn(); netConn != nil {\n\t\t\tif err := netConn.SetWriteDeadline(cn.deadline(ctx, effectiveTimeout)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t// Connection is not available - return preallocated error\n\t\t\treturn errConnNotAvailableForWrite\n\t\t}\n\t}\n\n\t// Reset the buffered writer if needed, should not happen\n\tif cn.bw.Buffered() > 0 {\n\t\tif netConn := cn.getNetConn(); netConn != nil {\n\t\t\tcn.bw.Reset(netConn)\n\t\t}\n\t}\n\n\tif err := fn(cn.wr); err != nil {\n\t\treturn err\n\t}\n\n\treturn cn.bw.Flush()\n}\n\nfunc (cn *Conn) IsClosed() bool {\n\treturn cn.closed.Load() || cn.stateMachine.GetState() == StateClosed\n}\n\nfunc (cn *Conn) Close() error {\n\tcn.closed.Store(true)\n\n\t// Transition to CLOSED state\n\tcn.stateMachine.Transition(StateClosed)\n\n\tif cn.onClose != nil {\n\t\t// ignore error\n\t\t_ = cn.onClose()\n\t}\n\n\t// Lock-free netConn access for better performance\n\tif netConn := cn.getNetConn(); netConn != nil {\n\t\treturn netConn.Close()\n\t}\n\treturn nil\n}\n\n// MaybeHasData tries to peek at the next byte in the socket without consuming it\n// This is used to check if there are push notifications available\n// Important: This will work on Linux, but not on Windows\nfunc (cn *Conn) MaybeHasData() bool {\n\t// Lock-free netConn access for better performance\n\tif netConn := cn.getNetConn(); netConn != nil {\n\t\treturn maybeHasData(netConn)\n\t}\n\treturn false\n}\n\n// deadline computes the effective deadline time based on context and timeout.\n// It updates the usedAt timestamp to now.\n// Uses cached time to avoid expensive syscall (max 50ms staleness is acceptable for deadline calculation).\nfunc (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time {\n\t// Use cached time for deadline calculation (called 2x per command: read + write)\n\tnowNs := getCachedTimeNs()\n\tcn.SetUsedAtNs(nowNs)\n\ttm := time.Unix(0, nowNs)\n\n\tif timeout > 0 {\n\t\ttm = tm.Add(timeout)\n\t}\n\n\tif ctx != nil {\n\t\tdeadline, ok := ctx.Deadline()\n\t\tif ok {\n\t\t\tif timeout == 0 {\n\t\t\t\treturn deadline\n\t\t\t}\n\t\t\tif deadline.Before(tm) {\n\t\t\t\treturn deadline\n\t\t\t}\n\t\t\treturn tm\n\t\t}\n\t}\n\n\tif timeout > 0 {\n\t\treturn tm\n\t}\n\n\treturn noDeadline\n}\n"
  },
  {
    "path": "internal/pool/conn_check.go",
    "content": "//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos\n\npackage pool\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"syscall\"\n\t\"time\"\n)\n\nvar errUnexpectedRead = errors.New(\"unexpected read from socket\")\n\n// connCheck checks if the connection is still alive and if there is data in the socket\n// it will try to peek at the next byte without consuming it since we may want to work with it\n// later on (e.g. push notifications)\nfunc connCheck(conn net.Conn) error {\n\t// Reset previous timeout.\n\t_ = conn.SetDeadline(time.Time{})\n\n\tsysConn, ok := conn.(syscall.Conn)\n\tif !ok {\n\t\treturn nil\n\t}\n\trawConn, err := sysConn.SyscallConn()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar sysErr error\n\n\tif err := rawConn.Read(func(fd uintptr) bool {\n\t\tvar buf [1]byte\n\t\t// Use MSG_PEEK to peek at data without consuming it\n\t\tn, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK|syscall.MSG_DONTWAIT)\n\n\t\tswitch {\n\t\tcase n == 0 && err == nil:\n\t\t\tsysErr = io.EOF\n\t\tcase n > 0:\n\t\t\tsysErr = errUnexpectedRead\n\t\tcase err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:\n\t\t\tsysErr = nil\n\t\tdefault:\n\t\t\tsysErr = err\n\t\t}\n\t\treturn true\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\treturn sysErr\n}\n\n// maybeHasData checks if there is data in the socket without consuming it\nfunc maybeHasData(conn net.Conn) bool {\n\treturn connCheck(conn) == errUnexpectedRead\n}\n"
  },
  {
    "path": "internal/pool/conn_check_dummy.go",
    "content": "//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos\n\npackage pool\n\nimport (\n\t\"errors\"\n\t\"net\"\n)\n\n// errUnexpectedRead is placeholder error variable for non-unix build constraints\nvar errUnexpectedRead = errors.New(\"unexpected read from socket\")\n\nfunc connCheck(_ net.Conn) error {\n\treturn nil\n}\n\n// since we can't check for data on the socket, we just assume there is some\nfunc maybeHasData(_ net.Conn) bool {\n\treturn true\n}\n"
  },
  {
    "path": "internal/pool/conn_check_test.go",
    "content": "//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos\n\npackage pool\n\nimport (\n\t\"net\"\n\t\"net/http/httptest\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nvar _ = Describe(\"tests conn_check with real conns\", func() {\n\tvar ts *httptest.Server\n\tvar conn net.Conn\n\tvar err error\n\n\tBeforeEach(func() {\n\t\tts = httptest.NewServer(nil)\n\t\tconn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tts.Close()\n\t})\n\n\tIt(\"good conn check\", func() {\n\t\tExpect(connCheck(conn)).NotTo(HaveOccurred())\n\n\t\tExpect(conn.Close()).NotTo(HaveOccurred())\n\t\tExpect(connCheck(conn)).To(HaveOccurred())\n\t})\n\n\tIt(\"bad conn check\", func() {\n\t\tExpect(conn.Close()).NotTo(HaveOccurred())\n\t\tExpect(connCheck(conn)).To(HaveOccurred())\n\t})\n\n\tIt(\"check conn deadline\", func() {\n\t\tExpect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred())\n\t\ttime.Sleep(time.Millisecond * 10)\n\t\tExpect(connCheck(conn)).NotTo(HaveOccurred())\n\t\tExpect(conn.Close()).NotTo(HaveOccurred())\n\t})\n})\n"
  },
  {
    "path": "internal/pool/conn_relaxed_timeout_test.go",
    "content": "package pool\n\nimport (\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\n// TestConcurrentRelaxedTimeoutClearing tests the race condition fix in ClearRelaxedTimeout\nfunc TestConcurrentRelaxedTimeoutClearing(t *testing.T) {\n\t// Create a dummy connection for testing\n\tnetConn := &net.TCPConn{}\n\tcn := NewConn(netConn)\n\tdefer cn.Close()\n\n\t// Set relaxed timeout multiple times to increase counter\n\tcn.SetRelaxedTimeout(time.Second, time.Second)\n\tcn.SetRelaxedTimeout(time.Second, time.Second)\n\tcn.SetRelaxedTimeout(time.Second, time.Second)\n\n\t// Verify counter is 3\n\tif count := cn.relaxedCounter.Load(); count != 3 {\n\t\tt.Errorf(\"Expected relaxed counter to be 3, got %d\", count)\n\t}\n\n\t// Clear timeouts concurrently to test race condition fix\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < 10; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tcn.ClearRelaxedTimeout()\n\t\t}()\n\t}\n\twg.Wait()\n\n\t// Verify counter is 0 and timeouts are cleared\n\tif count := cn.relaxedCounter.Load(); count != 0 {\n\t\tt.Errorf(\"Expected relaxed counter to be 0 after clearing, got %d\", count)\n\t}\n\tif timeout := cn.relaxedReadTimeoutNs.Load(); timeout != 0 {\n\t\tt.Errorf(\"Expected relaxed read timeout to be 0, got %d\", timeout)\n\t}\n\tif timeout := cn.relaxedWriteTimeoutNs.Load(); timeout != 0 {\n\t\tt.Errorf(\"Expected relaxed write timeout to be 0, got %d\", timeout)\n\t}\n}\n\n// TestRelaxedTimeoutCounterRaceCondition tests the specific race condition scenario\nfunc TestRelaxedTimeoutCounterRaceCondition(t *testing.T) {\n\tnetConn := &net.TCPConn{}\n\tcn := NewConn(netConn)\n\tdefer cn.Close()\n\n\t// Set relaxed timeout once\n\tcn.SetRelaxedTimeout(time.Second, time.Second)\n\n\t// Verify counter is 1\n\tif count := cn.relaxedCounter.Load(); count != 1 {\n\t\tt.Errorf(\"Expected relaxed counter to be 1, got %d\", count)\n\t}\n\n\t// Test concurrent clearing with race condition scenario\n\tvar wg sync.WaitGroup\n\n\t// Multiple goroutines try to clear simultaneously\n\tfor i := 0; i < 5; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tcn.ClearRelaxedTimeout()\n\t\t}()\n\t}\n\twg.Wait()\n\n\t// Verify final state is consistent\n\tif count := cn.relaxedCounter.Load(); count != 0 {\n\t\tt.Errorf(\"Expected relaxed counter to be 0 after concurrent clearing, got %d\", count)\n\t}\n\n\t// Verify timeouts are actually cleared\n\tif timeout := cn.relaxedReadTimeoutNs.Load(); timeout != 0 {\n\t\tt.Errorf(\"Expected relaxed read timeout to be cleared, got %d\", timeout)\n\t}\n\tif timeout := cn.relaxedWriteTimeoutNs.Load(); timeout != 0 {\n\t\tt.Errorf(\"Expected relaxed write timeout to be cleared, got %d\", timeout)\n\t}\n\tif deadline := cn.relaxedDeadlineNs.Load(); deadline != 0 {\n\t\tt.Errorf(\"Expected relaxed deadline to be cleared, got %d\", deadline)\n\t}\n}\n"
  },
  {
    "path": "internal/pool/conn_state.go",
    "content": "package pool\n\nimport (\n\t\"container/list\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\n// ConnState represents the connection state in the state machine.\n// States are designed to be lightweight and fast to check.\n//\n// State Transitions:\n//\n//\tCREATED → INITIALIZING → IDLE ⇄ IN_USE\n//\t                           ↓\n//\t                       UNUSABLE (handoff/reauth)\n//\t                           ↓\n//\t                        IDLE/CLOSED\ntype ConnState uint32\n\nconst (\n\t// StateCreated - Connection just created, not yet initialized\n\tStateCreated ConnState = iota\n\n\t// StateInitializing - Connection initialization in progress\n\tStateInitializing\n\n\t// StateIdle - Connection initialized and idle in pool, ready to be acquired\n\tStateIdle\n\n\t// StateInUse - Connection actively processing a command (retrieved from pool)\n\tStateInUse\n\n\t// StateUnusable - Connection temporarily unusable due to background operation\n\t// (handoff, reauth, etc.). Cannot be acquired from pool.\n\tStateUnusable\n\n\t// StateClosed - Connection closed\n\tStateClosed\n)\n\n// Predefined state slices to avoid allocations in hot paths\nvar (\n\tvalidFromInUse              = []ConnState{StateInUse}\n\tvalidFromCreatedOrIdle      = []ConnState{StateCreated, StateIdle}\n\tvalidFromCreatedInUseOrIdle = []ConnState{StateCreated, StateInUse, StateIdle}\n\t// For AwaitAndTransition calls\n\tvalidFromCreatedIdleOrUnusable = []ConnState{StateCreated, StateIdle, StateUnusable}\n\tvalidFromIdle                  = []ConnState{StateIdle}\n\t// For CompareAndSwapUsable\n\tvalidFromInitializingOrUnusable = []ConnState{StateInitializing, StateUnusable}\n)\n\n// Accessor functions for predefined slices to avoid allocations in external packages\n// These return the same slice instance, so they're zero-allocation\n\n// ValidFromIdle returns a predefined slice containing only StateIdle.\n// Use this to avoid allocations when calling AwaitAndTransition or TryTransition.\nfunc ValidFromIdle() []ConnState {\n\treturn validFromIdle\n}\n\n// ValidFromCreatedIdleOrUnusable returns a predefined slice for initialization transitions.\n// Use this to avoid allocations when calling AwaitAndTransition or TryTransition.\nfunc ValidFromCreatedIdleOrUnusable() []ConnState {\n\treturn validFromCreatedIdleOrUnusable\n}\n\n// String returns a human-readable string representation of the state.\nfunc (s ConnState) String() string {\n\tswitch s {\n\tcase StateCreated:\n\t\treturn \"CREATED\"\n\tcase StateInitializing:\n\t\treturn \"INITIALIZING\"\n\tcase StateIdle:\n\t\treturn \"IDLE\"\n\tcase StateInUse:\n\t\treturn \"IN_USE\"\n\tcase StateUnusable:\n\t\treturn \"UNUSABLE\"\n\tcase StateClosed:\n\t\treturn \"CLOSED\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"UNKNOWN(%d)\", s)\n\t}\n}\n\nvar (\n\t// ErrInvalidStateTransition is returned when a state transition is not allowed\n\tErrInvalidStateTransition = errors.New(\"invalid state transition\")\n\n\t// ErrStateMachineClosed is returned when operating on a closed state machine\n\tErrStateMachineClosed = errors.New(\"state machine is closed\")\n\n\t// ErrTimeout is returned when a state transition times out\n\tErrTimeout = errors.New(\"state transition timeout\")\n)\n\n// waiter represents a goroutine waiting for a state transition.\n// Designed for minimal allocations and fast processing.\ntype waiter struct {\n\tvalidStates map[ConnState]struct{} // States we're waiting for\n\ttargetState ConnState              // State to transition to\n\tdone        chan error             // Signaled when transition completes or times out\n}\n\n// ConnStateMachine manages connection state transitions with FIFO waiting queue.\n// Optimized for:\n// - Lock-free reads (hot path)\n// - Minimal allocations\n// - Fast state transitions\n// - FIFO fairness for waiters\n// Note: Handoff metadata (endpoint, seqID, retries) is managed separately in the Conn struct.\ntype ConnStateMachine struct {\n\t// Current state - atomic for lock-free reads\n\tstate atomic.Uint32\n\n\t// FIFO queue for waiters - only locked during waiter add/remove/notify\n\tmu          sync.Mutex\n\twaiters     *list.List   // List of *waiter\n\twaiterCount atomic.Int32 // Fast lock-free check for waiters (avoids mutex in hot path)\n}\n\n// NewConnStateMachine creates a new connection state machine.\n// Initial state is StateCreated.\nfunc NewConnStateMachine() *ConnStateMachine {\n\tsm := &ConnStateMachine{\n\t\twaiters: list.New(),\n\t}\n\tsm.state.Store(uint32(StateCreated))\n\treturn sm\n}\n\n// GetState returns the current state (lock-free read).\n// This is the hot path - optimized for zero allocations and minimal overhead.\n// Note: Zero allocations applies to state reads; converting the returned state to a string\n// (via String()) may allocate if the state is unknown.\nfunc (sm *ConnStateMachine) GetState() ConnState {\n\treturn ConnState(sm.state.Load())\n}\n\n// TryTransitionFast is an optimized version for the hot path (Get/Put operations).\n// It only handles simple state transitions without waiter notification.\n// This is safe because:\n// 1. Get/Put don't need to wait for state changes\n// 2. Background operations (handoff/reauth) use UNUSABLE state, which this won't match\n// 3. If a background operation is in progress (state is UNUSABLE), this fails fast\n//\n// Returns true if transition succeeded, false otherwise.\n// Use this for performance-critical paths where you don't need error details.\n//\n// Performance: Single CAS operation - as fast as the old atomic bool!\n// For multiple from states, use: sm.TryTransitionFast(State1, Target) || sm.TryTransitionFast(State2, Target)\n// The || operator short-circuits, so only 1 CAS is executed in the common case.\nfunc (sm *ConnStateMachine) TryTransitionFast(fromState, targetState ConnState) bool {\n\treturn sm.state.CompareAndSwap(uint32(fromState), uint32(targetState))\n}\n\n// TryTransition attempts an immediate state transition without waiting.\n// Returns the current state after the transition attempt and an error if the transition failed.\n// The returned state is the CURRENT state (after the attempt), not the previous state.\n// This is faster than AwaitAndTransition when you don't need to wait.\n// Uses compare-and-swap to atomically transition, preventing concurrent transitions.\n// This method does NOT wait - it fails immediately if the transition cannot be performed.\n//\n// Performance: Zero allocations on success path (hot path).\nfunc (sm *ConnStateMachine) TryTransition(validFromStates []ConnState, targetState ConnState) (ConnState, error) {\n\t// Try each valid from state with CAS\n\t// This ensures only ONE goroutine can successfully transition at a time\n\tfor _, fromState := range validFromStates {\n\t\t// Try to atomically swap from fromState to targetState\n\t\t// If successful, we won the race and can proceed\n\t\tif sm.state.CompareAndSwap(uint32(fromState), uint32(targetState)) {\n\t\t\t// Success! We transitioned atomically\n\t\t\t// Hot path optimization: only check for waiters if transition succeeded\n\t\t\t// This avoids atomic load on every Get/Put when no waiters exist\n\t\t\tif sm.waiterCount.Load() > 0 {\n\t\t\t\tsm.notifyWaiters()\n\t\t\t}\n\t\t\treturn targetState, nil\n\t\t}\n\t}\n\n\t// All CAS attempts failed - state is not valid for this transition\n\t// Return the current state so caller can decide what to do\n\t// Note: This error path allocates, but it's the exceptional case\n\tcurrentState := sm.GetState()\n\treturn currentState, fmt.Errorf(\"%w: cannot transition from %s to %s (valid from: %v)\",\n\t\tErrInvalidStateTransition, currentState, targetState, validFromStates)\n}\n\n// Transition unconditionally transitions to the target state.\n// Use with caution - prefer AwaitAndTransition or TryTransition for safety.\n// This is useful for error paths or when you know the transition is valid.\nfunc (sm *ConnStateMachine) Transition(targetState ConnState) {\n\tsm.state.Store(uint32(targetState))\n\tsm.notifyWaiters()\n}\n\n// AwaitAndTransition waits for the connection to reach one of the valid states,\n// then atomically transitions to the target state.\n// Returns the current state after the transition attempt and an error if the operation failed.\n// The returned state is the CURRENT state (after the attempt), not the previous state.\n// Returns error if timeout expires or context is cancelled.\n//\n// This method implements FIFO fairness - the first caller to wait gets priority\n// when the state becomes available.\n//\n// Performance notes:\n// - If already in a valid state, this is very fast (no allocation, no waiting)\n// - If waiting is required, allocates one waiter struct and one channel\nfunc (sm *ConnStateMachine) AwaitAndTransition(\n\tctx context.Context,\n\tvalidFromStates []ConnState,\n\ttargetState ConnState,\n) (ConnState, error) {\n\t// Fast path: try immediate transition with CAS to prevent race conditions\n\t// BUT: only if there are no waiters in the queue (to maintain FIFO ordering)\n\tif sm.waiterCount.Load() == 0 {\n\t\tfor _, fromState := range validFromStates {\n\t\t\t// Check if we're already in target state\n\t\t\tif fromState == targetState && sm.GetState() == targetState {\n\t\t\t\treturn targetState, nil\n\t\t\t}\n\n\t\t\t// Try to atomically swap from fromState to targetState\n\t\t\tif sm.state.CompareAndSwap(uint32(fromState), uint32(targetState)) {\n\t\t\t\t// Success! We transitioned atomically\n\t\t\t\tsm.notifyWaiters()\n\t\t\t\treturn targetState, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fast path failed - check if we should wait or fail\n\tcurrentState := sm.GetState()\n\n\t// Check if closed\n\tif currentState == StateClosed {\n\t\treturn currentState, ErrStateMachineClosed\n\t}\n\n\t// Slow path: need to wait for state change\n\t// Create waiter with valid states map for fast lookup\n\tvalidStatesMap := make(map[ConnState]struct{}, len(validFromStates))\n\tfor _, s := range validFromStates {\n\t\tvalidStatesMap[s] = struct{}{}\n\t}\n\n\tw := &waiter{\n\t\tvalidStates: validStatesMap,\n\t\ttargetState: targetState,\n\t\tdone:        make(chan error, 1), // Buffered to avoid goroutine leak\n\t}\n\n\t// Add to FIFO queue\n\tsm.mu.Lock()\n\telem := sm.waiters.PushBack(w)\n\tsm.waiterCount.Add(1)\n\tsm.mu.Unlock()\n\n\t// Wait for state change or timeout\n\tselect {\n\tcase <-ctx.Done():\n\t\t// Timeout or cancellation - remove from queue\n\t\tsm.mu.Lock()\n\t\tsm.waiters.Remove(elem)\n\t\tsm.waiterCount.Add(-1)\n\t\tsm.mu.Unlock()\n\t\treturn sm.GetState(), ctx.Err()\n\tcase err := <-w.done:\n\t\t// Transition completed (or failed)\n\t\t// Note: waiterCount is decremented either in notifyWaiters (when the waiter is notified and removed)\n\t\t// or here (on timeout/cancellation).\n\t\treturn sm.GetState(), err\n\t}\n}\n\n// notifyWaiters checks if any waiters can proceed and notifies them in FIFO order.\n// This is called after every state transition.\nfunc (sm *ConnStateMachine) notifyWaiters() {\n\t// Fast path: check atomic counter without acquiring lock\n\t// This eliminates mutex overhead in the common case (no waiters)\n\tif sm.waiterCount.Load() == 0 {\n\t\treturn\n\t}\n\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\n\t// Double-check after acquiring lock (waiters might have been processed)\n\tif sm.waiters.Len() == 0 {\n\t\treturn\n\t}\n\n\t// Process waiters in FIFO order until no more can be processed\n\t// We loop instead of recursing to avoid stack overflow and mutex issues\n\tfor {\n\t\tprocessed := false\n\n\t\t// Find the first waiter that can proceed\n\t\tfor elem := sm.waiters.Front(); elem != nil; elem = elem.Next() {\n\t\t\tw := elem.Value.(*waiter)\n\n\t\t\t// Read current state inside the loop to get the latest value\n\t\t\tcurrentState := sm.GetState()\n\n\t\t\t// Check if current state is valid for this waiter\n\t\t\tif _, valid := w.validStates[currentState]; valid {\n\t\t\t\t// Remove from queue first\n\t\t\t\tsm.waiters.Remove(elem)\n\t\t\t\tsm.waiterCount.Add(-1)\n\n\t\t\t\t// Use CAS to ensure state hasn't changed since we checked\n\t\t\t\t// This prevents race condition where another thread changes state\n\t\t\t\t// between our check and our transition\n\t\t\t\tif sm.state.CompareAndSwap(uint32(currentState), uint32(w.targetState)) {\n\t\t\t\t\t// Successfully transitioned - notify waiter\n\t\t\t\t\tw.done <- nil\n\t\t\t\t\tprocessed = true\n\t\t\t\t\tbreak\n\t\t\t\t} else {\n\t\t\t\t\t// State changed - re-add waiter to front of queue to maintain FIFO ordering\n\t\t\t\t\t// This waiter was first in line and should retain priority\n\t\t\t\t\tsm.waiters.PushFront(w)\n\t\t\t\t\tsm.waiterCount.Add(1)\n\t\t\t\t\t// Continue to next iteration to re-read state\n\t\t\t\t\tprocessed = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// If we didn't process any waiter, we're done\n\t\tif !processed {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/pool/conn_state_alloc_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"testing\"\n)\n\n// TestPredefinedSlicesAvoidAllocations verifies that using predefined slices\n// avoids allocations in AwaitAndTransition calls\nfunc TestPredefinedSlicesAvoidAllocations(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\tctx := context.Background()\n\n\t// Test with predefined slice - should have 0 allocations on fast path\n\tallocs := testing.AllocsPerRun(100, func() {\n\t\t_, _ = sm.AwaitAndTransition(ctx, validFromIdle, StateUnusable)\n\t\tsm.Transition(StateIdle)\n\t})\n\n\tif allocs > 0 {\n\t\tt.Errorf(\"Expected 0 allocations with predefined slice, got %.2f\", allocs)\n\t}\n}\n\n// TestInlineSliceAllocations shows that inline slices cause allocations\nfunc TestInlineSliceAllocations(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\tctx := context.Background()\n\n\t// Test with inline slice - will allocate\n\tallocs := testing.AllocsPerRun(100, func() {\n\t\t_, _ = sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateUnusable)\n\t\tsm.Transition(StateIdle)\n\t})\n\n\tif allocs == 0 {\n\t\tt.Logf(\"Inline slice had 0 allocations (compiler optimization)\")\n\t} else {\n\t\tt.Logf(\"Inline slice caused %.2f allocations per run (expected)\", allocs)\n\t}\n}\n\n// BenchmarkAwaitAndTransition_PredefinedSlice benchmarks with predefined slice\nfunc BenchmarkAwaitAndTransition_PredefinedSlice(b *testing.B) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\tctx := context.Background()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = sm.AwaitAndTransition(ctx, validFromIdle, StateUnusable)\n\t\tsm.Transition(StateIdle)\n\t}\n}\n\n// BenchmarkAwaitAndTransition_InlineSlice benchmarks with inline slice\nfunc BenchmarkAwaitAndTransition_InlineSlice(b *testing.B) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\tctx := context.Background()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateUnusable)\n\t\tsm.Transition(StateIdle)\n\t}\n}\n\n// BenchmarkAwaitAndTransition_MultipleStates_Predefined benchmarks with predefined multi-state slice\nfunc BenchmarkAwaitAndTransition_MultipleStates_Predefined(b *testing.B) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\tctx := context.Background()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = sm.AwaitAndTransition(ctx, validFromCreatedIdleOrUnusable, StateInitializing)\n\t\tsm.Transition(StateIdle)\n\t}\n}\n\n// BenchmarkAwaitAndTransition_MultipleStates_Inline benchmarks with inline multi-state slice\nfunc BenchmarkAwaitAndTransition_MultipleStates_Inline(b *testing.B) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\tctx := context.Background()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = sm.AwaitAndTransition(ctx, []ConnState{StateCreated, StateIdle, StateUnusable}, StateInitializing)\n\t\tsm.Transition(StateIdle)\n\t}\n}\n\n// TestPreallocatedErrorsAvoidAllocations verifies that preallocated errors\n// avoid allocations in hot paths\nfunc TestPreallocatedErrorsAvoidAllocations(t *testing.T) {\n\tcn := NewConn(nil)\n\n\t// Test MarkForHandoff - first call should succeed\n\terr := cn.MarkForHandoff(\"localhost:6379\", 123)\n\tif err != nil {\n\t\tt.Fatalf(\"First MarkForHandoff should succeed: %v\", err)\n\t}\n\n\t// Second call should return preallocated error with 0 allocations\n\tallocs := testing.AllocsPerRun(100, func() {\n\t\t_ = cn.MarkForHandoff(\"localhost:6380\", 124)\n\t})\n\n\tif allocs > 0 {\n\t\tt.Errorf(\"Expected 0 allocations for preallocated error, got %.2f\", allocs)\n\t}\n}\n\n// BenchmarkHandoffErrors_Preallocated benchmarks handoff errors with preallocated errors\nfunc BenchmarkHandoffErrors_Preallocated(b *testing.B) {\n\tcn := NewConn(nil)\n\tcn.MarkForHandoff(\"localhost:6379\", 123)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = cn.MarkForHandoff(\"localhost:6380\", 124)\n\t}\n}\n\n// BenchmarkCompareAndSwapUsable_Preallocated benchmarks with preallocated slices\nfunc BenchmarkCompareAndSwapUsable_Preallocated(b *testing.B) {\n\tcn := NewConn(nil)\n\tcn.stateMachine.Transition(StateIdle)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tcn.CompareAndSwapUsable(true, false) // IDLE -> UNUSABLE\n\t\tcn.CompareAndSwapUsable(false, true) // UNUSABLE -> IDLE\n\t}\n}\n\n// TestAllTryTransitionUsePredefinedSlices verifies all TryTransition calls use predefined slices\nfunc TestAllTryTransitionUsePredefinedSlices(t *testing.T) {\n\tcn := NewConn(nil)\n\tcn.stateMachine.Transition(StateIdle)\n\n\t// Test CompareAndSwapUsable - should have minimal allocations\n\tallocs := testing.AllocsPerRun(100, func() {\n\t\tcn.CompareAndSwapUsable(true, false) // IDLE -> UNUSABLE\n\t\tcn.CompareAndSwapUsable(false, true) // UNUSABLE -> IDLE\n\t})\n\n\t// Allow some allocations for error objects, but should be minimal\n\tif allocs > 2 {\n\t\tt.Errorf(\"Expected <= 2 allocations with predefined slices, got %.2f\", allocs)\n\t}\n}\n"
  },
  {
    "path": "internal/pool/conn_state_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConnStateMachine_GetState(t *testing.T) {\n\tsm := NewConnStateMachine()\n\n\tif state := sm.GetState(); state != StateCreated {\n\t\tt.Errorf(\"expected initial state to be CREATED, got %s\", state)\n\t}\n}\n\nfunc TestConnStateMachine_Transition(t *testing.T) {\n\tsm := NewConnStateMachine()\n\n\t// Unconditional transition\n\tsm.Transition(StateInitializing)\n\tif state := sm.GetState(); state != StateInitializing {\n\t\tt.Errorf(\"expected state to be INITIALIZING, got %s\", state)\n\t}\n\n\tsm.Transition(StateIdle)\n\tif state := sm.GetState(); state != StateIdle {\n\t\tt.Errorf(\"expected state to be IDLE, got %s\", state)\n\t}\n}\n\nfunc TestConnStateMachine_TryTransition(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tinitialState ConnState\n\t\tvalidStates  []ConnState\n\t\ttargetState  ConnState\n\t\texpectError  bool\n\t}{\n\t\t{\n\t\t\tname:         \"valid transition from CREATED to INITIALIZING\",\n\t\t\tinitialState: StateCreated,\n\t\t\tvalidStates:  []ConnState{StateCreated},\n\t\t\ttargetState:  StateInitializing,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid transition from CREATED to IDLE\",\n\t\t\tinitialState: StateCreated,\n\t\t\tvalidStates:  []ConnState{StateInitializing},\n\t\t\ttargetState:  StateIdle,\n\t\t\texpectError:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"transition to same state\",\n\t\t\tinitialState: StateIdle,\n\t\t\tvalidStates:  []ConnState{StateIdle},\n\t\t\ttargetState:  StateIdle,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple valid from states\",\n\t\t\tinitialState: StateIdle,\n\t\t\tvalidStates:  []ConnState{StateInitializing, StateIdle, StateUnusable},\n\t\t\ttargetState:  StateUnusable,\n\t\t\texpectError:  false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsm := NewConnStateMachine()\n\t\t\tsm.Transition(tt.initialState)\n\n\t\t\t_, err := sm.TryTransition(tt.validStates, tt.targetState)\n\n\t\t\tif tt.expectError && err == nil {\n\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tif !tt.expectError {\n\t\t\t\tif state := sm.GetState(); state != tt.targetState {\n\t\t\t\t\tt.Errorf(\"expected state %s, got %s\", tt.targetState, state)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConnStateMachine_AwaitAndTransition_FastPath(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\n\tctx := context.Background()\n\n\t// Fast path: already in valid state\n\t_, err := sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateUnusable)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %v\", err)\n\t}\n\n\tif state := sm.GetState(); state != StateUnusable {\n\t\tt.Errorf(\"expected state UNUSABLE, got %s\", state)\n\t}\n}\n\nfunc TestConnStateMachine_AwaitAndTransition_Timeout(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateCreated)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)\n\tdefer cancel()\n\n\t// Wait for a state that will never come\n\t_, err := sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateUnusable)\n\tif err == nil {\n\t\tt.Error(\"expected timeout error but got none\")\n\t}\n\tif err != context.DeadlineExceeded {\n\t\tt.Errorf(\"expected DeadlineExceeded, got %v\", err)\n\t}\n}\n\nfunc TestConnStateMachine_AwaitAndTransition_FIFO(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateCreated)\n\n\tconst numWaiters = 10\n\torder := make([]int, 0, numWaiters)\n\tvar orderMu sync.Mutex\n\tvar wg sync.WaitGroup\n\tvar startBarrier sync.WaitGroup\n\tstartBarrier.Add(numWaiters)\n\n\t// Start multiple waiters\n\tfor i := 0; i < numWaiters; i++ {\n\t\twg.Add(1)\n\t\twaiterID := i\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\t// Signal that this goroutine is ready\n\t\t\tstartBarrier.Done()\n\t\t\t// Wait for all goroutines to be ready before starting\n\t\t\tstartBarrier.Wait()\n\n\t\t\tctx := context.Background()\n\t\t\t_, err := sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateIdle)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"waiter %d got error: %v\", waiterID, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\torderMu.Lock()\n\t\t\torder = append(order, waiterID)\n\t\t\torderMu.Unlock()\n\n\t\t\t// Transition back to READY for next waiter\n\t\t\tsm.Transition(StateIdle)\n\t\t}()\n\t}\n\n\t// Give waiters time to queue up\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Transition to READY to start processing waiters\n\tsm.Transition(StateIdle)\n\n\t// Wait for all waiters to complete\n\twg.Wait()\n\n\t// Verify all waiters completed (FIFO order is not guaranteed due to goroutine scheduling)\n\tif len(order) != numWaiters {\n\t\tt.Errorf(\"expected %d waiters to complete, got %d\", numWaiters, len(order))\n\t}\n\n\t// Verify no duplicates\n\tseen := make(map[int]bool)\n\tfor _, id := range order {\n\t\tif seen[id] {\n\t\t\tt.Errorf(\"duplicate waiter ID %d in order\", id)\n\t\t}\n\t\tseen[id] = true\n\t}\n}\n\nfunc TestConnStateMachine_ConcurrentAccess(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\n\tconst numGoroutines = 100\n\tconst numIterations = 100\n\n\tvar wg sync.WaitGroup\n\tvar successCount atomic.Int32\n\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor j := 0; j < numIterations; j++ {\n\t\t\t\t// Try to transition from READY to REAUTH_IN_PROGRESS\n\t\t\t\t_, err := sm.TryTransition([]ConnState{StateIdle}, StateUnusable)\n\t\t\t\tif err == nil {\n\t\t\t\t\tsuccessCount.Add(1)\n\t\t\t\t\t// Transition back to READY\n\t\t\t\t\tsm.Transition(StateIdle)\n\t\t\t\t}\n\n\t\t\t\t// Read state (hot path)\n\t\t\t\t_ = sm.GetState()\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\t// At least some transitions should have succeeded\n\tif successCount.Load() == 0 {\n\t\tt.Error(\"expected at least some successful transitions\")\n\t}\n\n\tt.Logf(\"Successful transitions: %d out of %d attempts\", successCount.Load(), numGoroutines*numIterations)\n}\n\nfunc TestConnStateMachine_StateString(t *testing.T) {\n\ttests := []struct {\n\t\tstate    ConnState\n\t\texpected string\n\t}{\n\t\t{StateCreated, \"CREATED\"},\n\t\t{StateInitializing, \"INITIALIZING\"},\n\t\t{StateIdle, \"IDLE\"},\n\t\t{StateInUse, \"IN_USE\"},\n\t\t{StateUnusable, \"UNUSABLE\"},\n\t\t{StateClosed, \"CLOSED\"},\n\t\t{ConnState(999), \"UNKNOWN(999)\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tif got := tt.state.String(); got != tt.expected {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tt.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkConnStateMachine_GetState(b *testing.B) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = sm.GetState()\n\t}\n}\n\nfunc TestConnStateMachine_PreventsConcurrentInitialization(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\n\tconst numGoroutines = 10\n\tvar inInitializing atomic.Int32\n\tvar maxConcurrent atomic.Int32\n\tvar successCount atomic.Int32\n\tvar wg sync.WaitGroup\n\tvar startBarrier sync.WaitGroup\n\tstartBarrier.Add(numGoroutines)\n\n\t// Try to initialize concurrently from multiple goroutines\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// Wait for all goroutines to be ready\n\t\t\tstartBarrier.Done()\n\t\t\tstartBarrier.Wait()\n\n\t\t\t// Try to transition to INITIALIZING\n\t\t\t_, err := sm.TryTransition([]ConnState{StateIdle}, StateInitializing)\n\t\t\tif err == nil {\n\t\t\t\tsuccessCount.Add(1)\n\n\t\t\t\t// We successfully transitioned - increment concurrent count\n\t\t\t\tcurrent := inInitializing.Add(1)\n\n\t\t\t\t// Track maximum concurrent initializations\n\t\t\t\tfor {\n\t\t\t\t\tmax := maxConcurrent.Load()\n\t\t\t\t\tif current <= max || maxConcurrent.CompareAndSwap(max, current) {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tt.Logf(\"Goroutine %d: entered INITIALIZING (concurrent=%d)\", id, current)\n\n\t\t\t\t// Simulate initialization work\n\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t\t\t// Decrement before transitioning back\n\t\t\t\tinInitializing.Add(-1)\n\n\t\t\t\t// Transition back to READY\n\t\t\t\tsm.Transition(StateIdle)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"Goroutine %d: failed to enter INITIALIZING - %v\", id, err)\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\tt.Logf(\"Total successful transitions: %d, Max concurrent: %d\", successCount.Load(), maxConcurrent.Load())\n\n\t// The maximum number of concurrent initializations should be 1\n\tif maxConcurrent.Load() != 1 {\n\t\tt.Errorf(\"expected max 1 concurrent initialization, got %d\", maxConcurrent.Load())\n\t}\n}\n\nfunc TestConnStateMachine_AwaitAndTransitionWaitsForInitialization(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\n\tconst numGoroutines = 5\n\tvar completedCount atomic.Int32\n\tvar executionOrder []int\n\tvar orderMu sync.Mutex\n\tvar wg sync.WaitGroup\n\tvar startBarrier sync.WaitGroup\n\tstartBarrier.Add(numGoroutines)\n\n\t// All goroutines try to initialize concurrently\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// Wait for all goroutines to be ready\n\t\t\tstartBarrier.Done()\n\t\t\tstartBarrier.Wait()\n\n\t\t\tctx := context.Background()\n\n\t\t\t// Try to transition to INITIALIZING - should wait if another is initializing\n\t\t\t_, err := sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateInitializing)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Goroutine %d: failed to transition: %v\", id, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Record execution order\n\t\t\torderMu.Lock()\n\t\t\texecutionOrder = append(executionOrder, id)\n\t\t\torderMu.Unlock()\n\n\t\t\tt.Logf(\"Goroutine %d: entered INITIALIZING (position %d)\", id, len(executionOrder))\n\n\t\t\t// Simulate initialization work\n\t\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t\t// Transition back to READY\n\t\t\tsm.Transition(StateIdle)\n\n\t\t\tcompletedCount.Add(1)\n\t\t\tt.Logf(\"Goroutine %d: completed initialization (total=%d)\", id, completedCount.Load())\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// All goroutines should have completed successfully\n\tif completedCount.Load() != numGoroutines {\n\t\tt.Errorf(\"expected %d completions, got %d\", numGoroutines, completedCount.Load())\n\t}\n\n\t// Final state should be IDLE\n\tif sm.GetState() != StateIdle {\n\t\tt.Errorf(\"expected final state IDLE, got %s\", sm.GetState())\n\t}\n\n\tt.Logf(\"Execution order: %v\", executionOrder)\n}\n\nfunc TestConnStateMachine_FIFOOrdering(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateInitializing) // Start in INITIALIZING so all waiters must queue\n\n\tconst numGoroutines = 10\n\tvar executionOrder []int\n\tvar orderMu sync.Mutex\n\tvar wg sync.WaitGroup\n\n\t// Launch goroutines one at a time, ensuring each is queued before launching the next\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\texpectedWaiters := int32(i + 1)\n\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tctx := context.Background()\n\n\t\t\t// This should queue in FIFO order\n\t\t\t_, err := sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateInitializing)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Goroutine %d: failed to transition: %v\", id, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Record execution order\n\t\t\torderMu.Lock()\n\t\t\texecutionOrder = append(executionOrder, id)\n\t\t\torderMu.Unlock()\n\n\t\t\tt.Logf(\"Goroutine %d: executed (position %d)\", id, len(executionOrder))\n\n\t\t\t// Transition back to IDLE to allow next waiter\n\t\t\tsm.Transition(StateIdle)\n\t\t}(i)\n\n\t\t// Wait until this goroutine has been queued before launching the next\n\t\t// Poll the waiter count to ensure the goroutine is actually queued\n\t\ttimeout := time.After(100 * time.Millisecond)\n\t\tfor {\n\t\t\tif sm.waiterCount.Load() >= expectedWaiters {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\tt.Fatalf(\"Timeout waiting for goroutine %d to queue\", i)\n\t\t\tcase <-time.After(1 * time.Millisecond):\n\t\t\t\t// Continue polling\n\t\t\t}\n\t\t}\n\t}\n\n\t// Give all goroutines time to fully settle in the queue\n\ttime.Sleep(10 * time.Millisecond)\n\n\t// Transition to IDLE to start processing the queue\n\tsm.Transition(StateIdle)\n\n\twg.Wait()\n\n\tt.Logf(\"Execution order: %v\", executionOrder)\n\n\t// Verify FIFO ordering - should be [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n\tfor i := 0; i < numGoroutines; i++ {\n\t\tif executionOrder[i] != i {\n\t\t\tt.Errorf(\"FIFO violation: expected goroutine %d at position %d, got %d\", i, i, executionOrder[i])\n\t\t}\n\t}\n}\n\nfunc TestConnStateMachine_FIFOWithFastPath(t *testing.T) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle) // Start in READY so fast path is available\n\n\tconst numGoroutines = 10\n\tvar executionOrder []int\n\tvar orderMu sync.Mutex\n\tvar wg sync.WaitGroup\n\tvar startBarrier sync.WaitGroup\n\tstartBarrier.Add(numGoroutines)\n\n\t// Launch goroutines that will all try the fast path\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// Wait for all goroutines to be ready\n\t\t\tstartBarrier.Done()\n\t\t\tstartBarrier.Wait()\n\n\t\t\t// Small stagger to establish arrival order\n\t\t\ttime.Sleep(time.Duration(id) * 100 * time.Microsecond)\n\n\t\t\tctx := context.Background()\n\n\t\t\t// This might use fast path (CAS) or slow path (queue)\n\t\t\t_, err := sm.AwaitAndTransition(ctx, []ConnState{StateIdle}, StateInitializing)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Goroutine %d: failed to transition: %v\", id, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Record execution order\n\t\t\torderMu.Lock()\n\t\t\texecutionOrder = append(executionOrder, id)\n\t\t\torderMu.Unlock()\n\n\t\t\tt.Logf(\"Goroutine %d: executed (position %d)\", id, len(executionOrder))\n\n\t\t\t// Simulate work\n\t\t\ttime.Sleep(5 * time.Millisecond)\n\n\t\t\t// Transition back to READY to allow next waiter\n\t\t\tsm.Transition(StateIdle)\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\tt.Logf(\"Execution order: %v\", executionOrder)\n\n\t// Check if FIFO was maintained\n\t// With the current fast-path implementation, this might NOT be FIFO\n\tfifoViolations := 0\n\tfor i := 0; i < numGoroutines; i++ {\n\t\tif executionOrder[i] != i {\n\t\t\tfifoViolations++\n\t\t}\n\t}\n\n\tif fifoViolations > 0 {\n\t\tt.Logf(\"WARNING: %d FIFO violations detected (fast path bypasses queue)\", fifoViolations)\n\t\tt.Logf(\"This is expected with current implementation - fast path uses CAS race\")\n\t}\n}\n\nfunc BenchmarkConnStateMachine_TryTransition(b *testing.B) {\n\tsm := NewConnStateMachine()\n\tsm.Transition(StateIdle)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = sm.TryTransition([]ConnState{StateIdle}, StateUnusable)\n\t\tsm.Transition(StateIdle)\n\t}\n}\n\nfunc TestConnStateMachine_IdleInUseTransitions(t *testing.T) {\n\tsm := NewConnStateMachine()\n\n\t// Initialize to IDLE state\n\tsm.Transition(StateInitializing)\n\tsm.Transition(StateIdle)\n\n\t// Test IDLE → IN_USE transition\n\t_, err := sm.TryTransition([]ConnState{StateIdle}, StateInUse)\n\tif err != nil {\n\t\tt.Errorf(\"failed to transition from IDLE to IN_USE: %v\", err)\n\t}\n\tif state := sm.GetState(); state != StateInUse {\n\t\tt.Errorf(\"expected state IN_USE, got %s\", state)\n\t}\n\n\t// Test IN_USE → IDLE transition\n\t_, err = sm.TryTransition([]ConnState{StateInUse}, StateIdle)\n\tif err != nil {\n\t\tt.Errorf(\"failed to transition from IN_USE to IDLE: %v\", err)\n\t}\n\tif state := sm.GetState(); state != StateIdle {\n\t\tt.Errorf(\"expected state IDLE, got %s\", state)\n\t}\n\n\t// Test concurrent acquisition (only one should succeed)\n\tsm.Transition(StateIdle)\n\n\tvar successCount atomic.Int32\n\tvar wg sync.WaitGroup\n\n\tfor i := 0; i < 10; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\t_, err := sm.TryTransition([]ConnState{StateIdle}, StateInUse)\n\t\t\tif err == nil {\n\t\t\t\tsuccessCount.Add(1)\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\tif count := successCount.Load(); count != 1 {\n\t\tt.Errorf(\"expected exactly 1 successful transition, got %d\", count)\n\t}\n\n\tif state := sm.GetState(); state != StateInUse {\n\t\tt.Errorf(\"expected final state IN_USE, got %s\", state)\n\t}\n}\n\nfunc TestConn_UsedMethods(t *testing.T) {\n\tcn := NewConn(nil)\n\n\t// Initialize connection to IDLE state\n\tcn.stateMachine.Transition(StateInitializing)\n\tcn.stateMachine.Transition(StateIdle)\n\n\t// Test IsUsed - should be false when IDLE\n\tif cn.IsUsed() {\n\t\tt.Error(\"expected IsUsed to be false for IDLE connection\")\n\t}\n\n\t// Test CompareAndSwapUsed - acquire connection\n\tif !cn.CompareAndSwapUsed(false, true) {\n\t\tt.Error(\"failed to acquire connection with CompareAndSwapUsed\")\n\t}\n\n\t// Test IsUsed - should be true when IN_USE\n\tif !cn.IsUsed() {\n\t\tt.Error(\"expected IsUsed to be true for IN_USE connection\")\n\t}\n\n\t// Test CompareAndSwapUsed - release connection\n\tif !cn.CompareAndSwapUsed(true, false) {\n\t\tt.Error(\"failed to release connection with CompareAndSwapUsed\")\n\t}\n\n\t// Test IsUsed - should be false again\n\tif cn.IsUsed() {\n\t\tt.Error(\"expected IsUsed to be false after release\")\n\t}\n\n\t// Test SetUsed\n\tcn.SetUsed(true)\n\tif !cn.IsUsed() {\n\t\tt.Error(\"expected IsUsed to be true after SetUsed(true)\")\n\t}\n\n\tcn.SetUsed(false)\n\tif cn.IsUsed() {\n\t\tt.Error(\"expected IsUsed to be false after SetUsed(false)\")\n\t}\n}\n\nfunc TestConnStateMachine_UnusableState(t *testing.T) {\n\tsm := NewConnStateMachine()\n\n\t// Initialize to IDLE state\n\tsm.Transition(StateInitializing)\n\tsm.Transition(StateIdle)\n\n\t// Test IDLE → UNUSABLE transition (for background operations)\n\t_, err := sm.TryTransition([]ConnState{StateIdle}, StateUnusable)\n\tif err != nil {\n\t\tt.Errorf(\"failed to transition from IDLE to UNUSABLE: %v\", err)\n\t}\n\tif state := sm.GetState(); state != StateUnusable {\n\t\tt.Errorf(\"expected state UNUSABLE, got %s\", state)\n\t}\n\n\t// Test UNUSABLE → IDLE transition (after background operation completes)\n\t_, err = sm.TryTransition([]ConnState{StateUnusable}, StateIdle)\n\tif err != nil {\n\t\tt.Errorf(\"failed to transition from UNUSABLE to IDLE: %v\", err)\n\t}\n\tif state := sm.GetState(); state != StateIdle {\n\t\tt.Errorf(\"expected state IDLE, got %s\", state)\n\t}\n\n\t// Test that we can transition from IN_USE to UNUSABLE if needed\n\t// (e.g., for urgent handoff while connection is in use)\n\tsm.Transition(StateInUse)\n\t_, err = sm.TryTransition([]ConnState{StateInUse}, StateUnusable)\n\tif err != nil {\n\t\tt.Errorf(\"failed to transition from IN_USE to UNUSABLE: %v\", err)\n\t}\n\tif state := sm.GetState(); state != StateUnusable {\n\t\tt.Errorf(\"expected state UNUSABLE, got %s\", state)\n\t}\n\n\t// Test UNUSABLE → INITIALIZING transition (for handoff)\n\tsm.Transition(StateIdle)\n\tsm.Transition(StateUnusable)\n\t_, err = sm.TryTransition([]ConnState{StateUnusable}, StateInitializing)\n\tif err != nil {\n\t\tt.Errorf(\"failed to transition from UNUSABLE to INITIALIZING: %v\", err)\n\t}\n\tif state := sm.GetState(); state != StateInitializing {\n\t\tt.Errorf(\"expected state INITIALIZING, got %s\", state)\n\t}\n}\n\nfunc TestConn_UsableUnusable(t *testing.T) {\n\tcn := NewConn(nil)\n\n\t// Initialize connection to IDLE state\n\tcn.stateMachine.Transition(StateInitializing)\n\tcn.stateMachine.Transition(StateIdle)\n\n\t// Test IsUsable - should be true when IDLE\n\tif !cn.IsUsable() {\n\t\tt.Error(\"expected IsUsable to be true for IDLE connection\")\n\t}\n\n\t// Test CompareAndSwapUsable - make unusable for background operation\n\tif !cn.CompareAndSwapUsable(true, false) {\n\t\tt.Error(\"failed to make connection unusable with CompareAndSwapUsable\")\n\t}\n\n\t// Verify state is UNUSABLE\n\tif state := cn.stateMachine.GetState(); state != StateUnusable {\n\t\tt.Errorf(\"expected state UNUSABLE, got %s\", state)\n\t}\n\n\t// Test IsUsable - should be false when UNUSABLE\n\tif cn.IsUsable() {\n\t\tt.Error(\"expected IsUsable to be false for UNUSABLE connection\")\n\t}\n\n\t// Test CompareAndSwapUsable - make usable again\n\tif !cn.CompareAndSwapUsable(false, true) {\n\t\tt.Error(\"failed to make connection usable with CompareAndSwapUsable\")\n\t}\n\n\t// Verify state is IDLE\n\tif state := cn.stateMachine.GetState(); state != StateIdle {\n\t\tt.Errorf(\"expected state IDLE, got %s\", state)\n\t}\n\n\t// Test SetUsable(false)\n\tcn.SetUsable(false)\n\tif state := cn.stateMachine.GetState(); state != StateUnusable {\n\t\tt.Errorf(\"expected state UNUSABLE after SetUsable(false), got %s\", state)\n\t}\n\n\t// Test SetUsable(true)\n\tcn.SetUsable(true)\n\tif state := cn.stateMachine.GetState(); state != StateIdle {\n\t\tt.Errorf(\"expected state IDLE after SetUsable(true), got %s\", state)\n\t}\n}\n"
  },
  {
    "path": "internal/pool/conn_used_at_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\n// TestConn_UsedAtUpdatedOnRead verifies that usedAt is updated when reading from connection\nfunc TestConn_UsedAtUpdatedOnRead(t *testing.T) {\n\t// Create a mock connection\n\tserver, client := net.Pipe()\n\tdefer server.Close()\n\tdefer client.Close()\n\n\tcn := NewConn(client)\n\tdefer cn.Close()\n\n\t// Get initial usedAt time\n\tinitialUsedAt := cn.UsedAt()\n\n\t// Wait 100ms to ensure time difference (usedAt has ~50ms precision from cached time)\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Simulate a read operation by calling WithReader\n\tctx := context.Background()\n\terr := cn.WithReader(ctx, time.Second, func(rd *proto.Reader) error {\n\t\t// Don't actually read anything, just trigger the deadline update\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tt.Fatalf(\"WithReader failed: %v\", err)\n\t}\n\n\t// Get updated usedAt time\n\tupdatedUsedAt := cn.UsedAt()\n\n\t// Verify that usedAt was updated\n\tif !updatedUsedAt.After(initialUsedAt) {\n\t\tt.Errorf(\"Expected usedAt to be updated after read. Initial: %v, Updated: %v\",\n\t\t\tinitialUsedAt, updatedUsedAt)\n\t}\n\n\t// Verify the difference is reasonable (should be around 100ms, accounting for ~50ms cache precision and ~5ms sleep precision)\n\tdiff := updatedUsedAt.Sub(initialUsedAt)\n\tif diff < 45*time.Millisecond || diff > 155*time.Millisecond {\n\t\tt.Errorf(\"Expected usedAt difference to be around 100ms (±50ms for cache, ±5ms for sleep), got %v\", diff)\n\t}\n}\n\n// TestConn_UsedAtUpdatedOnWrite verifies that usedAt is updated when writing to connection\nfunc TestConn_UsedAtUpdatedOnWrite(t *testing.T) {\n\t// Create a mock connection\n\tserver, client := net.Pipe()\n\tdefer server.Close()\n\tdefer client.Close()\n\n\tcn := NewConn(client)\n\tdefer cn.Close()\n\n\t// Get initial usedAt time\n\tinitialUsedAt := cn.UsedAt()\n\n\t// Wait at least 100ms to ensure time difference (usedAt has ~50ms precision from cached time)\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Simulate a write operation by calling WithWriter\n\tctx := context.Background()\n\terr := cn.WithWriter(ctx, time.Second, func(wr *proto.Writer) error {\n\t\t// Don't actually write anything, just trigger the deadline update\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tt.Fatalf(\"WithWriter failed: %v\", err)\n\t}\n\n\t// Get updated usedAt time\n\tupdatedUsedAt := cn.UsedAt()\n\n\t// Verify that usedAt was updated\n\tif !updatedUsedAt.After(initialUsedAt) {\n\t\tt.Errorf(\"Expected usedAt to be updated after write. Initial: %v, Updated: %v\",\n\t\t\tinitialUsedAt, updatedUsedAt)\n\t}\n\n\t// Verify the difference is reasonable (should be around 100ms, accounting for ~50ms cache precision)\n\tdiff := updatedUsedAt.Sub(initialUsedAt)\n\n\t// 50 ms is the cache precision, so we allow up to 110ms difference\n\tif diff < 45*time.Millisecond || diff > 155*time.Millisecond {\n\t\tt.Errorf(\"Expected usedAt difference to be around 100 (±50ms for cache) (+-5ms for sleep precision), got %v\", diff)\n\t}\n}\n\n// TestConn_UsedAtUpdatedOnMultipleOperations verifies that usedAt is updated on each operation\nfunc TestConn_UsedAtUpdatedOnMultipleOperations(t *testing.T) {\n\t// Create a mock connection\n\tserver, client := net.Pipe()\n\tdefer server.Close()\n\tdefer client.Close()\n\n\tcn := NewConn(client)\n\tdefer cn.Close()\n\n\tctx := context.Background()\n\tvar previousUsedAt time.Time\n\n\t// Perform multiple operations and verify usedAt is updated each time\n\t// Note: usedAt has ~50ms precision from cached time\n\tfor i := 0; i < 5; i++ {\n\t\tcurrentUsedAt := cn.UsedAt()\n\n\t\tif i > 0 {\n\t\t\t// Verify usedAt was updated from previous iteration\n\t\t\tif !currentUsedAt.After(previousUsedAt) {\n\t\t\t\tt.Errorf(\"Iteration %d: Expected usedAt to be updated. Previous: %v, Current: %v\",\n\t\t\t\t\ti, previousUsedAt, currentUsedAt)\n\t\t\t}\n\t\t}\n\n\t\tpreviousUsedAt = currentUsedAt\n\n\t\t// Wait at least 100ms (accounting for ~50ms cache precision)\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Perform a read operation\n\t\terr := cn.WithReader(ctx, time.Second, func(rd *proto.Reader) error {\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Iteration %d: WithReader failed: %v\", i, err)\n\t\t}\n\t}\n\n\t// Verify final usedAt is significantly later than initial\n\tfinalUsedAt := cn.UsedAt()\n\tif !finalUsedAt.After(previousUsedAt) {\n\t\tt.Errorf(\"Expected final usedAt to be updated. Previous: %v, Final: %v\",\n\t\t\tpreviousUsedAt, finalUsedAt)\n\t}\n}\n\n// TestConn_UsedAtNotUpdatedWithoutOperation verifies that usedAt is NOT updated without operations\nfunc TestConn_UsedAtNotUpdatedWithoutOperation(t *testing.T) {\n\t// Create a mock connection\n\tserver, client := net.Pipe()\n\tdefer server.Close()\n\tdefer client.Close()\n\n\tcn := NewConn(client)\n\tdefer cn.Close()\n\n\t// Get initial usedAt time\n\tinitialUsedAt := cn.UsedAt()\n\n\t// Wait without performing any operations\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Get usedAt time again\n\tcurrentUsedAt := cn.UsedAt()\n\n\t// Verify that usedAt was NOT updated (should be the same)\n\tif !currentUsedAt.Equal(initialUsedAt) {\n\t\tt.Errorf(\"Expected usedAt to remain unchanged without operations. Initial: %v, Current: %v\",\n\t\t\tinitialUsedAt, currentUsedAt)\n\t}\n}\n\n// TestConn_UsedAtConcurrentUpdates verifies that usedAt updates are thread-safe\nfunc TestConn_UsedAtConcurrentUpdates(t *testing.T) {\n\t// Create a mock connection\n\tserver, client := net.Pipe()\n\tdefer server.Close()\n\tdefer client.Close()\n\n\tcn := NewConn(client)\n\tdefer cn.Close()\n\n\tctx := context.Background()\n\tconst numGoroutines = 10\n\tconst numIterations = 10\n\n\t// Launch multiple goroutines that perform operations concurrently\n\tdone := make(chan bool, numGoroutines)\n\tfor i := 0; i < numGoroutines; i++ {\n\t\tgo func() {\n\t\t\tfor j := 0; j < numIterations; j++ {\n\t\t\t\t// Alternate between read and write operations\n\t\t\t\tif j%2 == 0 {\n\t\t\t\t\t_ = cn.WithReader(ctx, time.Second, func(rd *proto.Reader) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\t_ = cn.WithWriter(ctx, time.Second, func(wr *proto.Writer) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t}\n\t\t\tdone <- true\n\t\t}()\n\t}\n\n\t// Wait for all goroutines to complete\n\tfor i := 0; i < numGoroutines; i++ {\n\t\t<-done\n\t}\n\n\t// Verify that usedAt was updated (should be recent)\n\tusedAt := cn.UsedAt()\n\ttimeSinceUsed := time.Since(usedAt)\n\n\t// Should be very recent (within last second)\n\tif timeSinceUsed > time.Second {\n\t\tt.Errorf(\"Expected usedAt to be recent, but it was %v ago\", timeSinceUsed)\n\t}\n}\n\n// TestConn_UsedAtPrecision verifies that usedAt has 50ms precision (not nanosecond)\nfunc TestConn_UsedAtPrecision(t *testing.T) {\n\t// Create a mock connection\n\tserver, client := net.Pipe()\n\tdefer server.Close()\n\tdefer client.Close()\n\n\tcn := NewConn(client)\n\tdefer cn.Close()\n\n\tctx := context.Background()\n\n\t// Perform an operation\n\terr := cn.WithReader(ctx, time.Second, func(rd *proto.Reader) error {\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"WithReader failed: %v\", err)\n\t}\n\n\t// Get usedAt time\n\tusedAt := cn.UsedAt()\n\n\t// Verify that usedAt has nanosecond precision (from the cached time which updates every 50ms)\n\t// The value should be reasonable (not year 1970 or something)\n\tif usedAt.Year() < 2020 {\n\t\tt.Errorf(\"Expected usedAt to be a recent time, got %v\", usedAt)\n\t}\n\n\t// The nanoseconds might be non-zero depending on when the cache was updated\n\t// We just verify the time is stored with full precision (not truncated to seconds)\n\tinitialNanos := usedAt.UnixNano()\n\tif initialNanos == 0 {\n\t\tt.Error(\"Expected usedAt to have nanosecond precision, got 0\")\n\t}\n}\n"
  },
  {
    "path": "internal/pool/dial_conn_retry_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestDialConn_HangingDial_RetriesWithPerAttemptTimeout(t *testing.T) {\n\tvar calls atomic.Int32\n\tvar sawDeadline atomic.Int32\n\n\tconst (\n\t\tdialTimeout = 50 * time.Millisecond\n\t\tbackoff     = 10 * time.Millisecond\n\t\tretries     = 3\n\t)\n\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\tcalls.Add(1)\n\n\t\t\t// Ensure each attempt has a deadline (pool applies DialTimeout per attempt).\n\t\t\tif dl, ok := ctx.Deadline(); ok {\n\t\t\t\trem := time.Until(dl)\n\t\t\t\t// Very generous bounds to avoid flakes.\n\t\t\t\tif rem > 5*time.Millisecond && rem <= 2*dialTimeout {\n\t\t\t\t\tsawDeadline.Add(1)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Simulate a TCP connect hang: block until the context cancels.\n\t\t\t<-ctx.Done()\n\t\t\treturn nil, ctx.Err()\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        dialTimeout,\n\t\tDialerRetries:      retries,\n\t\tDialerRetryTimeout: backoff,\n\t})\n\tdefer p.Close()\n\n\t// Use a parent context with a bounded timeout so this test fails fast (instead of hanging)\n\t// when dialConn does not apply per-attempt DialTimeout via context.\n\tparentBudget := dialTimeout*time.Duration(retries) + backoff*time.Duration(retries-1) + 250*time.Millisecond\n\tctx, cancel := context.WithTimeout(context.Background(), parentBudget)\n\tdefer cancel()\n\n\tstart := time.Now()\n\t_, err := p.dialConn(ctx, true)\n\telapsed := time.Since(start)\n\n\tif err == nil {\n\t\tt.Fatalf(\"expected error\")\n\t}\n\tif got := calls.Load(); got != retries {\n\t\tt.Fatalf(\"expected %d dial attempts, got %d\", retries, got)\n\t}\n\tif got := sawDeadline.Load(); got != retries {\n\t\tt.Fatalf(\"expected deadline on all attempts, got %d/%d\", got, retries)\n\t}\n\n\t// Each attempt should wait ~dialTimeout, plus backoff between attempts.\n\t// Allow wide bounds for CI noise.\n\tmin := dialTimeout*time.Duration(retries) + backoff*time.Duration(retries-1)\n\tif elapsed < min/2 {\n\t\tt.Fatalf(\"dialConn returned too quickly (%v < %v), retries/backoff may not have occurred\", elapsed, min/2)\n\t}\n\tif elapsed > 5*min {\n\t\tt.Fatalf(\"dialConn took too long (%v > %v), likely hung beyond expected timeouts\", elapsed, 5*min)\n\t}\n}\n\nfunc TestDialConn_DoesNotExtendEarlierParentDeadline(t *testing.T) {\n\tvar calls atomic.Int32\n\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\tcalls.Add(1)\n\t\t\tdl, ok := ctx.Deadline()\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.New(\"expected deadline\")\n\t\t\t}\n\t\t\t// Parent deadline should win (be soon).\n\t\t\tif time.Until(dl) > 100*time.Millisecond {\n\t\t\t\treturn nil, errors.New(\"deadline was unexpectedly extended\")\n\t\t\t}\n\t\t\t<-ctx.Done()\n\t\t\treturn nil, ctx.Err()\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        500 * time.Millisecond,\n\t\tDialerRetries:      1,\n\t})\n\tdefer p.Close()\n\n\tparent, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)\n\tdefer cancel()\n\n\t_, err := p.dialConn(parent, true)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error\")\n\t}\n\tif got := calls.Load(); got != 1 {\n\t\tt.Fatalf(\"expected 1 dial attempt, got %d\", got)\n\t}\n}\n\nfunc TestDialConn_ContextCancelStopsFurtherRetries(t *testing.T) {\n\tvar calls atomic.Int32\n\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\tn := calls.Add(1)\n\t\t\tif n == 1 {\n\t\t\t\t// First attempt fails immediately; test cancels context to stop retries.\n\t\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t\t}\n\t\t\treturn nil, errors.New(\"unexpected extra attempt\")\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        5 * time.Second,\n\t\tDialerRetries:      5,\n\t\tDialerRetryTimeout: 5 * time.Second,\n\t})\n\tdefer p.Close()\n\n\tctx, cancel := context.WithCancel(context.Background())\n\t// Cancel immediately after the first attempt fails by wrapping dialConn call.\n\t// We do it via a goroutine so dialConn has a chance to enter the backoff select.\n\tgo func() {\n\t\t// Give dialConn a moment to start. This avoids a race where ctx is already canceled\n\t\t// before the first attempt and we wouldn't be testing the retry stop path.\n\t\ttime.Sleep(5 * time.Millisecond)\n\t\tcancel()\n\t}()\n\n\t_, _ = p.dialConn(ctx, true)\n\n\tif got := calls.Load(); got != 1 {\n\t\tt.Fatalf(\"expected dialer to be called once after cancel, got %d\", got)\n\t}\n}\n\nfunc TestDialConn_DialTimeoutDisabled_DoesNotSetDeadline(t *testing.T) {\n\tvar calls atomic.Int32\n\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\tcalls.Add(1)\n\t\t\tif _, ok := ctx.Deadline(); ok {\n\t\t\t\treturn nil, errors.New(\"unexpected deadline when DialTimeout disabled\")\n\t\t\t}\n\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        0,\n\t\tDialerRetries:      1,\n\t})\n\tdefer p.Close()\n\n\t_, err := p.dialConn(context.Background(), true)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error\")\n\t}\n\tif got := calls.Load(); got != 1 {\n\t\tt.Fatalf(\"expected 1 dial attempt, got %d\", got)\n\t}\n}\n\nfunc TestDialConn_NoBackoffAfterLastAttempt(t *testing.T) {\n\tvar calls atomic.Int32\n\tbackoff := 300 * time.Millisecond\n\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\tcalls.Add(1)\n\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        5 * time.Second,\n\t\tDialerRetries:      1,\n\t\tDialerRetryTimeout: backoff,\n\t})\n\tdefer p.Close()\n\n\tstart := time.Now()\n\t_, err := p.dialConn(context.Background(), true)\n\telapsed := time.Since(start)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error\")\n\t}\n\tif got := calls.Load(); got != 1 {\n\t\tt.Fatalf(\"expected 1 dial attempt, got %d\", got)\n\t}\n\t// If we slept after the last attempt, this will be ~backoff.\n\tif elapsed >= backoff/2 {\n\t\tt.Fatalf(\"dialConn took too long (%v); likely slept after last attempt (backoff=%v)\", elapsed, backoff)\n\t}\n}\n"
  },
  {
    "path": "internal/pool/dial_context_timeout_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n)\n\n// Ensures ConnPool applies DialTimeout per attempt via context (so dialing doesn't hang\n// when a custom dialer ignores timeouts).\nfunc TestDialConn_AppliesDialTimeoutPerAttemptViaContext(t *testing.T) {\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t// Pool should apply DialTimeout per attempt via context.\n\t\t\tdl, ok := ctx.Deadline()\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.New(\"expected context deadline\")\n\t\t\t}\n\t\t\tremaining := time.Until(dl)\n\t\t\t// Allow slack for scheduling jitter.\n\t\t\tif remaining <= 50*time.Millisecond || remaining > 250*time.Millisecond {\n\t\t\t\treturn nil, errors.New(\"unexpected context deadline duration\")\n\t\t\t}\n\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        200 * time.Millisecond,\n\t\tPoolTimeout:        10 * time.Millisecond,\n\t\tDialerRetries:      1,\n\t})\n\tdefer p.Close()\n\n\t_, err := p.newConn(context.Background(), true)\n\tif err == nil {\n\t\tt.Fatalf(\"expected error\")\n\t}\n}\n"
  },
  {
    "path": "internal/pool/dial_retry_backoff_test.go",
    "content": "package pool\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestDialRetryBackoff_ConstantByDefault_ExponentialWhenMaxSet(t *testing.T) {\n\tp := &ConnPool{cfg: &Options{DialerRetryTimeout: 10 * time.Millisecond}}\n\n\t// default constant when max not set\n\tfor i := 0; i < 5; i++ {\n\t\tif got := p.dialRetryBackoff(i); got != 10*time.Millisecond {\n\t\t\tt.Fatalf(\"constant: attempt=%d got %v\", i, got)\n\t\t}\n\t}\n\n\t// custom function\n\tp.cfg.DialerRetryBackoff = func(attempt int) time.Duration {\n\t\treturn time.Duration(attempt+1) * time.Millisecond\n\t}\n\tfor i := 0; i < 5; i++ {\n\t\tgot := p.dialRetryBackoff(i)\n\t\twant := time.Duration(i+1) * time.Millisecond\n\t\tif got != want {\n\t\t\tt.Fatalf(\"custom: attempt=%d got %v, want %v\", i, got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/pool/double_freeturn_simple_test.go",
    "content": "package pool_test\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// TestDoubleFreeTurnSimple tests the double-free bug with a simple scenario.\n// This test FAILS with the OLD code and PASSES with the NEW code.\n//\n// Scenario:\n// 1. Request A times out, Dial A completes and delivers connection to Request B\n// 2. Request B's own Dial B completes later\n// 3. With the bug: Dial B frees Request B's turn (even though Request B is using connection A)\n// 4. Then Request B calls Put() and frees the turn AGAIN (double-free)\n// 5. This allows more concurrent operations than PoolSize permits\n//\n// Detection method:\n// - Try to acquire PoolSize+1 connections after the double-free\n// - With the bug: All succeed (pool size violated)\n// - With the fix: Only PoolSize succeed\nfunc TestDoubleFreeTurnSimple(t *testing.T) {\n\tctx := context.Background()\n\n\tvar dialCount atomic.Int32\n\tdialBComplete := make(chan struct{})\n\trequestBGotConn := make(chan struct{})\n\trequestBCalledPut := make(chan struct{})\n\n\tcontrolledDialer := func(ctx context.Context) (net.Conn, error) {\n\t\tcount := dialCount.Add(1)\n\n\t\tif count == 1 {\n\t\t\t// Dial A: takes 150ms\n\t\t\ttime.Sleep(150 * time.Millisecond)\n\t\t\tt.Logf(\"Dial A completed\")\n\t\t} else if count == 2 {\n\t\t\t// Dial B: takes 300ms (longer than Dial A)\n\t\t\ttime.Sleep(300 * time.Millisecond)\n\t\t\tt.Logf(\"Dial B completed\")\n\t\t\tclose(dialBComplete)\n\t\t} else {\n\t\t\t// Other dials: fast\n\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t}\n\n\t\treturn newDummyConn(), nil\n\t}\n\n\ttestPool := pool.NewConnPool(&pool.Options{\n\t\tDialer:             controlledDialer,\n\t\tPoolSize:           2, // Only 2 concurrent operations allowed\n\t\tMaxConcurrentDials: 5,\n\t\tDialTimeout:        1 * time.Second,\n\t\tPoolTimeout:        1 * time.Second,\n\t})\n\tdefer testPool.Close()\n\n\t// Request A: Short timeout (100ms), will timeout before dial completes (150ms)\n\tgo func() {\n\t\tshortCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)\n\t\tdefer cancel()\n\n\t\t_, err := testPool.Get(shortCtx)\n\t\tif err != nil {\n\t\t\tt.Logf(\"Request A: Timed out as expected: %v\", err)\n\t\t}\n\t}()\n\n\t// Wait for Request A to start\n\ttime.Sleep(20 * time.Millisecond)\n\n\t// Request B: Long timeout, will receive connection from Request A's dial\n\trequestBDone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(requestBDone)\n\n\t\tlongCtx, cancel := context.WithTimeout(ctx, 1*time.Second)\n\t\tdefer cancel()\n\n\t\tcn, err := testPool.Get(longCtx)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Request B: Should have received connection but got error: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"Request B: Got connection from Request A's dial\")\n\t\tclose(requestBGotConn)\n\n\t\t// Wait for dial B to complete\n\t\t<-dialBComplete\n\n\t\tt.Logf(\"Request B: Dial B completed\")\n\n\t\t// Wait a bit to allow Dial B goroutine to finish and call freeTurn()\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Signal that we're ready for the test to check semaphore state\n\t\tclose(requestBCalledPut)\n\n\t\t// Wait for the test to check QueueLen\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\tt.Logf(\"Request B: Now calling Put()\")\n\t\ttestPool.Put(ctx, cn)\n\t\tt.Logf(\"Request B: Put() called\")\n\t}()\n\n\t// Wait for Request B to get the connection\n\t<-requestBGotConn\n\n\t// Wait for Dial B to complete and freeTurn() to be called\n\t<-requestBCalledPut\n\n\t// NOW WE'RE IN THE CRITICAL WINDOW\n\t// Request B is holding a connection (from Dial A)\n\t// Dial B has completed and returned (freeTurn() has been called)\n\t// With the bug:\n\t//   - Dial B freed Request B's turn (BUG!)\n\t//   - QueueLen should be 0\n\t// With the fix:\n\t//   - Dial B did NOT free Request B's turn\n\t//   - QueueLen should be 1 (Request B still holds the turn)\n\n\tt.Logf(\"\\n=== CRITICAL CHECK: QueueLen ===\")\n\tt.Logf(\"Request B is holding a connection, Dial B has completed and returned\")\n\tqueueLen := testPool.QueueLen()\n\tt.Logf(\"QueueLen: %d\", queueLen)\n\n\t// Wait for Request B to finish\n\tselect {\n\tcase <-requestBDone:\n\tcase <-time.After(1 * time.Second):\n\t\tt.Logf(\"Request B timed out\")\n\t}\n\n\tt.Logf(\"\\n=== Results ===\")\n\tt.Logf(\"QueueLen during critical window: %d\", queueLen)\n\tt.Logf(\"Expected with fix: 1 (Request B still holds the turn)\")\n\tt.Logf(\"Expected with bug: 0 (Dial B freed Request B's turn)\")\n\n\tif queueLen == 0 {\n\t\tt.Errorf(\"DOUBLE-FREE BUG DETECTED!\")\n\t\tt.Errorf(\"QueueLen is 0, meaning Dial B freed Request B's turn\")\n\t\tt.Errorf(\"But Request B is still holding a connection, so its turn should NOT be freed yet\")\n\t} else if queueLen == 1 {\n\t\tt.Logf(\"✓ CORRECT: QueueLen is 1\")\n\t\tt.Logf(\"Request B is still holding the turn (will be freed when Request B calls Put())\")\n\t} else {\n\t\tt.Logf(\"Unexpected QueueLen: %d (expected 1 with fix, 0 with bug)\", queueLen)\n\t}\n}\n"
  },
  {
    "path": "internal/pool/double_freeturn_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\n// TestDoubleFreeTurnBug demonstrates the double freeTurn bug where:\n// 1. Dial goroutine creates a connection\n// 2. Original waiter times out\n// 3. putIdleConn delivers connection to another waiter\n// 4. Dial goroutine calls freeTurn() (FIRST FREE)\n// 5. Second waiter uses connection and calls Put()\n// 6. Put() calls freeTurn() (SECOND FREE - BUG!)\n//\n// This causes the semaphore to be released twice, allowing more concurrent\n// operations than PoolSize allows.\nfunc TestDoubleFreeTurnBug(t *testing.T) {\n\tvar dialCount atomic.Int32\n\tvar putCount atomic.Int32\n\n\t// Slow dialer - 150ms per dial\n\tslowDialer := func(ctx context.Context) (net.Conn, error) {\n\t\tdialCount.Add(1)\n\t\tselect {\n\t\tcase <-time.After(150 * time.Millisecond):\n\t\t\tserver, client := net.Pipe()\n\t\t\tgo func() {\n\t\t\t\tdefer server.Close()\n\t\t\t\tbuf := make([]byte, 1024)\n\t\t\t\tfor {\n\t\t\t\t\t_, err := server.Read(buf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn client, nil\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t}\n\n\topt := &Options{\n\t\tDialer:             slowDialer,\n\t\tPoolSize:           10, // Small pool to make bug easier to trigger\n\t\tMaxConcurrentDials: 10,\n\t\tMinIdleConns:       0,\n\t\tPoolTimeout:        100 * time.Millisecond,\n\t\tDialTimeout:        5 * time.Second,\n\t}\n\n\tconnPool := NewConnPool(opt)\n\tdefer connPool.Close()\n\n\t// Scenario:\n\t// 1. Request A starts dial (100ms timeout - will timeout before dial completes)\n\t// 2. Request B arrives (500ms timeout - will wait in queue)\n\t// 3. Request A times out at 100ms\n\t// 4. Dial completes at 150ms\n\t// 5. putIdleConn delivers connection to Request B\n\t// 6. Dial goroutine calls freeTurn() - FIRST FREE\n\t// 7. Request B uses connection and calls Put()\n\t// 8. Put() calls freeTurn() - SECOND FREE (BUG!)\n\n\tvar wg sync.WaitGroup\n\n\t// Request A: Short timeout, will timeout before dial completes\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n\t\tdefer cancel()\n\n\t\tcn, err := connPool.Get(ctx)\n\t\tif err != nil {\n\t\t\t// Expected to timeout\n\t\t\tt.Logf(\"Request A timed out as expected: %v\", err)\n\t\t} else {\n\t\t\t// Should not happen\n\t\t\tt.Errorf(\"Request A should have timed out but got connection\")\n\t\t\tconnPool.Put(ctx, cn)\n\t\t\tputCount.Add(1)\n\t\t}\n\t}()\n\n\t// Wait a bit for Request A to start dialing\n\ttime.Sleep(10 * time.Millisecond)\n\n\t// Request B: Long timeout, will receive the connection from putIdleConn\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\t\tdefer cancel()\n\n\t\tcn, err := connPool.Get(ctx)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Request B should have succeeded but got error: %v\", err)\n\t\t} else {\n\t\t\tt.Logf(\"Request B got connection successfully\")\n\t\t\t// Use the connection briefly\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\tconnPool.Put(ctx, cn)\n\t\t\tputCount.Add(1)\n\t\t}\n\t}()\n\n\twg.Wait()\n\n\t// Check results\n\tt.Logf(\"\\n=== Results ===\")\n\tt.Logf(\"Dials: %d\", dialCount.Load())\n\tt.Logf(\"Puts: %d\", putCount.Load())\n\n\t// The bug is hard to detect directly without instrumenting freeTurn,\n\t// but we can verify the scenario works correctly:\n\t// - Request A should timeout\n\t// - Request B should succeed and get the connection\n\t// - 1-2 dials may occur (Request A starts one, Request B may start another)\n\t// - 1 put should occur (Request B returning the connection)\n\n\tif putCount.Load() != 1 {\n\t\tt.Errorf(\"Expected 1 put, got %d\", putCount.Load())\n\t}\n\n\tt.Logf(\"✓ Scenario completed successfully\")\n\tt.Logf(\"Note: The double freeTurn bug would cause semaphore to be released twice,\")\n\tt.Logf(\"allowing more concurrent operations than PoolSize permits.\")\n\tt.Logf(\"With the fix, putIdleConn returns true when delivering to a waiter,\")\n\tt.Logf(\"preventing the dial goroutine from calling freeTurn (waiter will call it later).\")\n}\n\n// TestDoubleFreeTurnHighConcurrency tests the bug under high concurrency\nfunc TestDoubleFreeTurnHighConcurrency(t *testing.T) {\n\tvar dialCount atomic.Int32\n\tvar getSuccesses atomic.Int32\n\tvar getFailures atomic.Int32\n\n\tslowDialer := func(ctx context.Context) (net.Conn, error) {\n\t\tdialCount.Add(1)\n\t\tselect {\n\t\tcase <-time.After(200 * time.Millisecond):\n\t\t\tserver, client := net.Pipe()\n\t\t\tgo func() {\n\t\t\t\tdefer server.Close()\n\t\t\t\tbuf := make([]byte, 1024)\n\t\t\t\tfor {\n\t\t\t\t\t_, err := server.Read(buf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn client, nil\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t}\n\n\topt := &Options{\n\t\tDialer:             slowDialer,\n\t\tPoolSize:           20,\n\t\tMaxConcurrentDials: 20,\n\t\tMinIdleConns:       0,\n\t\tPoolTimeout:        100 * time.Millisecond,\n\t\tDialTimeout:        5 * time.Second,\n\t}\n\n\tconnPool := NewConnPool(opt)\n\tdefer connPool.Close()\n\n\t// Create many requests with varying timeouts\n\t// Some will timeout before dial completes, triggering the putIdleConn delivery path\n\tconst numRequests = 100\n\tvar wg sync.WaitGroup\n\n\tfor i := 0; i < numRequests; i++ {\n\t\twg.Add(1)\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// Vary timeout: some short (will timeout), some long (will succeed)\n\t\t\ttimeout := 100 * time.Millisecond\n\t\t\tif id%3 == 0 {\n\t\t\t\ttimeout = 500 * time.Millisecond\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\t\tdefer cancel()\n\n\t\t\tcn, err := connPool.Get(ctx)\n\t\t\tif err != nil {\n\t\t\t\tgetFailures.Add(1)\n\t\t\t} else {\n\t\t\t\tgetSuccesses.Add(1)\n\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\t\tconnPool.Put(ctx, cn)\n\t\t\t}\n\t\t}(i)\n\n\t\t// Stagger requests\n\t\tif i%10 == 0 {\n\t\t\ttime.Sleep(5 * time.Millisecond)\n\t\t}\n\t}\n\n\twg.Wait()\n\n\tt.Logf(\"\\n=== High Concurrency Results ===\")\n\tt.Logf(\"Requests: %d\", numRequests)\n\tt.Logf(\"Successes: %d\", getSuccesses.Load())\n\tt.Logf(\"Failures: %d\", getFailures.Load())\n\tt.Logf(\"Dials: %d\", dialCount.Load())\n\n\t// Verify that some requests succeeded despite timeouts\n\t// This exercises the putIdleConn delivery path\n\tif getSuccesses.Load() == 0 {\n\t\tt.Errorf(\"Expected some successful requests, got 0\")\n\t}\n\n\tt.Logf(\"✓ High concurrency test completed\")\n\tt.Logf(\"Note: This test exercises the putIdleConn delivery path where the bug occurs\")\n}\n"
  },
  {
    "path": "internal/pool/export_test.go",
    "content": "package pool\n\nimport (\n\t\"net\"\n\t\"time\"\n)\n\nfunc (cn *Conn) SetCreatedAt(tm time.Time) {\n\tcn.createdAt = tm\n}\n\nfunc (cn *Conn) NetConn() net.Conn {\n\treturn cn.getNetConn()\n}\n\nfunc (p *ConnPool) CheckMinIdleConns() {\n\tp.connsMu.Lock()\n\tp.checkMinIdleConns()\n\tp.connsMu.Unlock()\n}\n\nfunc (p *ConnPool) QueueLen() int {\n\treturn int(p.semaphore.Len())\n}\n\nfunc (p *ConnPool) DialsQueueLen() int {\n\treturn p.dialsQueue.len()\n}\n\nvar NoExpiration = noExpiration\n\nfunc (p *ConnPool) CalcConnExpiresAt() time.Time {\n\treturn p.calcConnExpiresAt()\n}\n"
  },
  {
    "path": "internal/pool/hooks.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\n// PoolHook defines the interface for connection lifecycle hooks.\ntype PoolHook interface {\n\t// OnGet is called when a connection is retrieved from the pool.\n\t// It can modify the connection or return an error to prevent its use.\n\t// The accept flag can be used to prevent the connection from being used.\n\t// On Accept = false the connection is rejected and returned to the pool.\n\t// The error can be used to prevent the connection from being used and returned to the pool.\n\t// On Errors, the connection is removed from the pool.\n\t// It has isNewConn flag to indicate if this is a new connection (rather than idle from the pool)\n\t// The flag can be used for gathering metrics on pool hit/miss ratio.\n\tOnGet(ctx context.Context, conn *Conn, isNewConn bool) (accept bool, err error)\n\n\t// OnPut is called when a connection is returned to the pool.\n\t// It returns whether the connection should be pooled and whether it should be removed.\n\tOnPut(ctx context.Context, conn *Conn) (shouldPool bool, shouldRemove bool, err error)\n\n\t// OnRemove is called when a connection is removed from the pool.\n\t// This happens when:\n\t// - Connection fails health check\n\t// - Connection exceeds max lifetime\n\t// - Pool is being closed\n\t// - Connection encounters an error\n\t// Implementations should clean up any per-connection state.\n\t// The reason parameter indicates why the connection was removed.\n\tOnRemove(ctx context.Context, conn *Conn, reason error)\n}\n\n// PoolHookManager manages multiple pool hooks.\ntype PoolHookManager struct {\n\thooks   []PoolHook\n\thooksMu sync.RWMutex\n}\n\n// NewPoolHookManager creates a new pool hook manager.\nfunc NewPoolHookManager() *PoolHookManager {\n\treturn &PoolHookManager{\n\t\thooks: make([]PoolHook, 0),\n\t}\n}\n\n// AddHook adds a pool hook to the manager.\n// Hooks are called in the order they were added.\nfunc (phm *PoolHookManager) AddHook(hook PoolHook) {\n\tphm.hooksMu.Lock()\n\tdefer phm.hooksMu.Unlock()\n\tphm.hooks = append(phm.hooks, hook)\n}\n\n// RemoveHook removes a pool hook from the manager.\nfunc (phm *PoolHookManager) RemoveHook(hook PoolHook) {\n\tphm.hooksMu.Lock()\n\tdefer phm.hooksMu.Unlock()\n\n\tfor i, h := range phm.hooks {\n\t\tif h == hook {\n\t\t\t// Remove hook by swapping with last element and truncating\n\t\t\tphm.hooks[i] = phm.hooks[len(phm.hooks)-1]\n\t\t\tphm.hooks = phm.hooks[:len(phm.hooks)-1]\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// ProcessOnGet calls all OnGet hooks in order.\n// If any hook returns an error, processing stops and the error is returned.\nfunc (phm *PoolHookManager) ProcessOnGet(ctx context.Context, conn *Conn, isNewConn bool) (acceptConn bool, err error) {\n\t// Copy slice reference while holding lock (fast)\n\tphm.hooksMu.RLock()\n\thooks := phm.hooks\n\tphm.hooksMu.RUnlock()\n\n\t// Call hooks without holding lock (slow operations)\n\tfor _, hook := range hooks {\n\t\tacceptConn, err := hook.OnGet(ctx, conn, isNewConn)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif !acceptConn {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// ProcessOnPut calls all OnPut hooks in order.\n// The first hook that returns shouldRemove=true or shouldPool=false will stop processing.\nfunc (phm *PoolHookManager) ProcessOnPut(ctx context.Context, conn *Conn) (shouldPool bool, shouldRemove bool, err error) {\n\t// Copy slice reference while holding lock (fast)\n\tphm.hooksMu.RLock()\n\thooks := phm.hooks\n\tphm.hooksMu.RUnlock()\n\n\tshouldPool = true // Default to pooling the connection\n\n\t// Call hooks without holding lock (slow operations)\n\tfor _, hook := range hooks {\n\t\thookShouldPool, hookShouldRemove, hookErr := hook.OnPut(ctx, conn)\n\n\t\tif hookErr != nil {\n\t\t\treturn false, true, hookErr\n\t\t}\n\n\t\t// If any hook says to remove or not pool, respect that decision\n\t\tif hookShouldRemove {\n\t\t\treturn false, true, nil\n\t\t}\n\n\t\tif !hookShouldPool {\n\t\t\tshouldPool = false\n\t\t}\n\t}\n\n\treturn shouldPool, false, nil\n}\n\n// ProcessOnRemove calls all OnRemove hooks in order.\nfunc (phm *PoolHookManager) ProcessOnRemove(ctx context.Context, conn *Conn, reason error) {\n\t// Copy slice reference while holding lock (fast)\n\tphm.hooksMu.RLock()\n\thooks := phm.hooks\n\tphm.hooksMu.RUnlock()\n\n\t// Call hooks without holding lock (slow operations)\n\tfor _, hook := range hooks {\n\t\thook.OnRemove(ctx, conn, reason)\n\t}\n}\n\n// GetHookCount returns the number of registered hooks (for testing).\nfunc (phm *PoolHookManager) GetHookCount() int {\n\tphm.hooksMu.RLock()\n\tdefer phm.hooksMu.RUnlock()\n\treturn len(phm.hooks)\n}\n\n// GetHooks returns a copy of all registered hooks.\nfunc (phm *PoolHookManager) GetHooks() []PoolHook {\n\tphm.hooksMu.RLock()\n\tdefer phm.hooksMu.RUnlock()\n\n\thooks := make([]PoolHook, len(phm.hooks))\n\tcopy(hooks, phm.hooks)\n\treturn hooks\n}\n\n// Clone creates a copy of the hook manager with the same hooks.\n// This is used for lock-free atomic updates of the hook manager.\nfunc (phm *PoolHookManager) Clone() *PoolHookManager {\n\tphm.hooksMu.RLock()\n\tdefer phm.hooksMu.RUnlock()\n\n\tnewManager := &PoolHookManager{\n\t\thooks: make([]PoolHook, len(phm.hooks)),\n\t}\n\tcopy(newManager.hooks, phm.hooks)\n\treturn newManager\n}\n"
  },
  {
    "path": "internal/pool/hooks_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n)\n\n// TestHook for testing hook functionality\ntype TestHook struct {\n\tOnGetCalled    int\n\tOnPutCalled    int\n\tOnRemoveCalled int\n\tGetError       error\n\tPutError       error\n\tShouldPool     bool\n\tShouldRemove   bool\n\tShouldAccept   bool\n}\n\nfunc (th *TestHook) OnGet(ctx context.Context, conn *Conn, isNewConn bool) (bool, error) {\n\tth.OnGetCalled++\n\treturn th.ShouldAccept, th.GetError\n}\n\nfunc (th *TestHook) OnPut(ctx context.Context, conn *Conn) (shouldPool bool, shouldRemove bool, err error) {\n\tth.OnPutCalled++\n\treturn th.ShouldPool, th.ShouldRemove, th.PutError\n}\n\nfunc (th *TestHook) OnRemove(ctx context.Context, conn *Conn, reason error) {\n\tth.OnRemoveCalled++\n}\n\nfunc TestPoolHookManager(t *testing.T) {\n\tmanager := NewPoolHookManager()\n\n\t// Test initial state\n\tif manager.GetHookCount() != 0 {\n\t\tt.Errorf(\"Expected 0 hooks initially, got %d\", manager.GetHookCount())\n\t}\n\n\t// Add hooks\n\thook1 := &TestHook{ShouldPool: true, ShouldAccept: true}\n\thook2 := &TestHook{ShouldPool: true, ShouldAccept: true}\n\n\tmanager.AddHook(hook1)\n\tmanager.AddHook(hook2)\n\n\tif manager.GetHookCount() != 2 {\n\t\tt.Errorf(\"Expected 2 hooks after adding, got %d\", manager.GetHookCount())\n\t}\n\n\t// Test ProcessOnGet\n\tctx := context.Background()\n\tconn := &Conn{} // Mock connection\n\n\taccept, err := manager.ProcessOnGet(ctx, conn, false)\n\tif err != nil {\n\t\tt.Errorf(\"ProcessOnGet should not error: %v\", err)\n\t}\n\tif !accept {\n\t\tt.Error(\"Expected accept to be true\")\n\t}\n\n\tif hook1.OnGetCalled != 1 {\n\t\tt.Errorf(\"Expected hook1.OnGetCalled to be 1, got %d\", hook1.OnGetCalled)\n\t}\n\n\tif hook2.OnGetCalled != 1 {\n\t\tt.Errorf(\"Expected hook2.OnGetCalled to be 1, got %d\", hook2.OnGetCalled)\n\t}\n\n\t// Test ProcessOnPut\n\tshouldPool, shouldRemove, err := manager.ProcessOnPut(ctx, conn)\n\tif err != nil {\n\t\tt.Errorf(\"ProcessOnPut should not error: %v\", err)\n\t}\n\n\tif !shouldPool {\n\t\tt.Error(\"Expected shouldPool to be true\")\n\t}\n\n\tif shouldRemove {\n\t\tt.Error(\"Expected shouldRemove to be false\")\n\t}\n\n\tif hook1.OnPutCalled != 1 {\n\t\tt.Errorf(\"Expected hook1.OnPutCalled to be 1, got %d\", hook1.OnPutCalled)\n\t}\n\n\tif hook2.OnPutCalled != 1 {\n\t\tt.Errorf(\"Expected hook2.OnPutCalled to be 1, got %d\", hook2.OnPutCalled)\n\t}\n\n\t// Remove a hook\n\tmanager.RemoveHook(hook1)\n\n\tif manager.GetHookCount() != 1 {\n\t\tt.Errorf(\"Expected 1 hook after removing, got %d\", manager.GetHookCount())\n\t}\n}\n\nfunc TestHookErrorHandling(t *testing.T) {\n\tmanager := NewPoolHookManager()\n\n\t// Hook that returns error on Get\n\terrorHook := &TestHook{\n\t\tGetError:     errors.New(\"test error\"),\n\t\tShouldPool:   true,\n\t\tShouldAccept: true,\n\t}\n\n\tnormalHook := &TestHook{ShouldPool: true, ShouldAccept: true}\n\n\tmanager.AddHook(errorHook)\n\tmanager.AddHook(normalHook)\n\n\tctx := context.Background()\n\tconn := &Conn{}\n\n\t// Test that error stops processing\n\taccept, err := manager.ProcessOnGet(ctx, conn, false)\n\tif err == nil {\n\t\tt.Error(\"Expected error from ProcessOnGet\")\n\t}\n\tif accept {\n\t\tt.Error(\"Expected accept to be false\")\n\t}\n\n\tif errorHook.OnGetCalled != 1 {\n\t\tt.Errorf(\"Expected errorHook.OnGetCalled to be 1, got %d\", errorHook.OnGetCalled)\n\t}\n\n\t// normalHook should not be called due to error\n\tif normalHook.OnGetCalled != 0 {\n\t\tt.Errorf(\"Expected normalHook.OnGetCalled to be 0, got %d\", normalHook.OnGetCalled)\n\t}\n}\n\nfunc TestHookShouldRemove(t *testing.T) {\n\tmanager := NewPoolHookManager()\n\n\t// Hook that says to remove connection\n\tremoveHook := &TestHook{\n\t\tShouldPool:   false,\n\t\tShouldRemove: true,\n\t\tShouldAccept: true,\n\t}\n\n\tnormalHook := &TestHook{ShouldPool: true, ShouldAccept: true}\n\n\tmanager.AddHook(removeHook)\n\tmanager.AddHook(normalHook)\n\n\tctx := context.Background()\n\tconn := &Conn{}\n\n\tshouldPool, shouldRemove, err := manager.ProcessOnPut(ctx, conn)\n\tif err != nil {\n\t\tt.Errorf(\"ProcessOnPut should not error: %v\", err)\n\t}\n\n\tif shouldPool {\n\t\tt.Error(\"Expected shouldPool to be false\")\n\t}\n\n\tif !shouldRemove {\n\t\tt.Error(\"Expected shouldRemove to be true\")\n\t}\n\n\tif removeHook.OnPutCalled != 1 {\n\t\tt.Errorf(\"Expected removeHook.OnPutCalled to be 1, got %d\", removeHook.OnPutCalled)\n\t}\n\n\t// normalHook should not be called due to early return\n\tif normalHook.OnPutCalled != 0 {\n\t\tt.Errorf(\"Expected normalHook.OnPutCalled to be 0, got %d\", normalHook.OnPutCalled)\n\t}\n}\n\nfunc TestPoolWithHooks(t *testing.T) {\n\t// Create a pool with hooks\n\thookManager := NewPoolHookManager()\n\ttestHook := &TestHook{ShouldPool: true, ShouldAccept: true}\n\thookManager.AddHook(testHook)\n\n\topt := &Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\treturn &net.TCPConn{}, nil // Mock connection\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        time.Second,\n\t}\n\n\tpool := NewConnPool(opt)\n\tdefer pool.Close()\n\n\t// Add hook to pool after creation\n\tpool.AddPoolHook(testHook)\n\n\t// Verify hooks are initialized\n\tmanager := pool.hookManager.Load()\n\tif manager == nil {\n\t\tt.Error(\"Expected hookManager to be initialized\")\n\t}\n\n\tif manager.GetHookCount() != 1 {\n\t\tt.Errorf(\"Expected 1 hook in pool, got %d\", manager.GetHookCount())\n\t}\n\n\t// Test adding hook to pool\n\tadditionalHook := &TestHook{ShouldPool: true, ShouldAccept: true}\n\tpool.AddPoolHook(additionalHook)\n\n\tmanager = pool.hookManager.Load()\n\tif manager.GetHookCount() != 2 {\n\t\tt.Errorf(\"Expected 2 hooks after adding, got %d\", manager.GetHookCount())\n\t}\n\n\t// Test removing hook from pool\n\tpool.RemovePoolHook(additionalHook)\n\n\tmanager = pool.hookManager.Load()\n\tif manager.GetHookCount() != 1 {\n\t\tt.Errorf(\"Expected 1 hook after removing, got %d\", manager.GetHookCount())\n\t}\n}\n"
  },
  {
    "path": "internal/pool/main_test.go",
    "content": "package pool_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nfunc TestGinkgoSuite(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"pool\")\n}\n\nfunc perform(n int, cbs ...func(int)) {\n\tvar wg sync.WaitGroup\n\tfor _, cb := range cbs {\n\t\tfor i := 0; i < n; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func(cb func(int), i int) {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\tcb(i)\n\t\t\t}(cb, i)\n\t\t}\n\t}\n\twg.Wait()\n}\n\nfunc dummyDialer(context.Context) (net.Conn, error) {\n\treturn newDummyConn(), nil\n}\n\nfunc newDummyConn() net.Conn {\n\treturn &dummyConn{\n\t\trawConn: new(dummyRawConn),\n\t}\n}\n\nvar (\n\t_ net.Conn     = (*dummyConn)(nil)\n\t_ syscall.Conn = (*dummyConn)(nil)\n)\n\ntype dummyConn struct {\n\trawConn *dummyRawConn\n}\n\nfunc (d *dummyConn) SyscallConn() (syscall.RawConn, error) {\n\treturn d.rawConn, nil\n}\n\nvar errDummy = fmt.Errorf(\"dummyConn err\")\n\nfunc (d *dummyConn) Read(b []byte) (n int, err error) {\n\treturn 0, errDummy\n}\n\nfunc (d *dummyConn) Write(b []byte) (n int, err error) {\n\treturn 0, errDummy\n}\n\nfunc (d *dummyConn) Close() error {\n\td.rawConn.Close()\n\treturn nil\n}\n\nfunc (d *dummyConn) LocalAddr() net.Addr {\n\treturn &net.TCPAddr{}\n}\n\nfunc (d *dummyConn) RemoteAddr() net.Addr {\n\treturn &net.TCPAddr{}\n}\n\nfunc (d *dummyConn) SetDeadline(t time.Time) error {\n\treturn nil\n}\n\nfunc (d *dummyConn) SetReadDeadline(t time.Time) error {\n\treturn nil\n}\n\nfunc (d *dummyConn) SetWriteDeadline(t time.Time) error {\n\treturn nil\n}\n\nvar _ syscall.RawConn = (*dummyRawConn)(nil)\n\ntype dummyRawConn struct {\n\tmu     sync.Mutex\n\tclosed bool\n}\n\nfunc (d *dummyRawConn) Control(f func(fd uintptr)) error {\n\treturn nil\n}\n\nfunc (d *dummyRawConn) Read(f func(fd uintptr) (done bool)) error {\n\td.mu.Lock()\n\tdefer d.mu.Unlock()\n\tif d.closed {\n\t\treturn fmt.Errorf(\"dummyRawConn closed\")\n\t}\n\treturn nil\n}\n\nfunc (d *dummyRawConn) Write(f func(fd uintptr) (done bool)) error {\n\treturn nil\n}\n\nfunc (d *dummyRawConn) Close() {\n\td.mu.Lock()\n\td.closed = true\n\td.mu.Unlock()\n}\n"
  },
  {
    "path": "internal/pool/pool.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/rand\"\n)\n\n// Connection close reason constants for metrics.\n// These are used as the \"reason\" parameter in CloseConn() calls.\nconst (\n\t// CloseReasonStale indicates the connection was closed because it exceeded\n\t// the idle timeout or max lifetime.\n\tCloseReasonStale = \"stale\"\n\n\t// CloseReasonHookError indicates the connection was closed due to an error\n\t// in a pool hook (OnGet or OnPut).\n\tCloseReasonHookError = \"hook_error\"\n\n\t// CloseReasonAuthError indicates the connection was closed due to an\n\t// authentication error during re-authentication.\n\tCloseReasonAuthError = \"auth_error\"\n\n\t// CloseReasonTest is used in tests when closing connections.\n\tCloseReasonTest = \"test\"\n)\n\n// Metric state constants for connection state tracking.\n// These represent the logical state of a connection from a metrics perspective,\n// not the internal state machine state (ConnState).\nconst (\n\t// MetricStateIdle indicates the connection is idle in the pool,\n\t// ready to be acquired.\n\tMetricStateIdle = \"idle\"\n\n\t// MetricStateUsed indicates the connection is currently being used\n\t// by a client operation.\n\tMetricStateUsed = \"used\"\n)\n\nvar (\n\t// ErrClosed performs any operation on the closed client will return this error.\n\tErrClosed = errors.New(\"redis: client is closed\")\n\n\t// ErrPoolExhausted is returned from a pool connection method\n\t// when the maximum number of database connections in the pool has been reached.\n\tErrPoolExhausted = errors.New(\"redis: connection pool exhausted\")\n\n\t// ErrPoolTimeout timed out waiting to get a connection from the connection pool.\n\tErrPoolTimeout = errors.New(\"redis: connection pool timeout\")\n\n\t// ErrConnUnusableTimeout is returned when a connection is not usable and we timed out trying to mark it as unusable.\n\tErrConnUnusableTimeout = errors.New(\"redis: timed out trying to mark connection as unusable\")\n\n\t// errHookRequestedRemoval is returned when a hook requests connection removal.\n\terrHookRequestedRemoval = errors.New(\"hook requested removal\")\n\n\t// errConnNotPooled is returned when trying to return a non-pooled connection to the pool.\n\terrConnNotPooled = errors.New(\"connection not pooled\")\n\t// metricCallbackMu protects all global metric callback functions for thread-safe access.\n\tmetricCallbackMu sync.RWMutex\n\n\t// Global metric callbacks for connection state changes\n\tmetricConnectionStateChangeCallback func(ctx context.Context, cn *Conn, fromState, toState string)\n\n\t// Global metric callback for connection creation time\n\tmetricConnectionCreateTimeCallback func(ctx context.Context, duration time.Duration, cn *Conn)\n\n\t// Global metric callback for connection relaxed timeout changes\n\t// Parameters: ctx, delta (+1/-1), cn, poolName, notificationType\n\tmetricConnectionRelaxedTimeoutCallback func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string)\n\n\t// Global metric callback for connection handoff\n\t// Parameters: ctx, cn, poolName\n\tmetricConnectionHandoffCallback func(ctx context.Context, cn *Conn, poolName string)\n\n\t// Global metric callback for error tracking\n\t// Parameters: ctx, errorType, cn, statusCode, isInternal, retryAttempts\n\tmetricErrorCallback func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int)\n\n\t// Global metric callback for maintenance notifications\n\t// Parameters: ctx, cn, notificationType\n\tmetricMaintenanceNotificationCallback func(ctx context.Context, cn *Conn, notificationType string)\n\n\t// Global metric callback for connection wait time\n\t// Parameters: ctx, duration, cn\n\tmetricConnectionWaitTimeCallback func(ctx context.Context, duration time.Duration, cn *Conn)\n\n\t// Global metric callback for connection timeouts\n\t// Parameters: ctx, cn, timeoutType\n\tmetricConnectionTimeoutCallback func(ctx context.Context, cn *Conn, timeoutType string)\n\n\t// Global metric callback for connection closed\n\t// Parameters: ctx, cn, reason, err\n\tmetricConnectionClosedCallback func(ctx context.Context, cn *Conn, reason string, err error)\n\n\t// errPanicInDial is returned when a panic occurs in the dial function.\n\terrPanicInQueuedNewConn = errors.New(\"panic in queuedNewConn\")\n\n\t// popAttempts is the maximum number of attempts to find a usable connection\n\t// when popping from the idle connection pool. This handles cases where connections\n\t// are temporarily marked as unusable (e.g., during maintenanceNotifications upgrades or network issues).\n\t// Value of 50 provides sufficient resilience without excessive overhead.\n\t// This is capped by the idle connection count, so we won't loop excessively.\n\tpopAttempts = 50\n\n\t// getAttempts is the maximum number of attempts to get a connection that passes\n\t// hook validation (e.g., maintenanceNotifications upgrade hooks). This protects against race conditions\n\t// where hooks might temporarily reject connections during cluster transitions.\n\t// Value of 3 balances resilience with performance - most hook rejections resolve quickly.\n\tgetAttempts = 3\n\n\tminTime      = time.Unix(-2208988800, 0) // Jan 1, 1900\n\tmaxTime      = minTime.Add(1<<63 - 1)\n\tnoExpiration = maxTime\n)\n\n// MetricCallbacks holds all metric callback functions.\n// Use SetAllMetricCallbacks to register all callbacks atomically.\ntype MetricCallbacks struct {\n\t// ConnectionCreateTime is called when a new connection is created\n\tConnectionCreateTime func(ctx context.Context, duration time.Duration, cn *Conn)\n\n\t// ConnectionRelaxedTimeout is called when connection timeout is relaxed/unrelaxed\n\t// delta: +1 for relaxed, -1 for unrelaxed\n\tConnectionRelaxedTimeout func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string)\n\n\t// ConnectionHandoff is called when a connection is handed off to another node\n\tConnectionHandoff func(ctx context.Context, cn *Conn, poolName string)\n\n\t// Error is called when an error occurs\n\tError func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int)\n\n\t// MaintenanceNotification is called when a maintenance notification is received\n\tMaintenanceNotification func(ctx context.Context, cn *Conn, notificationType string)\n\n\t// ConnectionWaitTime is called to record time spent waiting for a connection\n\tConnectionWaitTime func(ctx context.Context, duration time.Duration, cn *Conn)\n\n\t// ConnectionClosed is called when a connection is closed\n\tConnectionClosed func(ctx context.Context, cn *Conn, reason string, err error)\n}\n\n// SetAllMetricCallbacks sets all metric callbacks atomically.\n// Pass nil to clear all callbacks (disable metrics).\n// This ensures all callbacks are set together under a single lock,\n// preventing inconsistent state during registration.\n//\n// Note on thread safety: After returning, there is a small window where\n// concurrent getMetric* calls may return the old callback value. This is\n// acceptable for metrics - at most one event may go to the old recorder\n// or be missed during the transition. The callbacks themselves are immutable\n// function pointers, so calling an \"old\" callback is safe.\nfunc SetAllMetricCallbacks(callbacks *MetricCallbacks) {\n\tmetricCallbackMu.Lock()\n\tdefer metricCallbackMu.Unlock()\n\n\tif callbacks == nil {\n\t\tmetricConnectionCreateTimeCallback = nil\n\t\tmetricConnectionRelaxedTimeoutCallback = nil\n\t\tmetricConnectionHandoffCallback = nil\n\t\tmetricErrorCallback = nil\n\t\tmetricMaintenanceNotificationCallback = nil\n\t\tmetricConnectionWaitTimeCallback = nil\n\t\tmetricConnectionClosedCallback = nil\n\t\treturn\n\t}\n\n\tmetricConnectionCreateTimeCallback = callbacks.ConnectionCreateTime\n\tmetricConnectionRelaxedTimeoutCallback = callbacks.ConnectionRelaxedTimeout\n\tmetricConnectionHandoffCallback = callbacks.ConnectionHandoff\n\tmetricErrorCallback = callbacks.Error\n\tmetricMaintenanceNotificationCallback = callbacks.MaintenanceNotification\n\tmetricConnectionWaitTimeCallback = callbacks.ConnectionWaitTime\n\tmetricConnectionClosedCallback = callbacks.ConnectionClosed\n}\n\n// getMetricConnectionStateChangeCallback returns the metric callback for connection state changes.\nfunc getMetricConnectionStateChangeCallback() func(ctx context.Context, cn *Conn, fromState, toState string) {\n\tmetricCallbackMu.RLock()\n\tcb := metricConnectionStateChangeCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\n// GetMetricConnectionCreateTimeCallback returns the metric callback for connection creation time.\nfunc GetMetricConnectionCreateTimeCallback() func(ctx context.Context, duration time.Duration, cn *Conn) {\n\tmetricCallbackMu.RLock()\n\tcb := metricConnectionCreateTimeCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\n// GetMetricConnectionRelaxedTimeoutCallback returns the metric callback for connection relaxed timeout changes.\n// This is used by maintnotifications to record relaxed timeout metrics.\nfunc GetMetricConnectionRelaxedTimeoutCallback() func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string) {\n\tmetricCallbackMu.RLock()\n\tcb := metricConnectionRelaxedTimeoutCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\n// GetMetricConnectionHandoffCallback returns the metric callback for connection handoffs.\n// This is used by maintnotifications to record handoff metrics.\nfunc GetMetricConnectionHandoffCallback() func(ctx context.Context, cn *Conn, poolName string) {\n\tmetricCallbackMu.RLock()\n\tcb := metricConnectionHandoffCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\n// GetMetricErrorCallback returns the metric callback for error tracking.\n// This is used by cluster and client code to record error metrics.\nfunc GetMetricErrorCallback() func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int) {\n\tmetricCallbackMu.RLock()\n\tcb := metricErrorCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\n// GetMetricMaintenanceNotificationCallback returns the metric callback for maintenance notifications.\n// This is used by maintnotifications to record notification metrics.\nfunc GetMetricMaintenanceNotificationCallback() func(ctx context.Context, cn *Conn, notificationType string) {\n\tmetricCallbackMu.RLock()\n\tcb := metricMaintenanceNotificationCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\nfunc getMetricConnectionWaitTimeCallback() func(ctx context.Context, duration time.Duration, cn *Conn) {\n\tmetricCallbackMu.RLock()\n\tcb := metricConnectionWaitTimeCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\nfunc getMetricConnectionTimeoutCallback() func(ctx context.Context, cn *Conn, timeoutType string) {\n\tmetricCallbackMu.RLock()\n\tcb := metricConnectionTimeoutCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\nfunc getMetricConnectionClosedCallback() func(ctx context.Context, cn *Conn, reason string, err error) {\n\tmetricCallbackMu.RLock()\n\tcb := metricConnectionClosedCallback\n\tmetricCallbackMu.RUnlock()\n\treturn cb\n}\n\n// Stats contains pool state information and accumulated stats.\ntype Stats struct {\n\tHits           uint32 // number of times free connection was found in the pool\n\tMisses         uint32 // number of times free connection was NOT found in the pool\n\tTimeouts       uint32 // number of times a wait timeout occurred\n\tWaitCount      uint32 // number of times a connection was waited\n\tUnusable       uint32 // number of times a connection was found to be unusable\n\tWaitDurationNs int64  // total time spent for waiting a connection in nanoseconds\n\n\tTotalConns      uint32 // number of total connections in the pool\n\tIdleConns       uint32 // number of idle connections in the pool\n\tStaleConns      uint32 // number of stale connections removed from the pool\n\tPendingRequests uint32 // number of pending requests waiting for a connection\n\n\tPubSubStats PubSubStats\n}\n\ntype Pooler interface {\n\tNewConn(context.Context) (*Conn, error)\n\tCloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error\n\n\tGet(context.Context) (*Conn, error)\n\tPut(context.Context, *Conn)\n\tRemove(context.Context, *Conn, error)\n\n\tLen() int\n\tIdleLen() int\n\tStats() *Stats\n\n\t// Size returns the maximum pool size (capacity).\n\t// This is used by the streaming credentials manager to size the re-auth worker pool.\n\tSize() int\n\n\tAddPoolHook(hook PoolHook)\n\tRemovePoolHook(hook PoolHook)\n\n\t// RemoveWithoutTurn removes a connection from the pool without freeing a turn.\n\t// This should be used when removing a connection from a context that didn't acquire\n\t// a turn via Get() (e.g., background workers, cleanup tasks).\n\t// For normal removal after Get(), use Remove() instead.\n\tRemoveWithoutTurn(context.Context, *Conn, error)\n\n\tClose() error\n}\n\ntype Options struct {\n\tDialer          func(context.Context) (net.Conn, error)\n\tReadBufferSize  int\n\tWriteBufferSize int\n\n\tPoolFIFO                 bool\n\tPoolSize                 int32\n\tMaxConcurrentDials       int\n\tDialTimeout              time.Duration\n\tPoolTimeout              time.Duration\n\tMinIdleConns             int32\n\tMaxIdleConns             int32\n\tMaxActiveConns           int32\n\tConnMaxIdleTime          time.Duration\n\tConnMaxLifetime          time.Duration\n\tConnMaxLifetimeJitter    time.Duration\n\tPushNotificationsEnabled bool\n\n\t// DialerRetries is the maximum number of retry attempts when dialing fails.\n\t// Default: 5\n\tDialerRetries int\n\n\t// DialerRetryTimeout is the backoff duration between retry attempts.\n\t// Default: 100ms\n\tDialerRetryTimeout time.Duration\n\n\t// DialerRetryBackoff controls the delay between dial retry attempts.\n\t// If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms).\n\tDialerRetryBackoff func(attempt int) time.Duration\n\n\t// Name is a unique identifier for this pool, used in metrics.\n\t// Format: addr_uniqueID (e.g., \"localhost:6379_a1b2c3d4\")\n\tName string\n}\n\ntype lastDialErrorWrap struct {\n\terr error\n}\n\ntype ConnPool struct {\n\tcfg *Options\n\n\tdialErrorsNum uint32 // atomic\n\tlastDialError atomic.Value\n\n\tqueue           chan struct{}\n\tdialsInProgress chan struct{}\n\tdialsQueue      *wantConnQueue\n\t// Fast semaphore for connection limiting with eventual fairness\n\t// Uses fast path optimization to avoid timer allocation when tokens are available\n\tsemaphore *internal.FastSemaphore\n\n\tconnsMu   sync.Mutex\n\tconns     map[uint64]*Conn\n\tidleConns []*Conn\n\n\tpoolSize            atomic.Int32\n\tidleConnsLen        atomic.Int32\n\tidleCheckInProgress atomic.Bool\n\tidleCheckNeeded     atomic.Bool\n\n\tstats          Stats\n\twaitDurationNs atomic.Int64\n\n\t_closed uint32 // atomic\n\n\t// Pool hooks manager for flexible connection processing\n\t// Using atomic.Pointer for lock-free reads in hot paths (Get/Put)\n\thookManager atomic.Pointer[PoolHookManager]\n}\n\nvar _ Pooler = (*ConnPool)(nil)\n\nfunc NewConnPool(opt *Options) *ConnPool {\n\tp := &ConnPool{\n\t\tcfg:             opt,\n\t\tsemaphore:       internal.NewFastSemaphore(opt.PoolSize),\n\t\tqueue:           make(chan struct{}, opt.PoolSize),\n\t\tconns:           make(map[uint64]*Conn),\n\t\tdialsInProgress: make(chan struct{}, opt.MaxConcurrentDials),\n\t\tdialsQueue:      newWantConnQueue(),\n\t\tidleConns:       make([]*Conn, 0, opt.PoolSize),\n\t}\n\n\t// Only create MinIdleConns if explicitly requested (> 0)\n\t// This avoids creating connections during pool initialization for tests\n\tif opt.MinIdleConns > 0 {\n\t\tp.connsMu.Lock()\n\t\tp.checkMinIdleConns()\n\t\tp.connsMu.Unlock()\n\t}\n\n\treturn p\n}\n\n// initializeHooks sets up the pool hooks system.\nfunc (p *ConnPool) initializeHooks() {\n\tmanager := NewPoolHookManager()\n\tp.hookManager.Store(manager)\n}\n\n// AddPoolHook adds a pool hook to the pool.\nfunc (p *ConnPool) AddPoolHook(hook PoolHook) {\n\t// Lock-free read of current manager\n\tmanager := p.hookManager.Load()\n\tif manager == nil {\n\t\tp.initializeHooks()\n\t\tmanager = p.hookManager.Load()\n\t}\n\n\t// Create new manager with added hook\n\tnewManager := manager.Clone()\n\tnewManager.AddHook(hook)\n\n\t// Atomically swap to new manager\n\tp.hookManager.Store(newManager)\n}\n\n// RemovePoolHook removes a pool hook from the pool.\nfunc (p *ConnPool) RemovePoolHook(hook PoolHook) {\n\tmanager := p.hookManager.Load()\n\tif manager != nil {\n\t\t// Create new manager with removed hook\n\t\tnewManager := manager.Clone()\n\t\tnewManager.RemoveHook(hook)\n\n\t\t// Atomically swap to new manager\n\t\tp.hookManager.Store(newManager)\n\t}\n}\n\nfunc (p *ConnPool) checkMinIdleConns() {\n\t// If a check is already in progress, mark that we need another check and return\n\tif !p.idleCheckInProgress.CompareAndSwap(false, true) {\n\t\tp.idleCheckNeeded.Store(true)\n\t\treturn\n\t}\n\n\tif p.cfg.MinIdleConns == 0 {\n\t\tp.idleCheckInProgress.Store(false)\n\t\treturn\n\t}\n\n\t// Keep checking until no more checks are needed\n\t// This handles the case where multiple Remove() calls happen concurrently\n\tfor {\n\t\t// Clear the \"check needed\" flag before we start\n\t\tp.idleCheckNeeded.Store(false)\n\n\t\t// Only create idle connections if we haven't reached the total pool size limit\n\t\t// MinIdleConns should be a subset of PoolSize, not additional connections\n\t\tfor p.poolSize.Load() < p.cfg.PoolSize && p.idleConnsLen.Load() < p.cfg.MinIdleConns {\n\t\t\t// Try to acquire a semaphore token\n\t\t\tif !p.semaphore.TryAcquire() {\n\t\t\t\t// Semaphore is full, can't create more connections right now\n\t\t\t\t// Break out of inner loop to check if we need to retry\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tp.poolSize.Add(1)\n\t\t\tp.idleConnsLen.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif err := recover(); err != nil {\n\t\t\t\t\t\tp.poolSize.Add(-1)\n\t\t\t\t\t\tp.idleConnsLen.Add(-1)\n\n\t\t\t\t\t\tp.freeTurn()\n\t\t\t\t\t\tinternal.Logger.Printf(context.Background(), \"addIdleConn panic: %+v\", err)\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\terr := p.addIdleConn()\n\t\t\t\tif err != nil && err != ErrClosed {\n\t\t\t\t\tp.poolSize.Add(-1)\n\t\t\t\t\tp.idleConnsLen.Add(-1)\n\t\t\t\t}\n\t\t\t\tp.freeTurn()\n\t\t\t}()\n\t\t}\n\n\t\t// If no one requested another check while we were working, we're done\n\t\tif !p.idleCheckNeeded.Load() {\n\t\t\tp.idleCheckInProgress.Store(false)\n\t\t\treturn\n\t\t}\n\n\t\t// Otherwise, loop again to handle the new requests\n\t}\n}\n\nfunc (p *ConnPool) addIdleConn() error {\n\t// Do not apply DialTimeout via context here; dialConn applies DialTimeout per attempt.\n\tcn, err := p.dialConn(context.Background(), true)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// NOTE: Connection is in CREATED state and will be initialized by redis.go:initConn()\n\t// when first acquired from the pool. Do NOT transition to IDLE here - that happens\n\t// after initialization completes.\n\n\tp.connsMu.Lock()\n\tdefer p.connsMu.Unlock()\n\n\t// It is not allowed to add new connections to the closed connection pool.\n\tif p.closed() {\n\t\t_ = cn.Close()\n\t\treturn ErrClosed\n\t}\n\n\tp.conns[cn.GetID()] = cn\n\tp.idleConns = append(p.idleConns, cn)\n\treturn nil\n}\n\n// NewConn creates a new connection and returns it to the user.\n// This will still obey MaxActiveConns but will not include it in the pool and won't increase the pool size.\n//\n// NOTE: If you directly get a connection from the pool, it won't be pooled and won't support maintnotifications upgrades.\nfunc (p *ConnPool) NewConn(ctx context.Context) (*Conn, error) {\n\treturn p.newConn(ctx, false)\n}\n\nfunc (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {\n\tif p.closed() {\n\t\treturn nil, ErrClosed\n\t}\n\n\tif p.cfg.MaxActiveConns > 0 && p.poolSize.Load() >= p.cfg.MaxActiveConns {\n\t\treturn nil, ErrPoolExhausted\n\t}\n\n\t// Protect against nil context due to race condition in queuedNewConn\n\t// where the context can be set to nil after timeout/cancellation\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\n\t// Do not apply DialTimeout via context here; dialConn applies DialTimeout per attempt.\n\t// We still propagate ctx so callers can cancel explicitly.\n\tcn, err := p.dialConn(ctx, pooled)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// NOTE: Connection is in CREATED state and will be initialized by redis.go:initConn()\n\t// when first used. Do NOT transition to IDLE here - that happens after initialization completes.\n\t// The state machine flow is: CREATED → INITIALIZING (in initConn) → IDLE (after init success)\n\n\tif p.cfg.MaxActiveConns > 0 && p.poolSize.Load() > p.cfg.MaxActiveConns {\n\t\t_ = cn.Close()\n\t\treturn nil, ErrPoolExhausted\n\t}\n\n\tp.connsMu.Lock()\n\tdefer p.connsMu.Unlock()\n\tif p.closed() {\n\t\t_ = cn.Close()\n\t\treturn nil, ErrClosed\n\t}\n\t// Check if pool was closed while we were waiting for the lock\n\tif p.conns == nil {\n\t\tp.conns = make(map[uint64]*Conn)\n\t}\n\tp.conns[cn.GetID()] = cn\n\n\tif pooled {\n\t\t// If pool is full remove the cn on next Put.\n\t\tcurrentPoolSize := p.poolSize.Load()\n\t\tif currentPoolSize >= p.cfg.PoolSize {\n\t\t\tcn.pooled = false\n\t\t} else {\n\t\t\tp.poolSize.Add(1)\n\t\t}\n\t}\n\n\t// Notify metrics: new connection created and idle\n\tif cb := getMetricConnectionStateChangeCallback(); cb != nil {\n\t\tcb(ctx, cn, \"\", MetricStateIdle)\n\t}\n\n\treturn cn, nil\n}\n\nfunc (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {\n\tif p.closed() {\n\t\treturn nil, ErrClosed\n\t}\n\n\tif atomic.LoadUint32(&p.dialErrorsNum) >= uint32(p.cfg.PoolSize) {\n\t\treturn nil, p.getLastDialError()\n\t}\n\n\t// Record dial start time for connection creation metric\n\t// This will be used after handshake completes in redis.go _getConn()\n\t// Only call time.Now() if callback is registered to avoid overhead\n\tvar dialStartNs int64\n\tif GetMetricConnectionCreateTimeCallback() != nil {\n\t\tdialStartNs = time.Now().UnixNano()\n\t}\n\n\t// Retry dialing with backoff\n\t// Dial timeout is applied per attempt (so retries/backoff don't eat into the next\n\t// attempt's dial budget), while still honoring caller cancellation via ctx.\n\tmaxRetries := p.cfg.DialerRetries\n\tif maxRetries <= 0 {\n\t\tmaxRetries = 5 // Default value\n\t}\n\n\tvar lastErr error\n\tshouldLoop := true\n\t// when the timeout is reached, we should stop retrying\n\t// but keep the lastErr to return to the caller\n\t// instead of a generic context deadline exceeded error\n\tattempt := 0\n\tfor attempt = 0; (attempt < maxRetries) && shouldLoop; attempt++ {\n\t\tattemptCtx := ctx\n\t\tvar cancel context.CancelFunc\n\t\tif p.cfg.DialTimeout > 0 {\n\t\t\t// Apply DialTimeout per attempt, but never extend an existing earlier deadline.\n\t\t\tif deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > p.cfg.DialTimeout {\n\t\t\t\tattemptCtx, cancel = context.WithTimeout(ctx, p.cfg.DialTimeout)\n\t\t\t}\n\t\t}\n\n\t\tnetConn, err := p.cfg.Dialer(attemptCtx)\n\t\tif cancel != nil {\n\t\t\tcancel()\n\t\t}\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\t// Add backoff delay for retry attempts\n\t\t\t// (not for the first attempt, do at least one)\n\t\t\t// Do not sleep after the last attempt.\n\t\t\tif attempt+1 < maxRetries {\n\t\t\t\tbackoffDuration := p.dialRetryBackoff(attempt)\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tshouldLoop = false\n\t\t\t\tcase <-time.After(backoffDuration):\n\t\t\t\t\t// Continue with retry\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tcn := NewConnWithBufferSize(netConn, p.cfg.ReadBufferSize, p.cfg.WriteBufferSize)\n\t\tcn.pooled = pooled\n\t\t// Store dial start time only if we recorded it\n\t\tif dialStartNs > 0 {\n\t\t\tcn.dialStartNs.Store(dialStartNs)\n\t\t}\n\t\tcn.expiresAt = p.calcConnExpiresAt()\n\t\t// Set pool name for metrics\n\t\tcn.SetPoolName(p.cfg.Name)\n\n\t\treturn cn, nil\n\t}\n\n\tinternal.Logger.Printf(ctx, \"redis: connection pool: failed to dial after %d attempts: %v\", attempt, lastErr)\n\t// All retries failed - handle error tracking\n\tp.setLastDialError(lastErr)\n\tif atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.cfg.PoolSize) {\n\t\tgo p.tryDial()\n\t}\n\treturn nil, lastErr\n}\n\nfunc (p *ConnPool) dialRetryBackoff(attempt int) time.Duration {\n\tif p.cfg.DialerRetryBackoff != nil {\n\t\td := p.cfg.DialerRetryBackoff(attempt)\n\t\tif d < 0 {\n\t\t\treturn 0\n\t\t}\n\t\treturn d\n\t}\n\n\tbase := p.cfg.DialerRetryTimeout\n\tif base <= 0 {\n\t\tbase = 100 * time.Millisecond\n\t}\n\treturn base\n}\n\n// calcConnExpiresAt calculates the expiration time for a connection.\n// It applies random jitter to prevent all connections from expiring simultaneously,\n// avoiding the \"thundering herd\" problem where all connections expire at once.\n// Returns noExpiration if ConnMaxLifetime is not set.\nfunc (p *ConnPool) calcConnExpiresAt() time.Time {\n\tif p.cfg.ConnMaxLifetime <= 0 {\n\t\treturn noExpiration\n\t}\n\n\tif p.cfg.ConnMaxLifetimeJitter <= 0 {\n\t\treturn time.Now().Add(p.cfg.ConnMaxLifetime)\n\t}\n\n\tjitter := p.cfg.ConnMaxLifetimeJitter\n\tjitterRange := jitter.Nanoseconds() * 2\n\tjitterNs := rand.Int63n(jitterRange) - jitter.Nanoseconds()\n\treturn time.Now().Add(p.cfg.ConnMaxLifetime + time.Duration(jitterNs))\n}\n\nfunc (p *ConnPool) tryDial() {\n\tfor {\n\t\tif p.closed() {\n\t\t\treturn\n\t\t}\n\n\t\t// Probe dialing even when dialErrorsNum is saturated. Apply DialTimeout per probe\n\t\t// attempt so custom dialers can't hang indefinitely.\n\t\tctx := context.Background()\n\t\tvar cancel context.CancelFunc\n\t\tif p.cfg.DialTimeout > 0 {\n\t\t\tctx, cancel = context.WithTimeout(ctx, p.cfg.DialTimeout)\n\t\t}\n\n\t\tconn, err := p.cfg.Dialer(ctx)\n\t\tif cancel != nil {\n\t\t\tcancel()\n\t\t}\n\t\tif err != nil {\n\t\t\tp.setLastDialError(err)\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t}\n\n\t\tatomic.StoreUint32(&p.dialErrorsNum, 0)\n\t\t_ = conn.Close()\n\t\treturn\n\t}\n}\n\nfunc (p *ConnPool) setLastDialError(err error) {\n\tp.lastDialError.Store(&lastDialErrorWrap{err: err})\n}\n\nfunc (p *ConnPool) getLastDialError() error {\n\terr, _ := p.lastDialError.Load().(*lastDialErrorWrap)\n\tif err != nil {\n\t\treturn err.err\n\t}\n\treturn nil\n}\n\n// Get returns existed connection from the pool or creates a new one.\nfunc (p *ConnPool) Get(ctx context.Context) (*Conn, error) {\n\treturn p.getConn(ctx)\n}\n\n// getConn returns a connection from the pool.\nfunc (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {\n\tif p.closed() {\n\t\treturn nil, ErrClosed\n\t}\n\n\t// Track pending requests in pool stats\n\t// NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.\n\tatomic.AddUint32(&p.stats.PendingRequests, 1)\n\tdefer func() {\n\t\tif err != nil {\n\t\t\t// Failed to get connection, decrement pending requests\n\t\t\tatomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1\n\t\t}\n\t}()\n\n\t// Track wait time - only call time.Now() if callback is registered\n\tvar waitStart time.Time\n\twaitTimeCallback := getMetricConnectionWaitTimeCallback()\n\tif waitTimeCallback != nil {\n\t\twaitStart = time.Now()\n\t}\n\tif err = p.waitTurn(ctx); err != nil {\n\t\t// Record timeout if applicable\n\t\tif err == ErrPoolTimeout {\n\t\t\tif cb := getMetricConnectionTimeoutCallback(); cb != nil {\n\t\t\t\tcb(ctx, nil, \"pool\")\n\t\t\t}\n\t\t\t// Record general error metric for pool timeout\n\t\t\tif cb := GetMetricErrorCallback(); cb != nil {\n\t\t\t\tcb(ctx, \"POOL_TIMEOUT\", nil, \"POOL_TIMEOUT\", true, 0)\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\t}\n\tvar waitDuration time.Duration\n\tif waitTimeCallback != nil {\n\t\twaitDuration = time.Since(waitStart)\n\t}\n\n\t// Use cached time for health checks (max 50ms staleness is acceptable)\n\tnowNs := getCachedTimeNs()\n\n\t// Lock-free atomic read - no mutex overhead!\n\thookManager := p.hookManager.Load()\n\n\tfor attempts := 0; attempts < getAttempts; attempts++ {\n\n\t\tp.connsMu.Lock()\n\t\tcn, err = p.popIdle()\n\t\tp.connsMu.Unlock()\n\n\t\tif err != nil {\n\t\t\tp.freeTurn()\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif cn == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif !p.isHealthyConn(cn, nowNs) {\n\t\t\t// Connection was popped from idle pool, so fromState is MetricStateIdle\n\t\t\t_ = p.CloseConn(ctx, cn, CloseReasonStale, MetricStateIdle)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Process connection using the hooks system\n\t\t// Combine error and rejection checks to reduce branches\n\t\tif hookManager != nil {\n\t\t\tacceptConn, hookErr := hookManager.ProcessOnGet(ctx, cn, false)\n\t\t\tif hookErr != nil || !acceptConn {\n\t\t\t\tif hookErr != nil {\n\t\t\t\t\tinternal.Logger.Printf(ctx, \"redis: connection pool: failed to process idle connection by hook: %v\", hookErr)\n\t\t\t\t\t// Connection was popped from idle pool, so fromState is MetricStateIdle\n\t\t\t\t\t_ = p.CloseConn(ctx, cn, CloseReasonHookError, MetricStateIdle)\n\t\t\t\t} else {\n\t\t\t\t\tinternal.Logger.Printf(ctx, \"redis: connection pool: conn[%d] rejected by hook, returning to pool\", cn.GetID())\n\t\t\t\t\t// Return connection to pool without freeing the turn that this Get() call holds.\n\t\t\t\t\t// We use putConnWithoutTurn() to run all the Put hooks and logic without freeing a turn.\n\t\t\t\t\tp.putConnWithoutTurn(ctx, cn)\n\t\t\t\t\tcn = nil\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tatomic.AddUint32(&p.stats.Hits, 1)\n\n\t\t// Notify metrics: connection moved from idle to used\n\t\tif cb := getMetricConnectionStateChangeCallback(); cb != nil {\n\t\t\tcb(ctx, cn, MetricStateIdle, MetricStateUsed)\n\t\t}\n\n\t\t// Record wait time (use cached callback from above)\n\t\tif waitTimeCallback != nil {\n\t\t\twaitTimeCallback(ctx, waitDuration, cn)\n\t\t}\n\n\t\t// Decrement pending requests (connection acquired successfully)\n\t\t// NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.\n\t\tatomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1\n\n\t\treturn cn, nil\n\t}\n\n\tatomic.AddUint32(&p.stats.Misses, 1)\n\n\tvar newcn *Conn\n\tnewcn, err = p.queuedNewConn(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Process connection using the hooks system\n\t// This includes the handshake (HELLO/AUTH) via initConn hook\n\tif hookManager != nil {\n\t\tvar acceptConn bool\n\t\tacceptConn, err = hookManager.ProcessOnGet(ctx, newcn, true)\n\t\t// both errors and accept=false mean a hook rejected the connection\n\t\t// this should not happen with a new connection, but we handle it gracefully\n\t\tif err != nil || !acceptConn {\n\t\t\t// Failed to process connection, discard it\n\t\t\tinternal.Logger.Printf(ctx, \"redis: connection pool: failed to process new connection conn[%d] by hook: accept=%v, err=%v\", newcn.GetID(), acceptConn, err)\n\t\t\t// New connection was recorded as \"\" → MetricStateIdle in newConn, so fromState is MetricStateIdle\n\t\t\t_ = p.CloseConn(ctx, newcn, CloseReasonHookError, MetricStateIdle)\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Notify metrics: new connection is created and used\n\tif cb := getMetricConnectionStateChangeCallback(); cb != nil {\n\t\tcb(ctx, newcn, \"\", MetricStateUsed)\n\t}\n\n\t// Record wait time (use cached callback from above)\n\tif waitTimeCallback != nil {\n\t\twaitTimeCallback(ctx, waitDuration, newcn)\n\t}\n\n\t// Decrement pending requests (connection acquired successfully)\n\t// NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.\n\tatomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1\n\n\treturn newcn, nil\n}\n\nfunc (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) {\n\tselect {\n\tcase p.dialsInProgress <- struct{}{}:\n\t\t// Got permission, proceed to create connection\n\tcase <-ctx.Done():\n\t\tp.freeTurn()\n\t\treturn nil, ctx.Err()\n\t}\n\n\t// Don't apply DialTimeout via context here; dialConn applies DialTimeout per attempt.\n\tdialCtx, cancel := context.WithCancel(context.Background())\n\n\tw := &wantConn{\n\t\tctx:       dialCtx,\n\t\tcancelCtx: cancel,\n\t\tresult:    make(chan wantConnResult, 1),\n\t}\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cn := w.cancel(); cn != nil && p.putIdleConn(ctx, cn) {\n\t\t\t\tp.freeTurn()\n\t\t\t}\n\t\t}\n\t}()\n\n\tp.dialsQueue.discardDoneAtFront()\n\tp.dialsQueue.enqueue(w)\n\n\tgo func(w *wantConn) {\n\t\tvar freeTurnCalled bool\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tw.tryDeliver(nil, errPanicInQueuedNewConn)\n\t\t\t\tp.dialsQueue.discardDoneAtFront()\n\t\t\t\tif !freeTurnCalled {\n\t\t\t\t\tp.freeTurn()\n\t\t\t\t}\n\t\t\t\tinternal.Logger.Printf(context.Background(), \"queuedNewConn panic: %+v\", err)\n\t\t\t}\n\t\t}()\n\n\t\tdefer w.cancelCtx()\n\t\tdefer func() { <-p.dialsInProgress }() // Release connection creation permission\n\n\t\tdialCtx := w.getCtxForDial()\n\t\tcn, cnErr := p.newConn(dialCtx, true)\n\t\tif cnErr != nil {\n\t\t\tw.tryDeliver(nil, cnErr) // deliver error to caller, notify connection creation failed\n\t\t\tp.dialsQueue.discardDoneAtFront()\n\t\t\tp.freeTurn()\n\t\t\tfreeTurnCalled = true\n\t\t\treturn\n\t\t}\n\n\t\tdelivered := w.tryDeliver(cn, cnErr)\n\t\tp.dialsQueue.discardDoneAtFront()\n\t\tif !delivered && p.putIdleConn(dialCtx, cn) {\n\t\t\tp.freeTurn()\n\t\t\tfreeTurnCalled = true\n\t\t}\n\t}(w)\n\n\tselect {\n\tcase <-ctx.Done():\n\t\terr = ctx.Err()\n\t\treturn nil, err\n\tcase result := <-w.result:\n\t\terr = result.err\n\t\treturn result.cn, err\n\t}\n}\n\n// putIdleConn puts a connection back to the pool or passes it to the next waiting request.\n//\n// It returns true if the connection was put back to the pool,\n// which means the turn needs to be freed directly by the caller,\n// or false if the connection was passed to the next waiting request,\n// which means the turn will be freed by the waiting goroutine after it returns.\nfunc (p *ConnPool) putIdleConn(ctx context.Context, cn *Conn) bool {\n\tfor {\n\t\tw, ok := p.dialsQueue.dequeue()\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tif w.tryDeliver(cn, nil) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tp.connsMu.Lock()\n\tdefer p.connsMu.Unlock()\n\n\tif p.closed() {\n\t\t_ = cn.Close()\n\t\treturn true\n\t}\n\n\t// poolSize is increased in newConn\n\tp.idleConns = append(p.idleConns, cn)\n\tp.idleConnsLen.Add(1)\n\n\treturn true\n}\n\nfunc (p *ConnPool) waitTurn(ctx context.Context) error {\n\t// Fast path: check context first\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tdefault:\n\t}\n\n\t// Fast path: try to acquire without blocking\n\tif p.semaphore.TryAcquire() {\n\t\treturn nil\n\t}\n\n\t// Slow path: need to wait\n\tstart := time.Now()\n\terr := p.semaphore.Acquire(ctx, p.cfg.PoolTimeout, ErrPoolTimeout)\n\n\tswitch err {\n\tcase nil:\n\t\t// Successfully acquired after waiting\n\t\tp.waitDurationNs.Add(time.Now().UnixNano() - start.UnixNano())\n\t\tatomic.AddUint32(&p.stats.WaitCount, 1)\n\tcase ErrPoolTimeout:\n\t\tatomic.AddUint32(&p.stats.Timeouts, 1)\n\t}\n\n\treturn err\n}\n\nfunc (p *ConnPool) freeTurn() {\n\tp.semaphore.Release()\n}\n\nfunc (p *ConnPool) popIdle() (*Conn, error) {\n\tif p.closed() {\n\t\treturn nil, ErrClosed\n\t}\n\tdefer p.checkMinIdleConns()\n\n\tn := len(p.idleConns)\n\tif n == 0 {\n\t\treturn nil, nil\n\t}\n\n\tvar cn *Conn\n\tattempts := 0\n\n\tmaxAttempts := min(popAttempts, n)\n\tfor attempts < maxAttempts {\n\t\tif len(p.idleConns) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tif p.cfg.PoolFIFO {\n\t\t\tcn = p.idleConns[0]\n\t\t\tcopy(p.idleConns, p.idleConns[1:])\n\t\t\tp.idleConns = p.idleConns[:len(p.idleConns)-1]\n\t\t} else {\n\t\t\tidx := len(p.idleConns) - 1\n\t\t\tcn = p.idleConns[idx]\n\t\t\tp.idleConns = p.idleConns[:idx]\n\t\t}\n\t\tattempts++\n\n\t\t// Hot path optimization: try IDLE → IN_USE or CREATED → IN_USE transition\n\t\t// Using inline TryAcquire() method for better performance (avoids pointer dereference)\n\t\tif cn.TryAcquire() {\n\t\t\t// Successfully acquired the connection\n\t\t\tp.idleConnsLen.Add(-1)\n\t\t\tbreak\n\t\t}\n\n\t\t// Connection is in UNUSABLE, INITIALIZING, or other state - skip it\n\n\t\t// Connection is not in a valid state (might be UNUSABLE for handoff/re-auth, INITIALIZING, etc.)\n\t\t// Put it back in the pool and try the next one\n\t\tif p.cfg.PoolFIFO {\n\t\t\t// FIFO: put at end (will be picked up last since we pop from front)\n\t\t\tp.idleConns = append(p.idleConns, cn)\n\t\t} else {\n\t\t\t// LIFO: put at beginning (will be picked up last since we pop from end)\n\t\t\tp.idleConns = append([]*Conn{cn}, p.idleConns...)\n\t\t}\n\t\tcn = nil\n\t}\n\n\t// If we exhausted all attempts without finding a usable connection, return nil\n\tif attempts > 1 && attempts >= maxAttempts && int32(attempts) >= p.poolSize.Load() {\n\t\tinternal.Logger.Printf(context.Background(), \"redis: connection pool: failed to get a usable connection after %d attempts\", attempts)\n\t\treturn nil, nil\n\t}\n\n\treturn cn, nil\n}\n\nfunc (p *ConnPool) Put(ctx context.Context, cn *Conn) {\n\tp.putConn(ctx, cn, true)\n}\n\n// putConnWithoutTurn is an internal method that puts a connection back to the pool\n// without freeing a turn. This is used when returning a rejected connection from\n// within Get(), where the turn is still held by the Get() call.\nfunc (p *ConnPool) putConnWithoutTurn(ctx context.Context, cn *Conn) {\n\tp.putConn(ctx, cn, false)\n}\n\n// putConn is the internal implementation of Put that optionally frees a turn.\nfunc (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {\n\t// Guard against nil connection\n\tif cn == nil {\n\t\tinternal.Logger.Printf(ctx, \"putConn called with nil connection\")\n\t\tif freeTurn {\n\t\t\tp.freeTurn()\n\t\t}\n\t\treturn\n\t}\n\n\t// Process connection using the hooks system\n\tshouldPool := true\n\tshouldRemove := false\n\tvar err error\n\n\tif cn.HasBufferedData() {\n\t\t// Peek at the reply type to check if it's a push notification\n\t\tif replyType, err := cn.PeekReplyTypeSafe(); err != nil || replyType != proto.RespPush {\n\t\t\t// Not a push notification or error peeking, remove connection\n\t\t\tinternal.Logger.Printf(ctx, \"Conn has unread data (not push notification), removing it\")\n\t\t\tp.removeConnInternal(ctx, cn, err, freeTurn)\n\t\t\treturn\n\t\t}\n\t\t// It's a push notification, allow pooling (client will handle it)\n\t}\n\n\t// Lock-free atomic read - no mutex overhead!\n\thookManager := p.hookManager.Load()\n\n\tif hookManager != nil {\n\t\tshouldPool, shouldRemove, err = hookManager.ProcessOnPut(ctx, cn)\n\t\tif err != nil {\n\t\t\tinternal.Logger.Printf(ctx, \"Connection hook error: %v\", err)\n\t\t\tp.removeConnInternal(ctx, cn, err, freeTurn)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Combine all removal checks into one - reduces branches\n\tif shouldRemove || !shouldPool {\n\t\tp.removeConnInternal(ctx, cn, errHookRequestedRemoval, freeTurn)\n\t\treturn\n\t}\n\n\tif !cn.pooled {\n\t\tp.removeConnInternal(ctx, cn, errConnNotPooled, freeTurn)\n\t\treturn\n\t}\n\n\tvar shouldCloseConn bool\n\n\tif p.cfg.MaxIdleConns == 0 || p.idleConnsLen.Load() < p.cfg.MaxIdleConns {\n\t\t// Hot path optimization: try fast IN_USE → IDLE transition\n\t\t// Using inline Release() method for better performance (avoids pointer dereference)\n\t\ttransitionedToIdle := cn.Release()\n\n\t\t// Handle unexpected state changes\n\t\tif !transitionedToIdle {\n\t\t\t// Fast path failed - hook might have changed state (e.g., to UNUSABLE for handoff)\n\t\t\t// Keep the state set by the hook and pool the connection anyway\n\t\t\tsm := cn.GetStateMachine()\n\t\t\tif sm == nil {\n\t\t\t\t// State machine is nil - connection is in an invalid state, remove it\n\t\t\t\tinternal.Logger.Printf(ctx, \"conn[%d] has nil state machine, removing it\", cn.GetID())\n\t\t\t\tp.removeConnInternal(ctx, cn, errConnNotPooled, freeTurn)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcurrentState := sm.GetState()\n\t\t\tswitch currentState {\n\t\t\tcase StateUnusable:\n\t\t\t\t// expected state, don't log it\n\t\t\tcase StateClosed:\n\t\t\t\tinternal.Logger.Printf(ctx, \"Unexpected conn[%d] state changed by hook to %v, closing it\", cn.GetID(), currentState)\n\t\t\t\tshouldCloseConn = true\n\t\t\t\tp.removeConnWithLock(cn)\n\t\t\tdefault:\n\t\t\t\t// Pool as-is\n\t\t\t\tinternal.Logger.Printf(ctx, \"Unexpected conn[%d] state changed by hook to %v, pooling as-is\", cn.GetID(), currentState)\n\t\t\t}\n\t\t}\n\n\t\t// unusable conns are expected to become usable at some point (background process is reconnecting them)\n\t\t// put them at the opposite end of the queue\n\t\t// Optimization: if we just transitioned to IDLE, we know it's usable - skip the check\n\t\tif !transitionedToIdle && !cn.IsUsable() {\n\t\t\tif p.cfg.PoolFIFO {\n\t\t\t\tp.connsMu.Lock()\n\t\t\t\tp.idleConns = append(p.idleConns, cn)\n\t\t\t\tp.connsMu.Unlock()\n\t\t\t} else {\n\t\t\t\tp.connsMu.Lock()\n\t\t\t\tp.idleConns = append([]*Conn{cn}, p.idleConns...)\n\t\t\t\tp.connsMu.Unlock()\n\t\t\t}\n\t\t\tp.idleConnsLen.Add(1)\n\t\t} else if !shouldCloseConn {\n\t\t\tp.connsMu.Lock()\n\t\t\tp.idleConns = append(p.idleConns, cn)\n\t\t\tp.connsMu.Unlock()\n\t\t\tp.idleConnsLen.Add(1)\n\t\t}\n\n\t\t// Notify metrics: connection moved from used to idle\n\t\tif cb := getMetricConnectionStateChangeCallback(); cb != nil {\n\t\t\tcb(ctx, cn, MetricStateUsed, MetricStateIdle)\n\t\t}\n\t} else {\n\t\tshouldCloseConn = true\n\t\tp.removeConnWithLock(cn)\n\n\t\t// Notify metrics: connection removed (used -> nothing)\n\t\tif cb := getMetricConnectionStateChangeCallback(); cb != nil {\n\t\t\tcb(ctx, cn, MetricStateUsed, \"\")\n\t\t}\n\t}\n\n\tif freeTurn {\n\t\tp.freeTurn()\n\t}\n\n\tif shouldCloseConn {\n\t\t_ = p.closeConn(cn)\n\t}\n\n\tcn.SetLastPutAtNs(getCachedTimeNs())\n}\n\nfunc (p *ConnPool) Remove(ctx context.Context, cn *Conn, reason error) {\n\tp.removeConnInternal(ctx, cn, reason, true)\n}\n\n// RemoveWithoutTurn removes a connection from the pool without freeing a turn.\n// This should be used when removing a connection from a context that didn't acquire\n// a turn via Get() (e.g., background workers, cleanup tasks).\n// For normal removal after Get(), use Remove() instead.\nfunc (p *ConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) {\n\tp.removeConnInternal(ctx, cn, reason, false)\n}\n\n// removeConnInternal is the internal implementation of Remove that optionally frees a turn.\nfunc (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason error, freeTurn bool) {\n\t// Lock-free atomic read - no mutex overhead!\n\thookManager := p.hookManager.Load()\n\n\tif hookManager != nil {\n\t\thookManager.ProcessOnRemove(ctx, cn, reason)\n\t}\n\n\tp.removeConnWithLock(cn)\n\n\tif freeTurn {\n\t\tp.freeTurn()\n\t}\n\n\t// Notify metrics: connection removed (assume from used state)\n\tif cb := getMetricConnectionStateChangeCallback(); cb != nil {\n\t\tcb(ctx, cn, MetricStateUsed, \"\")\n\t}\n\n\t// Record connection closed\n\tif cb := getMetricConnectionClosedCallback(); cb != nil {\n\t\treasonStr := \"unknown\"\n\t\tif reason != nil {\n\t\t\treasonStr = reason.Error()\n\t\t}\n\t\tcb(ctx, cn, reasonStr, reason)\n\t}\n\n\t_ = p.closeConn(cn)\n\n\t// Check if we need to create new idle connections to maintain MinIdleConns\n\tp.checkMinIdleConns()\n}\n\n// CloseConn closes a connection and records metrics.\n// Parameters:\n//   - ctx: context for metric callbacks (enables trace-to-metric correlation)\n//   - cn: the connection to close\n//   - reason: why the connection is being closed (use CloseReason* constants)\n//   - fromState: the metric state the connection was in (use MetricState* constants)\nfunc (p *ConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error {\n\tp.removeConnWithLock(cn)\n\n\t// Record connection state change: connection is being removed from the specified state\n\tif cb := getMetricConnectionStateChangeCallback(); cb != nil && fromState != \"\" {\n\t\tcb(ctx, cn, fromState, \"\")\n\t}\n\n\t// Record connection closed metric with the specified reason\n\tif cb := getMetricConnectionClosedCallback(); cb != nil {\n\t\tcb(ctx, cn, reason, nil)\n\t}\n\n\treturn p.closeConn(cn)\n}\n\nfunc (p *ConnPool) removeConnWithLock(cn *Conn) {\n\tp.connsMu.Lock()\n\tdefer p.connsMu.Unlock()\n\tp.removeConn(cn)\n}\n\nfunc (p *ConnPool) removeConn(cn *Conn) {\n\tcid := cn.GetID()\n\tdelete(p.conns, cid)\n\tatomic.AddUint32(&p.stats.StaleConns, 1)\n\n\t// Decrement pool size counter when removing a connection\n\tif cn.pooled {\n\t\tp.poolSize.Add(-1)\n\t\t// this can be idle conn\n\t\tfor idx, ic := range p.idleConns {\n\t\t\tif ic == cn {\n\t\t\t\tp.idleConns = append(p.idleConns[:idx], p.idleConns[idx+1:]...)\n\t\t\t\tp.idleConnsLen.Add(-1)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (p *ConnPool) closeConn(cn *Conn) error {\n\treturn cn.Close()\n}\n\n// Len returns total number of connections.\nfunc (p *ConnPool) Len() int {\n\tp.connsMu.Lock()\n\tn := len(p.conns)\n\tp.connsMu.Unlock()\n\treturn n\n}\n\n// IdleLen returns number of idle connections.\nfunc (p *ConnPool) IdleLen() int {\n\tp.connsMu.Lock()\n\tn := p.idleConnsLen.Load()\n\tp.connsMu.Unlock()\n\treturn int(n)\n}\n\n// Size returns the maximum pool size (capacity).\n//\n// This is used by the streaming credentials manager to size the re-auth worker pool,\n// ensuring that re-auth operations don't exhaust the connection pool.\nfunc (p *ConnPool) Size() int {\n\treturn int(p.cfg.PoolSize)\n}\n\nfunc (p *ConnPool) Stats() *Stats {\n\treturn &Stats{\n\t\tHits:            atomic.LoadUint32(&p.stats.Hits),\n\t\tMisses:          atomic.LoadUint32(&p.stats.Misses),\n\t\tTimeouts:        atomic.LoadUint32(&p.stats.Timeouts),\n\t\tWaitCount:       atomic.LoadUint32(&p.stats.WaitCount),\n\t\tUnusable:        atomic.LoadUint32(&p.stats.Unusable),\n\t\tWaitDurationNs:  p.waitDurationNs.Load(),\n\t\tPendingRequests: atomic.LoadUint32(&p.stats.PendingRequests),\n\n\t\tTotalConns: uint32(p.Len()),\n\t\tIdleConns:  uint32(p.IdleLen()),\n\t\tStaleConns: atomic.LoadUint32(&p.stats.StaleConns),\n\t}\n}\n\nfunc (p *ConnPool) closed() bool {\n\treturn atomic.LoadUint32(&p._closed) == 1\n}\n\nfunc (p *ConnPool) Filter(fn func(*Conn) bool) error {\n\tp.connsMu.Lock()\n\tdefer p.connsMu.Unlock()\n\n\tvar firstErr error\n\tfor _, cn := range p.conns {\n\t\tif fn(cn) {\n\t\t\tif err := p.closeConn(cn); err != nil && firstErr == nil {\n\t\t\t\tfirstErr = err\n\t\t\t}\n\t\t}\n\t}\n\treturn firstErr\n}\n\nfunc (p *ConnPool) Close() error {\n\tif !atomic.CompareAndSwapUint32(&p._closed, 0, 1) {\n\t\treturn ErrClosed\n\t}\n\n\tvar firstErr error\n\tp.connsMu.Lock()\n\tfor _, cn := range p.conns {\n\t\tif err := p.closeConn(cn); err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\tp.conns = nil\n\tp.poolSize.Store(0)\n\tp.idleConns = nil\n\tp.idleConnsLen.Store(0)\n\tp.connsMu.Unlock()\n\n\treturn firstErr\n}\n\nfunc (p *ConnPool) isHealthyConn(cn *Conn, nowNs int64) bool {\n\t// Performance optimization: check conditions from cheapest to most expensive,\n\t// and from most likely to fail to least likely to fail.\n\n\t// Only fails if ConnMaxLifetime is set AND connection is old.\n\t// Most pools don't set ConnMaxLifetime, so this rarely fails.\n\tif p.cfg.ConnMaxLifetime > 0 {\n\t\tif cn.expiresAt.UnixNano() < nowNs {\n\t\t\treturn false // Connection has exceeded max lifetime\n\t\t}\n\t}\n\n\t// Most pools set ConnMaxIdleTime, and idle connections are common.\n\t// Checking this first allows us to fail fast without expensive syscalls.\n\tif p.cfg.ConnMaxIdleTime > 0 {\n\t\tif nowNs-cn.UsedAtNs() >= int64(p.cfg.ConnMaxIdleTime) {\n\t\t\treturn false // Connection has been idle too long\n\t\t}\n\t}\n\n\t// Only run this if the cheap checks passed.\n\tif err := connCheck(cn.getNetConn()); err != nil {\n\t\t// If there's unexpected data, it might be push notifications (RESP3)\n\t\tif p.cfg.PushNotificationsEnabled && err == errUnexpectedRead {\n\t\t\t// Peek at the reply type to check if it's a push notification\n\t\t\tif replyType, err := cn.rd.PeekReplyType(); err == nil && replyType == proto.RespPush {\n\t\t\t\t// For RESP3 connections with push notifications, we allow some buffered data\n\t\t\t\t// The client will process these notifications before using the connection\n\t\t\t\tinternal.Logger.Printf(\n\t\t\t\t\tcontext.Background(),\n\t\t\t\t\t\"push: conn[%d] has buffered data, likely push notifications - will be processed by client\",\n\t\t\t\t\tcn.GetID(),\n\t\t\t\t)\n\n\t\t\t\t// Update timestamp for healthy connection\n\t\t\t\tcn.SetUsedAtNs(nowNs)\n\n\t\t\t\t// Connection is healthy, client will handle notifications\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Not a push notification - treat as unhealthy\n\t\t\treturn false\n\t\t}\n\t\t// Connection failed health check\n\t\treturn false\n\t}\n\n\t// Only update UsedAt if connection is healthy (avoids unnecessary atomic store)\n\tcn.SetUsedAtNs(nowNs)\n\treturn true\n}\n"
  },
  {
    "path": "internal/pool/pool_single.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// SingleConnPool is a pool that always returns the same connection.\n// Note: This pool is not thread-safe.\n// It is intended to be used by clients that need a single connection.\ntype SingleConnPool struct {\n\tpool      Pooler\n\tcn        *Conn\n\tstickyErr error\n}\n\nvar _ Pooler = (*SingleConnPool)(nil)\n\n// NewSingleConnPool creates a new single connection pool.\n// The pool will always return the same connection.\n// The pool will not:\n// - Close the connection\n// - Reconnect the connection\n// - Track the connection in any way\nfunc NewSingleConnPool(pool Pooler, cn *Conn) *SingleConnPool {\n\treturn &SingleConnPool{\n\t\tpool: pool,\n\t\tcn:   cn,\n\t}\n}\n\nfunc (p *SingleConnPool) NewConn(ctx context.Context) (*Conn, error) {\n\treturn p.pool.NewConn(ctx)\n}\n\nfunc (p *SingleConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error {\n\treturn p.pool.CloseConn(ctx, cn, reason, fromState)\n}\n\nfunc (p *SingleConnPool) Get(_ context.Context) (*Conn, error) {\n\tif p.stickyErr != nil {\n\t\treturn nil, p.stickyErr\n\t}\n\tif p.cn == nil {\n\t\treturn nil, ErrClosed\n\t}\n\n\t// NOTE: SingleConnPool is NOT thread-safe by design and is used in special scenarios:\n\t// - During initialization (connection is in INITIALIZING state)\n\t// - During re-authentication (connection is in UNUSABLE state)\n\t// - For transactions (connection might be in various states)\n\t// We use SetUsed() which forces the transition, rather than TryTransition() which\n\t// would fail if the connection is not in IDLE/CREATED state.\n\tp.cn.SetUsed(true)\n\tp.cn.SetUsedAt(time.Now())\n\treturn p.cn, nil\n}\n\nfunc (p *SingleConnPool) Put(_ context.Context, cn *Conn) {\n\tif p.cn == nil {\n\t\treturn\n\t}\n\tif p.cn != cn {\n\t\treturn\n\t}\n\tp.cn.SetUsed(false)\n}\n\nfunc (p *SingleConnPool) Remove(_ context.Context, cn *Conn, reason error) {\n\tcn.SetUsed(false)\n\tp.cn = nil\n\tp.stickyErr = reason\n}\n\n// RemoveWithoutTurn has the same behavior as Remove for SingleConnPool\n// since SingleConnPool doesn't use a turn-based queue system.\nfunc (p *SingleConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) {\n\tp.Remove(ctx, cn, reason)\n}\n\nfunc (p *SingleConnPool) Close() error {\n\tp.cn = nil\n\tp.stickyErr = ErrClosed\n\treturn nil\n}\n\nfunc (p *SingleConnPool) Len() int {\n\treturn 0\n}\n\nfunc (p *SingleConnPool) IdleLen() int {\n\treturn 0\n}\n\n// Size returns the maximum pool size, which is always 1 for SingleConnPool.\nfunc (p *SingleConnPool) Size() int { return 1 }\n\nfunc (p *SingleConnPool) Stats() *Stats {\n\treturn &Stats{}\n}\n\nfunc (p *SingleConnPool) AddPoolHook(_ PoolHook) {}\n\nfunc (p *SingleConnPool) RemovePoolHook(_ PoolHook) {}\n"
  },
  {
    "path": "internal/pool/pool_sticky.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync/atomic\"\n)\n\nconst (\n\tstateDefault = 0\n\tstateInited  = 1\n\tstateClosed  = 2\n)\n\ntype BadConnError struct {\n\twrapped error\n}\n\nvar _ error = (*BadConnError)(nil)\n\nfunc (e BadConnError) Error() string {\n\ts := \"redis: Conn is in a bad state\"\n\tif e.wrapped != nil {\n\t\ts += \": \" + e.wrapped.Error()\n\t}\n\treturn s\n}\n\nfunc (e BadConnError) Unwrap() error {\n\treturn e.wrapped\n}\n\n//------------------------------------------------------------------------------\n\ntype StickyConnPool struct {\n\tpool   Pooler\n\tshared int32 // atomic\n\n\tstate uint32 // atomic\n\tch    chan *Conn\n\n\t_badConnError atomic.Value\n}\n\nvar _ Pooler = (*StickyConnPool)(nil)\n\nfunc NewStickyConnPool(pool Pooler) *StickyConnPool {\n\tp, ok := pool.(*StickyConnPool)\n\tif !ok {\n\t\tp = &StickyConnPool{\n\t\t\tpool: pool,\n\t\t\tch:   make(chan *Conn, 1),\n\t\t}\n\t}\n\tatomic.AddInt32(&p.shared, 1)\n\treturn p\n}\n\nfunc (p *StickyConnPool) NewConn(ctx context.Context) (*Conn, error) {\n\treturn p.pool.NewConn(ctx)\n}\n\nfunc (p *StickyConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error {\n\treturn p.pool.CloseConn(ctx, cn, reason, fromState)\n}\n\nfunc (p *StickyConnPool) Get(ctx context.Context) (*Conn, error) {\n\t// In worst case this races with Close which is not a very common operation.\n\tfor i := 0; i < 1000; i++ {\n\t\tswitch atomic.LoadUint32(&p.state) {\n\t\tcase stateDefault:\n\t\t\tcn, err := p.pool.Get(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif atomic.CompareAndSwapUint32(&p.state, stateDefault, stateInited) {\n\t\t\t\treturn cn, nil\n\t\t\t}\n\t\t\tp.pool.Remove(ctx, cn, ErrClosed)\n\t\tcase stateInited:\n\t\t\tif err := p.badConnError(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcn, ok := <-p.ch\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrClosed\n\t\t\t}\n\t\t\treturn cn, nil\n\t\tcase stateClosed:\n\t\t\treturn nil, ErrClosed\n\t\tdefault:\n\t\t\tpanic(\"not reached\")\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"redis: StickyConnPool.Get: infinite loop\")\n}\n\nfunc (p *StickyConnPool) Put(ctx context.Context, cn *Conn) {\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tp.freeConn(ctx, cn)\n\t\t}\n\t}()\n\tp.ch <- cn\n}\n\nfunc (p *StickyConnPool) freeConn(ctx context.Context, cn *Conn) {\n\tif err := p.badConnError(); err != nil {\n\t\tp.pool.Remove(ctx, cn, err)\n\t} else {\n\t\tp.pool.Put(ctx, cn)\n\t}\n}\n\nfunc (p *StickyConnPool) Remove(ctx context.Context, cn *Conn, reason error) {\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tp.pool.Remove(ctx, cn, ErrClosed)\n\t\t}\n\t}()\n\tp._badConnError.Store(BadConnError{wrapped: reason})\n\tp.ch <- cn\n}\n\n// RemoveWithoutTurn has the same behavior as Remove for StickyConnPool\n// since StickyConnPool doesn't use a turn-based queue system.\nfunc (p *StickyConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) {\n\tp.Remove(ctx, cn, reason)\n}\n\nfunc (p *StickyConnPool) Close() error {\n\tif shared := atomic.AddInt32(&p.shared, -1); shared > 0 {\n\t\treturn nil\n\t}\n\n\tfor i := 0; i < 1000; i++ {\n\t\tstate := atomic.LoadUint32(&p.state)\n\t\tif state == stateClosed {\n\t\t\treturn ErrClosed\n\t\t}\n\t\tif atomic.CompareAndSwapUint32(&p.state, state, stateClosed) {\n\t\t\tclose(p.ch)\n\t\t\tcn, ok := <-p.ch\n\t\t\tif ok {\n\t\t\t\tp.freeConn(context.TODO(), cn)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn errors.New(\"redis: StickyConnPool.Close: infinite loop\")\n}\n\nfunc (p *StickyConnPool) Reset(ctx context.Context) error {\n\tif p.badConnError() == nil {\n\t\treturn nil\n\t}\n\n\tselect {\n\tcase cn, ok := <-p.ch:\n\t\tif !ok {\n\t\t\treturn ErrClosed\n\t\t}\n\t\tp.pool.Remove(ctx, cn, ErrClosed)\n\t\tp._badConnError.Store(BadConnError{wrapped: nil})\n\tdefault:\n\t\treturn errors.New(\"redis: StickyConnPool does not have a Conn\")\n\t}\n\n\tif !atomic.CompareAndSwapUint32(&p.state, stateInited, stateDefault) {\n\t\tstate := atomic.LoadUint32(&p.state)\n\t\treturn fmt.Errorf(\"redis: invalid StickyConnPool state: %d\", state)\n\t}\n\n\treturn nil\n}\n\nfunc (p *StickyConnPool) badConnError() error {\n\tif v := p._badConnError.Load(); v != nil {\n\t\tif err := v.(BadConnError); err.wrapped != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *StickyConnPool) Len() int {\n\tswitch atomic.LoadUint32(&p.state) {\n\tcase stateDefault:\n\t\treturn 0\n\tcase stateInited:\n\t\treturn 1\n\tcase stateClosed:\n\t\treturn 0\n\tdefault:\n\t\tpanic(\"not reached\")\n\t}\n}\n\nfunc (p *StickyConnPool) IdleLen() int {\n\treturn len(p.ch)\n}\n\n// Size returns the maximum pool size, which is always 1 for StickyConnPool.\nfunc (p *StickyConnPool) Size() int { return 1 }\n\nfunc (p *StickyConnPool) Stats() *Stats {\n\treturn &Stats{}\n}\n\nfunc (p *StickyConnPool) AddPoolHook(hook PoolHook) {}\n\nfunc (p *StickyConnPool) RemovePoolHook(hook PoolHook) {}\n"
  },
  {
    "path": "internal/pool/pool_test.go",
    "content": "package pool_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/logging\"\n)\n\nvar _ = Describe(\"ConnPool\", func() {\n\tctx := context.Background()\n\tvar connPool *pool.ConnPool\n\n\tBeforeEach(func() {\n\t\tconnPool = pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           int32(10),\n\t\t\tMaxConcurrentDials: 10,\n\t\t\tPoolTimeout:        time.Hour,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tConnMaxIdleTime:    time.Millisecond,\n\t\t})\n\t})\n\n\tAfterEach(func() {\n\t\tconnPool.Close()\n\t})\n\n\tIt(\"should safe close\", func() {\n\t\tconst minIdleConns = 10\n\n\t\tvar (\n\t\t\twg         sync.WaitGroup\n\t\t\tclosedChan = make(chan struct{})\n\t\t)\n\t\twg.Add(minIdleConns)\n\t\tconnPool = pool.NewConnPool(&pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\twg.Done()\n\t\t\t\t<-closedChan\n\t\t\t\treturn &net.TCPConn{}, nil\n\t\t\t},\n\t\t\tPoolSize:           int32(10),\n\t\t\tMaxConcurrentDials: 10,\n\t\t\tPoolTimeout:        time.Hour,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tConnMaxIdleTime:    time.Millisecond,\n\t\t\tMinIdleConns:       int32(minIdleConns),\n\t\t})\n\t\twg.Wait()\n\t\tExpect(connPool.Close()).NotTo(HaveOccurred())\n\t\tclose(closedChan)\n\n\t\t// We wait for 1 second and believe that checkIdleConns has been executed.\n\t\ttime.Sleep(time.Second)\n\n\t\tExpect(connPool.Stats()).To(Equal(&pool.Stats{\n\t\t\tHits:           0,\n\t\t\tMisses:         0,\n\t\t\tTimeouts:       0,\n\t\t\tWaitCount:      0,\n\t\t\tWaitDurationNs: 0,\n\t\t\tTotalConns:     0,\n\t\t\tIdleConns:      0,\n\t\t\tStaleConns:     0,\n\t\t}))\n\t})\n\n\tIt(\"should unblock client when conn is removed\", func() {\n\t\t// Reserve one connection.\n\t\tcn, err := connPool.Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Reserve all other connections.\n\t\tvar cns []*pool.Conn\n\t\tfor i := 0; i < 9; i++ {\n\t\t\tcn, err := connPool.Get(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tcns = append(cns, cn)\n\t\t}\n\n\t\tstarted := make(chan bool, 1)\n\t\tdone := make(chan bool, 1)\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\n\t\t\tstarted <- true\n\t\t\t_, err := connPool.Get(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tdone <- true\n\n\t\t\tconnPool.Put(ctx, cn)\n\t\t}()\n\t\t<-started\n\n\t\t// Check that Get is blocked.\n\t\tselect {\n\t\tcase <-done:\n\t\t\tFail(\"Get is not blocked\")\n\t\tcase <-time.After(time.Millisecond):\n\t\t\t// ok\n\t\t}\n\n\t\tconnPool.Remove(ctx, cn, errors.New(\"test\"))\n\n\t\t// Check that Get is unblocked.\n\t\tselect {\n\t\tcase <-done:\n\t\t\t// ok\n\t\tcase <-time.After(time.Second):\n\t\t\tFail(\"Get is not unblocked\")\n\t\t}\n\n\t\tfor _, cn := range cns {\n\t\t\tconnPool.Put(ctx, cn)\n\t\t}\n\t})\n})\n\nvar _ = Describe(\"MinIdleConns\", func() {\n\tconst poolSize = 100\n\tctx := context.Background()\n\tvar minIdleConns int\n\tvar connPool *pool.ConnPool\n\n\tnewConnPool := func() *pool.ConnPool {\n\t\tconnPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           int32(poolSize),\n\t\t\tMaxConcurrentDials: poolSize,\n\t\t\tMinIdleConns:       int32(minIdleConns),\n\t\t\tPoolTimeout:        100 * time.Millisecond,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tConnMaxIdleTime:    -1,\n\t\t})\n\t\tEventually(func() int {\n\t\t\treturn connPool.Len()\n\t\t}).Should(Equal(minIdleConns))\n\t\treturn connPool\n\t}\n\n\tassert := func() {\n\t\tIt(\"has idle connections when created\", func() {\n\t\t\tExpect(connPool.Len()).To(Equal(minIdleConns))\n\t\t\tExpect(connPool.IdleLen()).To(Equal(minIdleConns))\n\t\t})\n\n\t\tContext(\"after Get\", func() {\n\t\t\tvar cn *pool.Conn\n\n\t\t\tBeforeEach(func() {\n\t\t\t\tvar err error\n\t\t\t\tcn, err = connPool.Get(ctx)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tEventually(func() int {\n\t\t\t\t\treturn connPool.Len()\n\t\t\t\t}).Should(Equal(minIdleConns + 1))\n\t\t\t})\n\n\t\t\tIt(\"has idle connections\", func() {\n\t\t\t\tExpect(connPool.Len()).To(Equal(minIdleConns + 1))\n\t\t\t\tExpect(connPool.IdleLen()).To(Equal(minIdleConns))\n\t\t\t})\n\n\t\t\tContext(\"after Remove\", func() {\n\t\t\t\tBeforeEach(func() {\n\t\t\t\t\tconnPool.Remove(ctx, cn, errors.New(\"test\"))\n\t\t\t\t})\n\n\t\t\t\tIt(\"has idle connections\", func() {\n\t\t\t\t\tExpect(connPool.Len()).To(Equal(minIdleConns))\n\t\t\t\t\tExpect(connPool.IdleLen()).To(Equal(minIdleConns))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Get does not exceed pool size\", func() {\n\t\t\tvar mu sync.RWMutex\n\t\t\tvar cns []*pool.Conn\n\n\t\t\tBeforeEach(func() {\n\t\t\t\tcns = make([]*pool.Conn, 0)\n\n\t\t\t\tperform(poolSize, func(_ int) {\n\t\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tcns = append(cns, cn)\n\t\t\t\t\tmu.Unlock()\n\t\t\t\t})\n\n\t\t\t\tEventually(func() int {\n\t\t\t\t\treturn connPool.Len()\n\t\t\t\t}).Should(BeNumerically(\">=\", poolSize))\n\t\t\t})\n\n\t\t\tIt(\"Get is blocked\", func() {\n\t\t\t\tdone := make(chan struct{})\n\t\t\t\tgo func() {\n\t\t\t\t\tconnPool.Get(ctx)\n\t\t\t\t\tclose(done)\n\t\t\t\t}()\n\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\t\tFail(\"Get is not blocked\")\n\t\t\t\tcase <-time.After(time.Millisecond):\n\t\t\t\t\t// ok\n\t\t\t\t}\n\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\t\t// ok\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tFail(\"Get is not unblocked\")\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tContext(\"after Put\", func() {\n\t\t\t\tBeforeEach(func() {\n\t\t\t\t\tperform(len(cns), func(i int) {\n\t\t\t\t\t\tmu.RLock()\n\t\t\t\t\t\tconnPool.Put(ctx, cns[i])\n\t\t\t\t\t\tmu.RUnlock()\n\t\t\t\t\t})\n\n\t\t\t\t\tEventually(func() int {\n\t\t\t\t\t\treturn connPool.Len()\n\t\t\t\t\t}).Should(Equal(poolSize))\n\t\t\t\t})\n\n\t\t\t\tIt(\"pool.Len is back to normal\", func() {\n\t\t\t\t\tExpect(connPool.Len()).To(Equal(poolSize))\n\t\t\t\t\tExpect(connPool.IdleLen()).To(Equal(poolSize))\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tContext(\"after Remove\", func() {\n\t\t\t\tBeforeEach(func() {\n\t\t\t\t\tperform(len(cns), func(i int) {\n\t\t\t\t\t\tmu.RLock()\n\t\t\t\t\t\tconnPool.Remove(ctx, cns[i], errors.New(\"test\"))\n\t\t\t\t\t\tmu.RUnlock()\n\t\t\t\t\t})\n\n\t\t\t\t\tEventually(func() int {\n\t\t\t\t\t\treturn connPool.Len()\n\t\t\t\t\t}).Should(Equal(minIdleConns))\n\t\t\t\t})\n\n\t\t\t\tIt(\"has idle connections\", func() {\n\t\t\t\t\tExpect(connPool.Len()).To(Equal(minIdleConns))\n\t\t\t\t\tExpect(connPool.IdleLen()).To(Equal(minIdleConns))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t}\n\n\tContext(\"minIdleConns = 1\", func() {\n\t\tBeforeEach(func() {\n\t\t\tminIdleConns = 1\n\t\t\tconnPool = newConnPool()\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\tconnPool.Close()\n\t\t})\n\n\t\tassert()\n\t})\n\n\tContext(\"minIdleConns = 32\", func() {\n\t\tBeforeEach(func() {\n\t\t\tminIdleConns = 32\n\t\t\tconnPool = newConnPool()\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\tconnPool.Close()\n\t\t})\n\n\t\tassert()\n\t})\n})\n\nvar _ = Describe(\"race\", func() {\n\tctx := context.Background()\n\tvar connPool *pool.ConnPool\n\tvar C, N int\n\n\tBeforeEach(func() {\n\t\tC, N = 10, 1000\n\t\tif testing.Short() {\n\t\t\tC = 2\n\t\t\tN = 50\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\tconnPool.Close()\n\t})\n\n\tIt(\"does not happen on Get, Put, and Remove\", func() {\n\t\tconnPool = pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           int32(10),\n\t\t\tMaxConcurrentDials: 10,\n\t\t\tPoolTimeout:        time.Minute,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tConnMaxIdleTime:    time.Millisecond,\n\t\t})\n\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif err == nil {\n\t\t\t\t\tconnPool.Put(ctx, cn)\n\t\t\t\t}\n\t\t\t}\n\t\t}, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif err == nil {\n\t\t\t\t\tconnPool.Remove(ctx, cn, errors.New(\"test\"))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n\n\tIt(\"limit the number of connections\", func() {\n\t\topt := &pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\treturn &net.TCPConn{}, nil\n\t\t\t},\n\t\t\tPoolSize:           int32(1000),\n\t\t\tMaxConcurrentDials: 1000,\n\t\t\tMinIdleConns:       int32(50),\n\t\t\tPoolTimeout:        3 * time.Second,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t}\n\t\tp := pool.NewConnPool(opt)\n\n\t\tvar wg sync.WaitGroup\n\t\tfor i := int32(0); i < opt.PoolSize; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_, _ = p.Get(ctx)\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\tstats := p.Stats()\n\t\tExpect(stats.IdleConns).To(Equal(uint32(0)))\n\t\tExpect(stats.TotalConns).To(Equal(uint32(opt.PoolSize)))\n\t})\n\n\tIt(\"recover addIdleConn panic\", func() {\n\t\topt := &pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\tpanic(\"test panic\")\n\t\t\t},\n\t\t\tPoolSize:           int32(100),\n\t\t\tMaxConcurrentDials: 100,\n\t\t\tMinIdleConns:       int32(30),\n\t\t}\n\t\tp := pool.NewConnPool(opt)\n\n\t\tp.CheckMinIdleConns()\n\n\t\tEventually(func() bool {\n\t\t\tstate := p.Stats()\n\t\t\treturn state.TotalConns == 0 && state.IdleConns == 0 && p.QueueLen() == 0\n\t\t}, \"3s\", \"50ms\").Should(BeTrue())\n\t})\n\n\tIt(\"wait\", func() {\n\t\topt := &pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\treturn &net.TCPConn{}, nil\n\t\t\t},\n\t\t\tPoolSize:           int32(1),\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        3 * time.Second,\n\t\t}\n\t\tp := pool.NewConnPool(opt)\n\n\t\twait := make(chan struct{})\n\t\tconn, _ := p.Get(ctx)\n\t\tgo func() {\n\t\t\t_, _ = p.Get(ctx)\n\t\t\twait <- struct{}{}\n\t\t}()\n\t\ttime.Sleep(time.Second)\n\t\tp.Put(ctx, conn)\n\t\t<-wait\n\n\t\tstats := p.Stats()\n\t\tExpect(stats.IdleConns).To(Equal(uint32(0)))\n\t\tExpect(stats.TotalConns).To(Equal(uint32(1)))\n\t\tExpect(stats.WaitCount).To(Equal(uint32(1)))\n\t\tExpect(stats.WaitDurationNs).To(BeNumerically(\"~\", time.Second.Nanoseconds(), 100*time.Millisecond.Nanoseconds()))\n\t})\n\n\tIt(\"timeout\", func() {\n\t\ttestPoolTimeout := 1 * time.Second\n\t\topt := &pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\t// Artificial delay to force pool timeout\n\t\t\t\ttime.Sleep(3 * testPoolTimeout)\n\n\t\t\t\treturn &net.TCPConn{}, nil\n\t\t\t},\n\t\t\tPoolSize:           int32(1),\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        testPoolTimeout,\n\t\t}\n\t\tp := pool.NewConnPool(opt)\n\n\t\tstats := p.Stats()\n\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\n\t\tconn, err := p.Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_, err = p.Get(ctx)\n\t\tExpect(err).To(MatchError(pool.ErrPoolTimeout))\n\t\tp.Put(ctx, conn)\n\t\t_, err = p.Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tstats = p.Stats()\n\t\tExpect(stats.Timeouts).To(Equal(uint32(1)))\n\t})\n})\n\n// TestDialerRetryConfiguration tests the new DialerRetries and DialerRetryTimeout options\nfunc TestDialerRetryConfiguration(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"CustomDialerRetries\", func(t *testing.T) {\n\t\tvar attempts int64\n\t\tfailingDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\tatomic.AddInt64(&attempts, 1)\n\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t}\n\n\t\tconnPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             failingDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        time.Second,\n\t\t\tDialTimeout:        time.Second,\n\t\t\tDialerRetries:      3,                     // Custom retry count\n\t\t\tDialerRetryTimeout: 10 * time.Millisecond, // Fast retries for testing\n\t\t})\n\t\tdefer connPool.Close()\n\n\t\t_, err := connPool.Get(ctx)\n\t\tif err == nil {\n\t\t\tt.Error(\"Expected error from failing dialer\")\n\t\t}\n\n\t\t// Should have attempted at least 3 times (DialerRetries = 3)\n\t\t// There might be additional attempts due to pool logic\n\t\tfinalAttempts := atomic.LoadInt64(&attempts)\n\t\tif finalAttempts < 3 {\n\t\t\tt.Errorf(\"Expected at least 3 dial attempts, got %d\", finalAttempts)\n\t\t}\n\t\tif finalAttempts > 6 {\n\t\t\tt.Errorf(\"Expected around 3 dial attempts, got %d (too many)\", finalAttempts)\n\t\t}\n\t})\n\n\tt.Run(\"DefaultDialerRetries\", func(t *testing.T) {\n\t\tvar attempts int64\n\t\tfailingDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\tatomic.AddInt64(&attempts, 1)\n\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t}\n\n\t\tconnPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             failingDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tPoolTimeout:        time.Second,\n\t\t\tDialTimeout:        time.Second,\n\t\t\t// DialerRetries and DialerRetryTimeout not set - should use defaults\n\t\t})\n\t\tdefer connPool.Close()\n\n\t\t_, err := connPool.Get(ctx)\n\t\tif err == nil {\n\t\t\tt.Error(\"Expected error from failing dialer\")\n\t\t}\n\n\t\t// Should have attempted 5 times (default DialerRetries = 5)\n\t\t// Note: There may be one additional attempt from tryDial() goroutine\n\t\t// which is launched when dialErrorsNum reaches PoolSize\n\t\tfinalAttempts := atomic.LoadInt64(&attempts)\n\t\tif finalAttempts < 5 {\n\t\t\tt.Errorf(\"Expected at least 5 dial attempts (default), got %d\", finalAttempts)\n\t\t}\n\t\tif finalAttempts > 6 {\n\t\t\tt.Errorf(\"Expected around 5 dial attempts, got %d (too many)\", finalAttempts)\n\t\t}\n\t})\n}\n\nvar _ = Describe(\"queuedNewConn\", func() {\n\tctx := context.Background()\n\n\tIt(\"should successfully create connection when pool is exhausted\", func() {\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 2,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tPoolTimeout:        2 * time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Fill the pool\n\t\tconn1, err := testPool.Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(conn1).NotTo(BeNil())\n\n\t\t// Get second connection in another goroutine\n\t\tdone := make(chan struct{})\n\t\tvar conn2 *pool.Conn\n\t\tvar err2 error\n\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tconn2, err2 = testPool.Get(ctx)\n\t\t\tclose(done)\n\t\t}()\n\n\t\t// Wait a bit to let the second Get start waiting\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Release first connection to let second Get acquire Turn\n\t\ttestPool.Put(ctx, conn1)\n\n\t\t// Wait for second Get to complete\n\t\t<-done\n\t\tExpect(err2).NotTo(HaveOccurred())\n\t\tExpect(conn2).NotTo(BeNil())\n\n\t\t// Clean up second connection\n\t\ttestPool.Put(ctx, conn2)\n\t})\n\n\tIt(\"should handle context cancellation before acquiring dialsInProgress\", func() {\n\t\tslowDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\t// Simulate slow dialing to let first connection creation occupy dialsInProgress\n\t\t\ttime.Sleep(200 * time.Millisecond)\n\t\t\treturn newDummyConn(), nil\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             slowDialer,\n\t\t\tPoolSize:           2,\n\t\t\tMaxConcurrentDials: 1, // Limit to 1 so second request cannot get dialsInProgress permission\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tPoolTimeout:        1 * time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Start first connection creation, this will occupy dialsInProgress\n\t\tdone1 := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tconn1, err := testPool.Get(ctx)\n\t\t\tif err == nil {\n\t\t\t\tdefer testPool.Put(ctx, conn1)\n\t\t\t}\n\t\t\tclose(done1)\n\t\t}()\n\n\t\t// Wait a bit to ensure first request starts and occupies dialsInProgress\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t// Create a context that will be cancelled quickly\n\t\tcancelCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)\n\t\tdefer cancel()\n\n\t\t// Second request should timeout while waiting for dialsInProgress\n\t\t_, err := testPool.Get(cancelCtx)\n\t\tExpect(err).To(Equal(context.DeadlineExceeded))\n\n\t\t// Wait for first request to complete\n\t\t<-done1\n\n\t\t// Verify all turns are released after requests complete\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0), \"All turns should be released after requests complete\")\n\t})\n\n\tIt(\"should handle context cancellation while waiting for connection result\", func() {\n\t\t// This test focuses on proper error handling when context is cancelled\n\t\t// during queuedNewConn execution (not testing connection reuse)\n\n\t\tslowDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\t// Simulate slow dialing\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\treturn newDummyConn(), nil\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             slowDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 2,\n\t\t\tDialTimeout:        2 * time.Second,\n\t\t\tPoolTimeout:        2 * time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Get first connection to fill the pool\n\t\tconn1, err := testPool.Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Create a context that will be cancelled during connection creation\n\t\tcancelCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)\n\t\tdefer cancel()\n\n\t\t// This request should timeout while waiting for connection creation result\n\t\t// Testing the error handling path in queuedNewConn select statement\n\t\tdone := make(chan struct{})\n\t\tvar err2 error\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\t_, err2 = testPool.Get(cancelCtx)\n\t\t\tclose(done)\n\t\t}()\n\n\t\t<-done\n\t\tExpect(err2).To(Equal(context.DeadlineExceeded))\n\n\t\t// Verify turn state - background goroutine may still hold turn\n\t\t// Note: Background connection creation will complete and release turn\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(1), \"Only conn1's turn should be held\")\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty - cancelled request should be cleaned up\")\n\n\t\t// Clean up - release the first connection\n\t\ttestPool.Put(ctx, conn1)\n\n\t\t// Verify all turns are released after cleanup\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0), \"All turns should be released after cleanup\")\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0))\n\t})\n\n\tIt(\"should handle dial failures gracefully\", func() {\n\t\talwaysFailDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\treturn nil, fmt.Errorf(\"dial failed\")\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             alwaysFailDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tPoolTimeout:        1 * time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// This call should fail, testing error handling branch in goroutine\n\t\t_, err := testPool.Get(ctx)\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"dial failed\"))\n\n\t\t// Verify turn is released after dial failure\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0), \"Turn should be released after dial failure\")\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty after dial failure\")\n\n\t\t// Verify connection counts are correct\n\t\tstats := testPool.Stats()\n\t\tExpect(stats.TotalConns).To(Equal(uint32(0)),\n\t\t\t\"No connections should exist after dial failure\")\n\t\tExpect(stats.IdleConns).To(Equal(uint32(0)))\n\t})\n\n\tIt(\"should handle connection creation success with normal delivery\", func() {\n\t\t// This test verifies normal case where connection creation and delivery both succeed\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 2,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tPoolTimeout:        2 * time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Get first connection\n\t\tconn1, err := testPool.Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Get second connection in another goroutine\n\t\tdone := make(chan struct{})\n\t\tvar conn2 *pool.Conn\n\t\tvar err2 error\n\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tconn2, err2 = testPool.Get(ctx)\n\t\t\tclose(done)\n\t\t}()\n\n\t\t// Wait a bit to let second Get start waiting\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Release first connection\n\t\ttestPool.Put(ctx, conn1)\n\n\t\t// Wait for second Get to complete\n\t\t<-done\n\t\tExpect(err2).NotTo(HaveOccurred())\n\t\tExpect(conn2).NotTo(BeNil())\n\n\t\t// Clean up second connection\n\t\ttestPool.Put(ctx, conn2)\n\t})\n\n\tIt(\"should handle MaxConcurrentDials limit\", func() {\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             dummyDialer,\n\t\t\tPoolSize:           3,\n\t\t\tMaxConcurrentDials: 1, // Only allow 1 concurrent dial\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tPoolTimeout:        1 * time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Get all connections to fill the pool\n\t\tvar conns []*pool.Conn\n\t\tfor i := 0; i < 3; i++ {\n\t\t\tconn, err := testPool.Get(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tconns = append(conns, conn)\n\t\t}\n\n\t\t// Now pool is full, next request needs to create new connection\n\t\t// But due to MaxConcurrentDials=1, only one concurrent dial is allowed\n\t\tdone := make(chan struct{})\n\t\tvar err4 error\n\t\tvar conn4 *pool.Conn\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tconn4, err4 = testPool.Get(ctx)\n\t\t\tclose(done)\n\t\t}()\n\n\t\t// Release one connection to let the request complete\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\ttestPool.Put(ctx, conns[0])\n\n\t\t<-done\n\t\tExpect(err4).NotTo(HaveOccurred())\n\n\t\tif conn4 != nil {\n\t\t\ttestPool.Put(ctx, conn4)\n\t\t}\n\n\t\t// Clean up remaining connections\n\t\tfor i := 1; i < len(conns); i++ {\n\t\t\ttestPool.Put(ctx, conns[i])\n\t\t}\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty after all operations complete\")\n\n\t\t// Verify queue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0))\n\n\t\t// Verify connection counts are correct\n\t\tstats := testPool.Stats()\n\t\tExpect(stats.IdleConns).To(Equal(uint32(3)),\n\t\t\t\"All connections should be idle\")\n\t})\n\n\tIt(\"should reuse connections created in background after request timeout\", func() {\n\t\t// This test focuses on connection reuse mechanism:\n\t\t// When a request times out but background connection creation succeeds,\n\t\t// the created connection should be added to pool for future reuse\n\n\t\tslowDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\t// Simulate delay for connection creation\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\treturn newDummyConn(), nil\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             slowDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 1,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tPoolTimeout:        150 * time.Millisecond, // Short timeout for waiting Turn\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Fill the pool with one connection\n\t\tconn1, err := testPool.Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t// Don't put it back yet, so pool is full\n\n\t\t// Start a goroutine that will create a new connection but take time\n\t\tdone1 := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tdefer close(done1)\n\t\t\t// This will trigger queuedNewConn since pool is full\n\t\t\tconn, err := testPool.Get(ctx)\n\t\t\tif err == nil {\n\t\t\t\t// Put connection back to pool after creation\n\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\t\ttestPool.Put(ctx, conn)\n\t\t\t}\n\t\t}()\n\n\t\t// Wait a bit to let the goroutine start and begin connection creation\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t// Now make a request that should timeout waiting for Turn\n\t\tstart := time.Now()\n\t\t_, err = testPool.Get(ctx)\n\t\tduration := time.Since(start)\n\n\t\tExpect(err).To(Equal(pool.ErrPoolTimeout))\n\t\t// Should timeout around PoolTimeout\n\t\tExpect(duration).To(BeNumerically(\"~\", 150*time.Millisecond, 50*time.Millisecond))\n\n\t\t// Release the first connection to allow the background creation to complete\n\t\ttestPool.Put(ctx, conn1)\n\n\t\t// Wait for background connection creation to complete\n\t\t<-done1\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// CORE TEST: Verify connection reuse mechanism\n\t\t// The connection created in background should now be available in pool\n\t\tstart = time.Now()\n\t\tconn3, err := testPool.Get(ctx)\n\t\tduration = time.Since(start)\n\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(conn3).NotTo(BeNil())\n\t\t// Should be fast since connection is from pool (not newly created)\n\t\tExpect(duration).To(BeNumerically(\"<\", 50*time.Millisecond))\n\n\t\ttestPool.Put(ctx, conn3)\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0))\n\n\t\t// Verify queue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0))\n\n\t\t// Verify connection counts are correct\n\t\tstats := testPool.Stats()\n\t\tExpect(stats.TotalConns).To(Equal(uint32(1)),\n\t\t\t\"Should have 1 total connection\")\n\t\tExpect(stats.IdleConns).To(Equal(uint32(1)),\n\t\t\t\"Connection should be idle\")\n\t})\n\n\tIt(\"recover queuedNewConn panic\", func() {\n\t\topt := &pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\tpanic(\"test panic in queuedNewConn\")\n\t\t\t},\n\t\t\tPoolSize:           int32(10),\n\t\t\tMaxConcurrentDials: 10,\n\t\t\tDialTimeout:        1 * time.Second,\n\t\t\tPoolTimeout:        1 * time.Second,\n\t\t}\n\t\ttestPool := pool.NewConnPool(opt)\n\t\tdefer testPool.Close()\n\n\t\t// Trigger queuedNewConn - calling Get() on empty pool will trigger it\n\t\t// Since dialer will panic, it should be handled by recover\n\t\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\n\t\tdefer cancel()\n\n\t\t// Try to get connections multiple times, each will trigger panic but should be properly recovered\n\t\tfor i := 0; i < 3; i++ {\n\t\t\tconn, err := testPool.Get(ctx)\n\t\t\t// Connection should be nil, error should exist (panic converted to error)\n\t\t\tExpect(conn).To(BeNil())\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t}\n\n\t\t// Verify state after panic recovery:\n\t\t// - turn should be properly released (QueueLen() == 0)\n\t\t// - connection counts should be correct (TotalConns == 0, IdleConns == 0)\n\t\t// - dialsQueue should be empty\n\t\tEventually(func() bool {\n\t\t\tstats := testPool.Stats()\n\t\t\tqueueLen := testPool.QueueLen()\n\t\t\tdialsQueueLen := testPool.DialsQueueLen()\n\t\t\treturn stats.TotalConns == 0 && stats.IdleConns == 0 && queueLen == 0 && dialsQueueLen == 0\n\t\t}, \"3s\", \"50ms\").Should(BeTrue(),\n\t\t\t\"After panic recovery, all resources should be cleaned up\")\n\t})\n\n\tIt(\"should handle connection creation success but delivery failure (putIdleConn path)\", func() {\n\t\t// This test covers the most important untested branch in queuedNewConn:\n\t\t// cnErr == nil && !delivered -> putIdleConn()\n\n\t\t// Use slow dialer to ensure request times out before connection is ready\n\t\tslowDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\t// Delay long enough for client request to timeout first\n\t\t\ttime.Sleep(300 * time.Millisecond)\n\t\t\treturn newDummyConn(), nil\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             slowDialer,\n\t\t\tPoolSize:           1,\n\t\t\tMaxConcurrentDials: 2,\n\t\t\tDialTimeout:        500 * time.Millisecond, // Long enough for dialer to complete\n\t\t\tPoolTimeout:        100 * time.Millisecond, // Client requests will timeout quickly\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Record initial idle connection count\n\t\tinitialIdleConns := testPool.Stats().IdleConns\n\n\t\t// Make a request that will timeout\n\t\t// This request will start queuedNewConn, create connection, but fail to deliver due to timeout\n\t\tshortCtx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)\n\t\tdefer cancel()\n\n\t\tconn, err := testPool.Get(shortCtx)\n\n\t\t// Request should fail due to timeout\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(conn).To(BeNil())\n\n\t\t// However, background queuedNewConn should continue and complete connection creation\n\t\t// Since it cannot deliver (request timed out), it should call putIdleConn to add connection to idle pool\n\t\tEventually(func() bool {\n\t\t\tstats := testPool.Stats()\n\t\t\treturn stats.IdleConns > initialIdleConns\n\t\t}, \"1s\", \"50ms\").Should(BeTrue())\n\n\t\t// Verify the connection can indeed be used by subsequent requests\n\t\tconn2, err2 := testPool.Get(context.Background())\n\t\tExpect(err2).NotTo(HaveOccurred())\n\t\tExpect(conn2).NotTo(BeNil())\n\t\tExpect(conn2.IsUsable()).To(BeTrue())\n\n\t\t// Cleanup\n\t\ttestPool.Put(context.Background(), conn2)\n\n\t\t// Verify turn is released after putIdleConn path completes\n\t\t// This is critical: ensures freeTurn() was called in the putIdleConn branch\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0),\n\t\t\t\"Turn should be released after putIdleConn path completes\")\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty - timed out request should be dequeued\")\n\n\t\t// Verify connection counts are correct\n\t\tstats := testPool.Stats()\n\t\tExpect(stats.IdleConns).To(Equal(uint32(1)),\n\t\t\t\"Connection should be in idle pool for reuse\")\n\t\tExpect(stats.TotalConns).To(Equal(uint32(1)))\n\t})\n\n\tIt(\"should not leak turn when delivering connection via putIdleConn\", func() {\n\t\t// This test verifies that freeTurn() is called when putIdleConn successfully\n\t\t// delivers a connection to another waiting request\n\t\t//\n\t\t// Scenario:\n\t\t// 1. Request A: timeout 150ms, connection creation takes 200ms\n\t\t// 2. Request B: timeout 500ms, connection creation takes 400ms\n\t\t// 3. Both requests enter dialsQueue and start async connection creation\n\t\t// 4. Request A times out at 150ms\n\t\t// 5. Request A's connection completes at 200ms\n\t\t// 6. putIdleConn delivers Request A's connection to Request B\n\t\t// 7. queuedNewConn must call freeTurn()\n\t\t// 8. Check: QueueLen should be 1 (only B holding turn), not 2 (A's turn leaked)\n\n\t\tcallCount := int32(0)\n\n\t\tcontrolledDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\tcount := atomic.AddInt32(&callCount, 1)\n\t\t\tif count == 1 {\n\t\t\t\t// Request A's connection: takes 200ms\n\t\t\t\ttime.Sleep(200 * time.Millisecond)\n\t\t\t} else {\n\t\t\t\t// Request B's connection: takes 400ms (longer, so A's connection is used)\n\t\t\t\ttime.Sleep(400 * time.Millisecond)\n\t\t\t}\n\t\t\treturn newDummyConn(), nil\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             controlledDialer,\n\t\t\tPoolSize:           2, // Allows both requests to get turns\n\t\t\tMaxConcurrentDials: 2, // Allows both connections to be created simultaneously\n\t\t\tDialTimeout:        500 * time.Millisecond,\n\t\t\tPoolTimeout:        1 * time.Second,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Verify initial state\n\t\tExpect(testPool.QueueLen()).To(Equal(0))\n\n\t\t// Request A: Short timeout (150ms), connection takes 200ms\n\t\treqADone := make(chan error, 1)\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tshortCtx, cancel := context.WithTimeout(ctx, 150*time.Millisecond)\n\t\t\tdefer cancel()\n\t\t\t_, err := testPool.Get(shortCtx)\n\t\t\treqADone <- err\n\t\t}()\n\n\t\t// Wait for Request A to acquire turn and enter dialsQueue\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\tExpect(testPool.QueueLen()).To(Equal(1), \"Request A should occupy turn\")\n\n\t\t// Request B: Long timeout (500ms), will receive Request A's connection\n\t\treqBDone := make(chan struct{})\n\t\tvar reqBConn *pool.Conn\n\t\tvar reqBErr error\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tlongCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)\n\t\t\tdefer cancel()\n\t\t\treqBConn, reqBErr = testPool.Get(longCtx)\n\t\t\tclose(reqBDone)\n\t\t}()\n\n\t\t// Wait for Request B to acquire turn and enter dialsQueue\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\tExpect(testPool.QueueLen()).To(Equal(2), \"Both requests should occupy turns\")\n\n\t\t// Request A times out at 150ms\n\t\treqAErr := <-reqADone\n\t\tExpect(reqAErr).To(HaveOccurred(), \"Request A should timeout\")\n\n\t\t// Request A's connection completes at 200ms\n\t\t// putIdleConn delivers it to Request B via tryDeliver\n\t\t// queuedNewConn MUST call freeTurn() to release Request A's turn\n\t\t<-reqBDone\n\t\tExpect(reqBErr).NotTo(HaveOccurred(), \"Request B should receive Request A's connection\")\n\t\tExpect(reqBConn).NotTo(BeNil())\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"500ms\", \"50ms\").Should(BeNumerically(\"<=\", 1),\n\t\t\t\"Request A's wantConn should be dequeued after delivery to Request B\")\n\n\t\t// FIRST CRITICAL CHECK: Turn state after connection delivery\n\t\t// After Request B receives connection from putIdleConn:\n\t\t// - Request A's turn is held by Request B (connection delivered)\n\t\t// - Request B's turn is still held by Request B's dial to complete the connection\n\t\t// Expected QueueLen: 2 (Request B holding turn for connection usage)\n\t\ttime.Sleep(100 * time.Millisecond) // ~300ms total\n\t\tExpect(testPool.QueueLen()).To(Equal(2))\n\n\t\t// SECOND CRITICAL CHECK: Turn release after dial completion\n\t\t// Wait for Request B's dial result to complete\n\t\ttime.Sleep(300 * time.Millisecond) // ~600ms total\n\t\tExpect(testPool.QueueLen()).To(Equal(1))\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0),\n\t\t\t\"All wantConn should be dequeued after connections are delivered\")\n\n\t\t// Cleanup and verify turn is released\n\t\ttestPool.Put(ctx, reqBConn)\n\t\tEventually(func() int { return testPool.QueueLen() }, \"600ms\").Should(Equal(0))\n\t})\n\t// Test for race condition where nil context can be passed to newConn\n\t// This reproduces the issue reported in GitHub where queuedNewConn panics\n\t// with \"cannot create context from nil parent\"\n\tIt(\"should handle nil context race condition in queuedNewConn\", func() {\n\t\t// Create a pool with very short timeouts to trigger the race condition\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\t\t// Add a small delay to increase chance of race condition\n\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\t\treturn dummyDialer(ctx)\n\t\t\t},\n\t\t\tPoolSize:           int32(10),\n\t\t\tMaxConcurrentDials: 10,\n\t\t\tPoolTimeout:        10 * time.Millisecond, // Very short timeout\n\t\t\tDialTimeout:        100 * time.Millisecond,\n\t\t\tConnMaxIdleTime:    time.Millisecond,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Try to trigger the race condition by making many concurrent requests\n\t\t// with short timeouts\n\t\tconst numGoroutines = 50\n\t\tvar wg sync.WaitGroup\n\t\terrors := make(chan error, numGoroutines)\n\n\t\tfor i := 0; i < numGoroutines; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\t// Use a very short context timeout to trigger the race\n\t\t\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)\n\t\t\t\tdefer cancel()\n\n\t\t\t\t_, err := testPool.Get(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// We expect timeout errors, but not panics\n\t\t\t\t\terrors <- err\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\t\tclose(errors)\n\n\t\t// Check that we got timeout errors (expected) but no panics\n\t\t// The test passes if it doesn't panic\n\t\ttimeoutCount := 0\n\t\tfor err := range errors {\n\t\t\tif err == context.DeadlineExceeded || err == pool.ErrPoolTimeout {\n\t\t\t\ttimeoutCount++\n\t\t\t}\n\t\t}\n\n\t\t// We should have at least some timeouts due to the short timeout\n\t\tExpect(timeoutCount).To(BeNumerically(\">\", 0))\n\n\t\t// Verify all asynchronous operations are completed\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t// Verify no resources are leaked\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"2s\", \"50ms\").Should(Equal(0),\n\t\t\t\"All turns should be released after race condition test\")\n\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"2s\", \"50ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty - no zombie wantConn\")\n\n\t\t// Verify connection counts are correct\n\t\tstats := testPool.Stats()\n\t\tExpect(stats.TotalConns).To(BeNumerically(\">=\", 0))\n\t\tEventually(func() uint32 {\n\t\t\treturn testPool.Stats().IdleConns + testPool.Stats().StaleConns\n\t\t}, \"2s\", \"50ms\").Should(Equal(stats.TotalConns),\n\t\t\t\"All connections should be accounted for\")\n\t})\n\n\tIt(\"should cleanup dialsQueue under high concurrency with continuous dial failures\", func() {\n\t\talwaysFailDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\treturn nil, fmt.Errorf(\"network unreachable\")\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             alwaysFailDialer,\n\t\t\tPoolSize:           100,\n\t\t\tMaxConcurrentDials: 100,\n\t\t\tDialTimeout:        50 * time.Millisecond,\n\t\t\tPoolTimeout:        30 * time.Millisecond,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Make many concurrent requests\n\t\tconst totalRequests = 1000\n\t\tvar wg sync.WaitGroup\n\n\t\tfor i := 0; i < totalRequests; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_, _ = testPool.Get(context.Background())\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"2s\", \"100ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty after all failed requests complete\")\n\n\t\t// Verify turn is released\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0))\n\n\t\t// Verify connection counts are correct\n\t\tstats := testPool.Stats()\n\t\tExpect(stats.TotalConns).To(Equal(uint32(0)),\n\t\t\t\"No connections should exist after all failures\")\n\t})\n\n\tIt(\"should cleanup zombie wantConn when request times out and dial fails\", func() {\n\t\t// Delayed failed dialer\n\t\tslowFailDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\ttime.Sleep(100 * time.Millisecond) // Exceed request timeout\n\t\t\treturn nil, fmt.Errorf(\"dial failed\")\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             slowFailDialer,\n\t\t\tPoolSize:           10,\n\t\t\tMaxConcurrentDials: 10,\n\t\t\tDialTimeout:        200 * time.Millisecond,\n\t\t\tPoolTimeout:        50 * time.Millisecond, // Request timeout quickly\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Make many requests with quick timeout\n\t\tconst numRequests = 100\n\t\tvar wg sync.WaitGroup\n\n\t\tfor i := 0; i < numRequests; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tshortCtx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)\n\t\t\t\tdefer cancel()\n\t\t\t\t_, _ = testPool.Get(shortCtx)\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Wait for asynchronous connection creation to complete\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t// Verify dialsQueue is empty\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"2s\", \"100ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty even when requests timeout and dials fail\")\n\n\t\t// Verify turn is released\n\t\tEventually(func() int {\n\t\t\treturn testPool.QueueLen()\n\t\t}, \"1s\", \"50ms\").Should(Equal(0))\n\t})\n\n\tIt(\"should handle intermittent dial failures without queue accumulation\", func() {\n\t\tcallCount := int64(0)\n\n\t\tintermittentDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\tcount := atomic.AddInt64(&callCount, 1)\n\t\t\tif count%2 == 0 {\n\t\t\t\treturn nil, fmt.Errorf(\"network timeout\")\n\t\t\t}\n\t\t\treturn newDummyConn(), nil\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             intermittentDialer,\n\t\t\tPoolSize:           50,\n\t\t\tMaxConcurrentDials: 50,\n\t\t\tDialTimeout:        100 * time.Millisecond,\n\t\t\tPoolTimeout:        50 * time.Millisecond,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\t// Send requests continuously for 5 seconds\n\t\tconst duration = 5 * time.Second\n\t\tstart := time.Now()\n\t\tvar wg sync.WaitGroup\n\n\t\tfor time.Since(start) < duration {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_, _ = testPool.Get(context.Background())\n\t\t\t}()\n\t\t\ttime.Sleep(10 * time.Millisecond) // Control request rate\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Verify dialsQueue does not accumulate indefinitely\n\t\tmaxExpectedQueueLen := int(testPool.Size()) * 2\n\t\tqueueLen := testPool.DialsQueueLen()\n\t\tExpect(queueLen).To(BeNumerically(\"<=\", maxExpectedQueueLen),\n\t\t\tfmt.Sprintf(\"dialsQueue length (%d) should not exceed reasonable limit (%d)\",\n\t\t\t\tqueueLen, maxExpectedQueueLen))\n\n\t\t// Wait for final cleanup\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"2s\", \"100ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should eventually be cleaned up\")\n\t})\n\n\tIt(\"should enforce dialsQueue upper bound\", func() {\n\t\talwaysFailDialer := func(ctx context.Context) (net.Conn, error) {\n\t\t\treturn nil, fmt.Errorf(\"dial failed\")\n\t\t}\n\n\t\ttestPool := pool.NewConnPool(&pool.Options{\n\t\t\tDialer:             alwaysFailDialer,\n\t\t\tPoolSize:           100,\n\t\t\tMaxConcurrentDials: 100,\n\t\t\tDialTimeout:        50 * time.Millisecond,\n\t\t\tPoolTimeout:        30 * time.Millisecond,\n\t\t})\n\t\tdefer testPool.Close()\n\n\t\tconst numRequests = 1000\n\t\tmaxExpectedQueueLen := int(testPool.Size()) * 2 // Reasonable upper bound\n\n\t\tvar wg sync.WaitGroup\n\t\tmaxObservedQueueLen := int32(0)\n\n\t\t// Send many failed requests concurrently\n\t\tfor i := 0; i < numRequests; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_, _ = testPool.Get(context.Background())\n\t\t\t}()\n\n\t\t\t// Check queue length periodically\n\t\t\tif i%50 == 0 {\n\t\t\t\tqueueLen := testPool.DialsQueueLen()\n\t\t\t\tif queueLen > int(atomic.LoadInt32(&maxObservedQueueLen)) {\n\t\t\t\t\tatomic.StoreInt32(&maxObservedQueueLen, int32(queueLen))\n\t\t\t\t}\n\t\t\t\tExpect(queueLen).To(BeNumerically(\"<=\", maxExpectedQueueLen),\n\t\t\t\t\tfmt.Sprintf(\"Queue length (%d) should not exceed limit (%d)\",\n\t\t\t\t\t\tqueueLen, maxExpectedQueueLen))\n\t\t\t}\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Verify final cleanup\n\t\tEventually(func() int {\n\t\t\treturn testPool.DialsQueueLen()\n\t\t}, \"2s\", \"100ms\").Should(Equal(0),\n\t\t\t\"dialsQueue should be empty after all requests complete\")\n\t})\n\n\tDescribe(\"calcConnExpiresAt\", func() {\n\t\t// Case 1: lifetime <= 0 returns noExpiration\n\t\tIt(\"returns noExpiration when ConnMaxLifetime is not positive\", func() {\n\t\t\tp := pool.NewConnPool(&pool.Options{\n\t\t\t\tDialer:                dummyDialer,\n\t\t\t\tPoolSize:              1,\n\t\t\t\tConnMaxLifetime:       0,\n\t\t\t\tConnMaxLifetimeJitter: 0,\n\t\t\t})\n\t\t\tdefer p.Close()\n\t\t\tExpect(p.CalcConnExpiresAt()).To(Equal(pool.NoExpiration))\n\t\t})\n\n\t\t// Case 2: lifetime > 0, jitter <= 0 returns exact lifetime\n\t\tIt(\"returns exact lifetime when jitter is zero\", func() {\n\t\t\tlifetime := 1 * time.Hour\n\t\t\tp := pool.NewConnPool(&pool.Options{\n\t\t\t\tDialer:                dummyDialer,\n\t\t\t\tPoolSize:              1,\n\t\t\t\tConnMaxLifetime:       lifetime,\n\t\t\t\tConnMaxLifetimeJitter: 0,\n\t\t\t})\n\t\t\tdefer p.Close()\n\n\t\t\tbefore := time.Now()\n\t\t\texpiresAt := p.CalcConnExpiresAt()\n\t\t\tafter := time.Now()\n\n\t\t\tExpect(expiresAt).To(BeTemporally(\">=\", before.Add(lifetime)))\n\t\t\tExpect(expiresAt).To(BeTemporally(\"<=\", after.Add(lifetime)))\n\t\t})\n\n\t\t// Case 3: lifetime > 0, jitter > 0 returns value in jitter range\n\t\tIt(\"returns value in jitter range when jitter is positive\", func() {\n\t\t\tlifetime := 1 * time.Hour\n\t\t\tjitter := 6 * time.Minute\n\t\t\tp := pool.NewConnPool(&pool.Options{\n\t\t\t\tDialer:                dummyDialer,\n\t\t\t\tPoolSize:              1,\n\t\t\t\tConnMaxLifetime:       lifetime,\n\t\t\t\tConnMaxLifetimeJitter: jitter,\n\t\t\t})\n\t\t\tdefer p.Close()\n\n\t\t\tbefore := time.Now()\n\t\t\texpiresAt := p.CalcConnExpiresAt()\n\n\t\t\tExpect(expiresAt).To(BeTemporally(\">=\", before.Add(lifetime-jitter)))\n\t\t\tExpect(expiresAt).To(BeTemporally(\"<=\", before.Add(lifetime+jitter)))\n\t\t})\n\t})\n})\n\nfunc init() {\n\tlogging.Disable()\n}\n"
  },
  {
    "path": "internal/pool/pubsub.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\ntype PubSubStats struct {\n\tCreated   uint32\n\tUntracked uint32\n\tActive    uint32\n}\n\n// PubSubPool manages a pool of PubSub connections.\ntype PubSubPool struct {\n\topt       *Options\n\tnetDialer func(ctx context.Context, network, addr string) (net.Conn, error)\n\n\t// Map to track active PubSub connections\n\tactiveConns sync.Map // map[uint64]*Conn (connID -> conn)\n\tclosed      atomic.Bool\n\tstats       PubSubStats\n}\n\n// NewPubSubPool implements a pool for PubSub connections.\n// It intentionally does not implement the Pooler interface\nfunc NewPubSubPool(opt *Options, netDialer func(ctx context.Context, network, addr string) (net.Conn, error)) *PubSubPool {\n\treturn &PubSubPool{\n\t\topt:       opt,\n\t\tnetDialer: netDialer,\n\t}\n}\n\nfunc (p *PubSubPool) NewConn(ctx context.Context, network string, addr string, channels []string) (*Conn, error) {\n\tif p.closed.Load() {\n\t\treturn nil, ErrClosed\n\t}\n\n\tnetConn, err := p.netDialer(ctx, network, addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcn := NewConnWithBufferSize(netConn, p.opt.ReadBufferSize, p.opt.WriteBufferSize)\n\tcn.pubsub = true\n\t// Set pool name for metrics\n\tcn.SetPoolName(p.opt.Name)\n\tatomic.AddUint32(&p.stats.Created, 1)\n\treturn cn, nil\n}\n\nfunc (p *PubSubPool) TrackConn(cn *Conn) {\n\tatomic.AddUint32(&p.stats.Active, 1)\n\tp.activeConns.Store(cn.GetID(), cn)\n}\n\nfunc (p *PubSubPool) UntrackConn(cn *Conn) {\n\tatomic.AddUint32(&p.stats.Active, ^uint32(0))\n\tatomic.AddUint32(&p.stats.Untracked, 1)\n\tp.activeConns.Delete(cn.GetID())\n}\n\nfunc (p *PubSubPool) Close() error {\n\tp.closed.Store(true)\n\tp.activeConns.Range(func(key, value interface{}) bool {\n\t\tcn := value.(*Conn)\n\t\t_ = cn.Close()\n\t\treturn true\n\t})\n\treturn nil\n}\n\nfunc (p *PubSubPool) Stats() *PubSubStats {\n\t// load stats atomically\n\treturn &PubSubStats{\n\t\tCreated:   atomic.LoadUint32(&p.stats.Created),\n\t\tUntracked: atomic.LoadUint32(&p.stats.Untracked),\n\t\tActive:    atomic.LoadUint32(&p.stats.Active),\n\t}\n}\n"
  },
  {
    "path": "internal/pool/try_dial_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestTryDial_AppliesDialTimeoutWhenSet(t *testing.T) {\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\tif _, ok := ctx.Deadline(); !ok {\n\t\t\t\treturn nil, errors.New(\"expected deadline in tryDial\")\n\t\t\t}\n\t\t\tc1, c2 := net.Pipe()\n\t\t\t_ = c2.Close()\n\t\t\treturn c1, nil\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        200 * time.Millisecond,\n\t})\n\tdefer p.Close()\n\n\tp.tryDial()\n}\n\nfunc TestTryDial_DoesNotApplyDialTimeoutWhenDisabled(t *testing.T) {\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\tif _, ok := ctx.Deadline(); ok {\n\t\t\t\treturn nil, errors.New(\"unexpected deadline in tryDial when DialTimeout disabled\")\n\t\t\t}\n\t\t\t// Ensure context is still a real context.\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tc1, c2 := net.Pipe()\n\t\t\t_ = c2.Close()\n\t\t\treturn c1, nil\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        0,\n\t})\n\tdefer p.Close()\n\n\tp.tryDial()\n}\n\nfunc TestTryDial_RespectsPoolClose(t *testing.T) {\n\t// If Dialer keeps failing, tryDial should exit once the pool is closed.\n\tp := NewConnPool(&Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t},\n\t\tPoolSize:           1,\n\t\tMaxConcurrentDials: 1,\n\t\tDialTimeout:        10 * time.Millisecond,\n\t})\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tp.tryDial()\n\t\tclose(done)\n\t}()\n\n\ttime.Sleep(20 * time.Millisecond)\n\t_ = p.Close()\n\n\tselect {\n\tcase <-done:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatalf(\"tryDial did not exit after pool close\")\n\t}\n}\n"
  },
  {
    "path": "internal/pool/want_conn.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\ntype wantConn struct {\n\tmu        sync.RWMutex    // protects ctx, done and sending of the result\n\tctx       context.Context // context for dial, cleared after delivered or canceled\n\tcancelCtx context.CancelFunc\n\tdone      bool                // true after delivered or canceled\n\tresult    chan wantConnResult // channel to deliver connection or error\n}\n\n// getCtxForDial returns context for dial or nil if connection was delivered or canceled.\nfunc (w *wantConn) getCtxForDial() context.Context {\n\tw.mu.RLock()\n\tdefer w.mu.RUnlock()\n\n\treturn w.ctx\n}\n\nfunc (w *wantConn) tryDeliver(cn *Conn, err error) bool {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\tif w.done {\n\t\treturn false\n\t}\n\n\tw.done = true\n\tw.ctx = nil\n\n\tw.result <- wantConnResult{cn: cn, err: err}\n\tclose(w.result)\n\n\treturn true\n}\n\nfunc (w *wantConn) cancel() *Conn {\n\tw.mu.Lock()\n\tvar cn *Conn\n\tif w.done {\n\t\tselect {\n\t\tcase result := <-w.result:\n\t\t\tcn = result.cn\n\t\tdefault:\n\t\t}\n\t} else {\n\t\tclose(w.result)\n\t}\n\n\tw.done = true\n\tw.ctx = nil\n\tw.mu.Unlock()\n\n\treturn cn\n}\n\nfunc (w *wantConn) isOngoing() bool {\n\tw.mu.RLock()\n\tdefer w.mu.RUnlock()\n\treturn !w.done\n}\n\ntype wantConnResult struct {\n\tcn  *Conn\n\terr error\n}\n\ntype wantConnQueue struct {\n\tmu    sync.RWMutex\n\titems []*wantConn\n}\n\nfunc newWantConnQueue() *wantConnQueue {\n\treturn &wantConnQueue{\n\t\titems: make([]*wantConn, 0),\n\t}\n}\n\nfunc (q *wantConnQueue) enqueue(w *wantConn) {\n\tq.mu.Lock()\n\tdefer q.mu.Unlock()\n\tq.items = append(q.items, w)\n}\n\nfunc (q *wantConnQueue) dequeue() (*wantConn, bool) {\n\tq.mu.Lock()\n\tdefer q.mu.Unlock()\n\n\tif len(q.items) == 0 {\n\t\treturn nil, false\n\t}\n\n\titem := q.items[0]\n\tq.items = q.items[1:]\n\treturn item, true\n}\n\nfunc (q *wantConnQueue) discardDoneAtFront() int {\n\tq.mu.Lock()\n\tdefer q.mu.Unlock()\n\tcount := 0\n\tfor len(q.items) > 0 {\n\t\tif q.items[0].isOngoing() {\n\t\t\tbreak\n\t\t}\n\n\t\tq.items = q.items[1:]\n\t\tcount++\n\t}\n\n\treturn count\n}\n"
  },
  {
    "path": "internal/pool/want_conn_test.go",
    "content": "package pool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc (q *wantConnQueue) len() int {\n\tq.mu.RLock()\n\tdefer q.mu.RUnlock()\n\treturn len(q.items)\n}\n\nfunc TestWantConn_getCtxForDial(t *testing.T) {\n\tctx := context.Background()\n\tw := &wantConn{\n\t\tctx:    ctx,\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\n\t// Test getting context when not done\n\tgotCtx := w.getCtxForDial()\n\tif gotCtx != ctx {\n\t\tt.Errorf(\"getCtxForDial() = %v, want %v\", gotCtx, ctx)\n\t}\n\n\t// Test getting context when done\n\tw.mu.Lock()\n\tw.done = true\n\tw.ctx = nil\n\tw.mu.Unlock()\n\tgotCtx = w.getCtxForDial()\n\tif gotCtx != nil {\n\t\tt.Errorf(\"getCtxForDial() after done = %v, want nil\", gotCtx)\n\t}\n}\n\nfunc TestWantConn_tryDeliver_Success(t *testing.T) {\n\tw := &wantConn{\n\t\tctx:    context.Background(),\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\n\t// Create a mock connection\n\tconn := &Conn{}\n\n\t// Test successful delivery\n\tdelivered := w.tryDeliver(conn, nil)\n\tif !delivered {\n\t\tt.Error(\"tryDeliver() = false, want true\")\n\t}\n\n\t// Check that wantConn is marked as done\n\tif w.isOngoing() {\n\t\tt.Error(\"wantConn.done = false, want true after delivery\")\n\t}\n\n\t// Check that context is cleared\n\tif w.getCtxForDial() != nil {\n\t\tt.Error(\"wantConn.ctx should be nil after delivery\")\n\t}\n\n\t// Check that result is sent\n\tselect {\n\tcase result := <-w.result:\n\t\tif result.cn != conn {\n\t\t\tt.Errorf(\"result.cn = %v, want %v\", result.cn, conn)\n\t\t}\n\t\tif result.err != nil {\n\t\t\tt.Errorf(\"result.err = %v, want nil\", result.err)\n\t\t}\n\tcase <-time.After(time.Millisecond):\n\t\tt.Error(\"Expected result to be sent to channel\")\n\t}\n}\n\nfunc TestWantConn_tryDeliver_WithError(t *testing.T) {\n\tw := &wantConn{\n\t\tctx:    context.Background(),\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\n\ttestErr := errors.New(\"test error\")\n\n\t// Test delivery with error\n\tdelivered := w.tryDeliver(nil, testErr)\n\tif !delivered {\n\t\tt.Error(\"tryDeliver() = false, want true\")\n\t}\n\n\t// Check result\n\tselect {\n\tcase result := <-w.result:\n\t\tif result.cn != nil {\n\t\t\tt.Errorf(\"result.cn = %v, want nil\", result.cn)\n\t\t}\n\t\tif result.err != testErr {\n\t\t\tt.Errorf(\"result.err = %v, want %v\", result.err, testErr)\n\t\t}\n\tcase <-time.After(time.Millisecond):\n\t\tt.Error(\"Expected result to be sent to channel\")\n\t}\n}\n\nfunc TestWantConn_tryDeliver_AlreadyDone(t *testing.T) {\n\tw := &wantConn{\n\t\tctx:    context.Background(),\n\t\tdone:   true, // Already done\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\n\t// Test delivery when already done\n\tdelivered := w.tryDeliver(&Conn{}, nil)\n\tif delivered {\n\t\tt.Error(\"tryDeliver() = true, want false when already done\")\n\t}\n\n\t// Check that no result is sent\n\tselect {\n\tcase <-w.result:\n\t\tt.Error(\"No result should be sent when already done\")\n\tcase <-time.After(time.Millisecond):\n\t\t// Expected\n\t}\n}\n\nfunc TestWantConn_cancel_NotDone(t *testing.T) {\n\tw := &wantConn{\n\t\tctx:    context.Background(),\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\n\t// Test cancel when not done\n\tcn := w.cancel()\n\n\t// Should return nil since no connection was not delivered\n\tif cn != nil {\n\t\tt.Errorf(\"cancel()= %v, want nil when no connection delivered\", cn)\n\t}\n\n\t// Check that wantConn is marked as done\n\tif w.isOngoing() {\n\t\tt.Error(\"wantConn.done = false, want true after cancel\")\n\t}\n\n\t// Check that context is cleared\n\tif w.getCtxForDial() != nil {\n\t\tt.Error(\"wantConn.ctx should be nil after cancel\")\n\t}\n\n\t// Check that channel is closed\n\tselect {\n\tcase _, ok := <-w.result:\n\t\tif ok {\n\t\t\tt.Error(\"result channel should be closed after cancel\")\n\t\t}\n\tcase <-time.After(time.Millisecond):\n\t\tt.Error(\"Expected channel to be closed\")\n\t}\n}\n\nfunc TestWantConn_cancel_AlreadyDone(t *testing.T) {\n\tw := &wantConn{\n\t\tctx:    context.Background(),\n\t\tdone:   true,\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\n\t// Put a result in the channel without connection (to avoid nil pointer issues)\n\ttestErr := errors.New(\"test error\")\n\tw.result <- wantConnResult{cn: nil, err: testErr}\n\n\t// Test cancel when already done\n\tcn := w.cancel()\n\n\t// Should return nil since the result had no connection\n\tif cn != nil {\n\t\tt.Errorf(\"cancel()= %v, want nil when result had no connection\", cn)\n\t}\n\n\t// Check that wantConn remains done\n\tif w.isOngoing() {\n\t\tt.Error(\"wantConn.done = false, want true\")\n\t}\n\n\t// Check that context is cleared\n\tif w.getCtxForDial() != nil {\n\t\tt.Error(\"wantConn.ctx should be nil after cancel\")\n\t}\n}\n\nfunc TestWantConnQueue_newWantConnQueue(t *testing.T) {\n\tq := newWantConnQueue()\n\tif q == nil {\n\t\tt.Fatal(\"newWantConnQueue() returned nil\")\n\t}\n\tif q.items == nil {\n\t\tt.Error(\"queue items should be initialized\")\n\t}\n\tif len(q.items) != 0 {\n\t\tt.Errorf(\"new queue length = %d, want 0\", len(q.items))\n\t}\n}\n\nfunc TestWantConnQueue_enqueue_dequeue(t *testing.T) {\n\tq := newWantConnQueue()\n\n\t// Test dequeue from empty queue\n\titem, ok := q.dequeue()\n\tif ok {\n\t\tt.Error(\"dequeue() from empty queue should return false\")\n\t}\n\tif item != nil {\n\t\tt.Error(\"dequeue() from empty queue should return nil\")\n\t}\n\n\t// Create test wantConn items\n\tw1 := &wantConn{ctx: context.Background(), result: make(chan wantConnResult, 1)}\n\tw2 := &wantConn{ctx: context.Background(), result: make(chan wantConnResult, 1)}\n\tw3 := &wantConn{ctx: context.Background(), result: make(chan wantConnResult, 1)}\n\n\t// Test enqueue\n\tq.enqueue(w1)\n\tq.enqueue(w2)\n\tq.enqueue(w3)\n\n\t// Test FIFO behavior\n\titem, ok = q.dequeue()\n\tif !ok {\n\t\tt.Error(\"dequeue() should return true when queue has items\")\n\t}\n\tif item != w1 {\n\t\tt.Errorf(\"dequeue() = %v, want %v (FIFO order)\", item, w1)\n\t}\n\n\titem, ok = q.dequeue()\n\tif !ok {\n\t\tt.Error(\"dequeue() should return true when queue has items\")\n\t}\n\tif item != w2 {\n\t\tt.Errorf(\"dequeue() = %v, want %v (FIFO order)\", item, w2)\n\t}\n\n\titem, ok = q.dequeue()\n\tif !ok {\n\t\tt.Error(\"dequeue() should return true when queue has items\")\n\t}\n\tif item != w3 {\n\t\tt.Errorf(\"dequeue() = %v, want %v (FIFO order)\", item, w3)\n\t}\n\n\t// Test dequeue from empty queue again\n\titem, ok = q.dequeue()\n\tif ok {\n\t\tt.Error(\"dequeue() from empty queue should return false\")\n\t}\n\tif item != nil {\n\t\tt.Error(\"dequeue() from empty queue should return nil\")\n\t}\n}\n\nfunc TestWantConnQueue_ConcurrentAccess(t *testing.T) {\n\tq := newWantConnQueue()\n\tconst numWorkers = 10\n\tconst itemsPerWorker = 100\n\n\tvar wg sync.WaitGroup\n\n\t// Start enqueuers\n\tfor i := 0; i < numWorkers; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < itemsPerWorker; j++ {\n\t\t\t\tw := &wantConn{\n\t\t\t\t\tctx:    context.Background(),\n\t\t\t\t\tresult: make(chan wantConnResult, 1),\n\t\t\t\t}\n\t\t\t\tq.enqueue(w)\n\t\t\t}\n\t\t}()\n\t}\n\n\t// Start dequeuers\n\tdequeued := make(chan *wantConn, numWorkers*itemsPerWorker)\n\tfor i := 0; i < numWorkers; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < itemsPerWorker; j++ {\n\t\t\t\tfor {\n\t\t\t\t\tif item, ok := q.dequeue(); ok {\n\t\t\t\t\t\tdequeued <- item\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\t// Small delay to avoid busy waiting\n\t\t\t\t\ttime.Sleep(time.Microsecond)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\tclose(dequeued)\n\n\t// Count dequeued items\n\tcount := 0\n\tfor range dequeued {\n\t\tcount++\n\t}\n\n\texpectedCount := numWorkers * itemsPerWorker\n\tif count != expectedCount {\n\t\tt.Errorf(\"dequeued %d items, want %d\", count, expectedCount)\n\t}\n\n\t// Queue should be empty\n\tif item, ok := q.dequeue(); ok {\n\t\tt.Errorf(\"queue should be empty but got item: %v\", item)\n\t}\n}\n\nfunc TestWantConnQueue_ThreadSafety(t *testing.T) {\n\tq := newWantConnQueue()\n\tconst numOperations = 1000\n\n\tvar wg sync.WaitGroup\n\terrors := make(chan error, numOperations*2)\n\n\t// Concurrent enqueue operations\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < numOperations; i++ {\n\t\t\tw := &wantConn{\n\t\t\t\tctx:    context.Background(),\n\t\t\t\tresult: make(chan wantConnResult, 1),\n\t\t\t}\n\t\t\tq.enqueue(w)\n\t\t}\n\t}()\n\n\t// Concurrent dequeue operations\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tdequeued := 0\n\t\tfor dequeued < numOperations {\n\t\t\tif _, ok := q.dequeue(); ok {\n\t\t\t\tdequeued++\n\t\t\t} else {\n\t\t\t\t// Small delay when queue is empty\n\t\t\t\ttime.Sleep(time.Microsecond)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Wait for completion\n\twg.Wait()\n\tclose(errors)\n\n\t// Check for any errors\n\tfor err := range errors {\n\t\tt.Error(err)\n\t}\n\n\t// Final queue should be empty\n\tif item, ok := q.dequeue(); ok {\n\t\tt.Errorf(\"queue should be empty but got item: %v\", item)\n\t}\n}\n\n// Benchmark tests\nfunc BenchmarkWantConnQueue_Enqueue(b *testing.B) {\n\tq := newWantConnQueue()\n\n\t// Pre-allocate a pool of wantConn to reuse\n\tconst poolSize = 1000\n\twantConnPool := make([]*wantConn, poolSize)\n\tfor i := 0; i < poolSize; i++ {\n\t\twantConnPool[i] = &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t}\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tw := wantConnPool[i%poolSize]\n\t\tq.enqueue(w)\n\t}\n}\n\nfunc BenchmarkWantConnQueue_Dequeue(b *testing.B) {\n\tq := newWantConnQueue()\n\n\t// Use a reasonable fixed size for pre-population to avoid memory issues\n\tconst queueSize = 10000\n\n\t// Pre-populate queue with a fixed reasonable size\n\tfor i := 0; i < queueSize; i++ {\n\t\tw := &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t\tq.enqueue(w)\n\t}\n\n\tb.ResetTimer()\n\n\t// Benchmark dequeue operations, refilling as needed\n\tfor i := 0; i < b.N; i++ {\n\t\tif _, ok := q.dequeue(); !ok {\n\t\t\t// Queue is empty, refill a batch\n\t\t\tfor j := 0; j < 1000; j++ {\n\t\t\t\tw := &wantConn{\n\t\t\t\t\tctx:    context.Background(),\n\t\t\t\t\tresult: make(chan wantConnResult, 1),\n\t\t\t\t}\n\t\t\t\tq.enqueue(w)\n\t\t\t}\n\t\t\t// Dequeue again\n\t\t\tq.dequeue()\n\t\t}\n\t}\n}\n\nfunc BenchmarkWantConnQueue_EnqueueDequeue(b *testing.B) {\n\tq := newWantConnQueue()\n\n\t// Pre-allocate a pool of wantConn to reuse\n\tconst poolSize = 1000\n\twantConnPool := make([]*wantConn, poolSize)\n\tfor i := 0; i < poolSize; i++ {\n\t\twantConnPool[i] = &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t}\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tw := wantConnPool[i%poolSize]\n\t\tq.enqueue(w)\n\t\tq.dequeue()\n\t}\n}\n\n// TestWantConn_RaceConditionNilContext tests the race condition where\n// getCtxForDial can return nil after the context is cancelled.\n// This test verifies that the fix in newConn handles nil context gracefully.\nfunc TestWantConn_RaceConditionNilContext(t *testing.T) {\n\t// This test simulates the race condition described in the issue:\n\t// 1. Main goroutine creates a wantConn with a context\n\t// 2. Background goroutine starts but hasn't called getCtxForDial yet\n\t// 3. Main goroutine times out and calls cancel(), setting w.ctx to nil\n\t// 4. Background goroutine calls getCtxForDial() and gets nil\n\t// 5. Background goroutine calls newConn(nil, true) which should not panic\n\n\tdialCtx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n\tdefer cancel()\n\n\tw := &wantConn{\n\t\tctx:       dialCtx,\n\t\tcancelCtx: cancel,\n\t\tresult:    make(chan wantConnResult, 1),\n\t}\n\n\t// Simulate the race condition by canceling the context\n\t// and then trying to get it\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t// Small delay to ensure cancel happens first\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t// This should return nil after cancel\n\t\tctx := w.getCtxForDial()\n\n\t\t// Verify that we got nil context\n\t\tif ctx != nil {\n\t\t\tt.Errorf(\"Expected nil context after cancel, got %v\", ctx)\n\t\t}\n\t}()\n\n\t// Cancel the context immediately\n\tw.cancel()\n\n\twg.Wait()\n\n\t// Verify the wantConn state\n\tif w.isOngoing() {\n\t\tt.Error(\"wantConn should be marked as done after cancel\")\n\t}\n\tif w.getCtxForDial() != nil {\n\t\tt.Error(\"wantConn.ctx should be nil after cancel\")\n\t}\n}\n\n// TestWantConnQueue_dropFrontDone_EmptyQueue tests dropFrontDone on an empty queue.\nfunc TestWantConnQueue_dropFrontDone_EmptyQueue(t *testing.T) {\n\tq := newWantConnQueue()\n\n\t// Call dropFrontDone on empty queue\n\tcount := q.discardDoneAtFront()\n\n\t// Verify no elements were removed\n\tif count != 0 {\n\t\tt.Errorf(\"dropFrontDone() on empty queue = %d, want 0\", count)\n\t}\n\n\t// Verify queue is still empty\n\tif q.len() != 0 {\n\t\tt.Errorf(\"queue length after dropFrontDone = %d, want 0\", q.len())\n\t}\n}\n\n// TestWantConnQueue_dropFrontDone_AllDone tests dropFrontDone when all elements are done.\nfunc TestWantConnQueue_dropFrontDone_AllDone(t *testing.T) {\n\tq := newWantConnQueue()\n\n\t// Create 3 wantConn items, all marked as done\n\tfor i := 0; i < 3; i++ {\n\t\tw := &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tdone:   true, // Mark as done\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t\tq.enqueue(w)\n\t}\n\n\t// Verify initial queue length\n\tif q.len() != 3 {\n\t\tt.Errorf(\"initial queue length = %d, want 3\", q.len())\n\t}\n\n\t// Call dropFrontDone\n\tcount := q.discardDoneAtFront()\n\n\t// Verify all 3 elements were removed\n\tif count != 3 {\n\t\tt.Errorf(\"dropFrontDone() = %d, want 3\", count)\n\t}\n\n\t// Verify queue is now empty\n\tif q.len() != 0 {\n\t\tt.Errorf(\"queue length after dropFrontDone = %d, want 0\", q.len())\n\t}\n}\n\n// TestWantConnQueue_dropFrontDone_NoneDone tests dropFrontDone when no elements are done.\nfunc TestWantConnQueue_dropFrontDone_NoneDone(t *testing.T) {\n\tq := newWantConnQueue()\n\n\t// Create 3 wantConn items, none marked as done\n\tfor i := 0; i < 3; i++ {\n\t\tw := &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tdone:   false, // Not done\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t\tq.enqueue(w)\n\t}\n\n\t// Verify initial queue length\n\tif q.len() != 3 {\n\t\tt.Errorf(\"initial queue length = %d, want 3\", q.len())\n\t}\n\n\t// Call dropFrontDone\n\tcount := q.discardDoneAtFront()\n\n\t// Verify no elements were removed\n\tif count != 0 {\n\t\tt.Errorf(\"dropFrontDone() = %d, want 0\", count)\n\t}\n\n\t// Verify queue length unchanged\n\tif q.len() != 3 {\n\t\tt.Errorf(\"queue length after dropFrontDone = %d, want 3\", q.len())\n\t}\n}\n\n// TestWantConnQueue_dropFrontDone_PartialDone tests dropFrontDone with mixed done/not-done elements.\n// This is the core test case that verifies dropFrontDone stops at the first not-done element.\nfunc TestWantConnQueue_dropFrontDone_PartialDone(t *testing.T) {\n\tq := newWantConnQueue()\n\n\t// Create pattern: [done, done, not-done, done, not-done]\n\tstates := []bool{true, true, false, true, false}\n\tvar items []*wantConn\n\n\tfor _, done := range states {\n\t\tw := &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tdone:   done,\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t\tq.enqueue(w)\n\t\titems = append(items, w)\n\t}\n\n\t// Verify initial queue length\n\tif q.len() != 5 {\n\t\tt.Errorf(\"initial queue length = %d, want 5\", q.len())\n\t}\n\n\t// Call dropFrontDone\n\tcount := q.discardDoneAtFront()\n\n\t// Verify only first 2 elements were removed (stopped at first not-done)\n\tif count != 2 {\n\t\tt.Errorf(\"dropFrontDone() = %d, want 2\", count)\n\t}\n\n\t// Verify queue length is now 3\n\tif q.len() != 3 {\n\t\tt.Errorf(\"queue length after dropFrontDone = %d, want 3\", q.len())\n\t}\n\n\t// Verify the front element is the third item (first not-done)\n\tfront, ok := q.dequeue()\n\tif !ok {\n\t\tt.Fatal(\"expected to dequeue an item\")\n\t}\n\tif front != items[2] {\n\t\tt.Error(\"front element should be the third item (first not-done)\")\n\t}\n\n\t// Verify remaining elements are items[3] and items[4]\n\tnext, ok := q.dequeue()\n\tif !ok || next != items[3] {\n\t\tt.Error(\"second element should be items[3]\")\n\t}\n\tnext, ok = q.dequeue()\n\tif !ok || next != items[4] {\n\t\tt.Error(\"third element should be items[4]\")\n\t}\n\n\t// Queue should now be empty\n\tif q.len() != 0 {\n\t\tt.Errorf(\"queue should be empty, got length %d\", q.len())\n\t}\n}\n\n// TestWantConnQueue_dropFrontDone_SingleElement tests dropFrontDone with single element.\nfunc TestWantConnQueue_dropFrontDone_SingleElement(t *testing.T) {\n\t// Test 1: Single done element\n\tq1 := newWantConnQueue()\n\tw1 := &wantConn{\n\t\tctx:    context.Background(),\n\t\tdone:   true,\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\tq1.enqueue(w1)\n\n\tcount := q1.discardDoneAtFront()\n\tif count != 1 {\n\t\tt.Errorf(\"dropFrontDone() with single done element = %d, want 1\", count)\n\t}\n\tif q1.len() != 0 {\n\t\tt.Errorf(\"queue should be empty after dropping single done element\")\n\t}\n\n\t// Test 2: Single not-done element\n\tq2 := newWantConnQueue()\n\tw2 := &wantConn{\n\t\tctx:    context.Background(),\n\t\tdone:   false,\n\t\tresult: make(chan wantConnResult, 1),\n\t}\n\tq2.enqueue(w2)\n\n\tcount = q2.discardDoneAtFront()\n\tif count != 0 {\n\t\tt.Errorf(\"dropFrontDone() with single not-done element = %d, want 0\", count)\n\t}\n\tif q2.len() != 1 {\n\t\tt.Errorf(\"queue length should remain 1 after dropFrontDone\")\n\t}\n}\n\n// TestWantConnQueue_dropFrontDone_MultipleCalls tests consecutive calls to dropFrontDone.\nfunc TestWantConnQueue_dropFrontDone_MultipleCalls(t *testing.T) {\n\tq := newWantConnQueue()\n\n\t// Add initial elements: [done, done, not-done]\n\tw1 := &wantConn{ctx: context.Background(), done: true, result: make(chan wantConnResult, 1)}\n\tw2 := &wantConn{ctx: context.Background(), done: true, result: make(chan wantConnResult, 1)}\n\tw3 := &wantConn{ctx: context.Background(), done: false, result: make(chan wantConnResult, 1)}\n\tq.enqueue(w1)\n\tq.enqueue(w2)\n\tq.enqueue(w3)\n\n\t// First call: should remove 2 done elements\n\tcount1 := q.discardDoneAtFront()\n\tif count1 != 2 {\n\t\tt.Errorf(\"first dropFrontDone() = %d, want 2\", count1)\n\t}\n\tif q.len() != 1 {\n\t\tt.Errorf(\"queue length after first drop = %d, want 1\", q.len())\n\t}\n\n\t// Mark w3 as done and add more elements\n\tw3.mu.Lock()\n\tw3.done = true\n\tw3.mu.Unlock()\n\tw4 := &wantConn{ctx: context.Background(), done: true, result: make(chan wantConnResult, 1)}\n\tw5 := &wantConn{ctx: context.Background(), done: false, result: make(chan wantConnResult, 1)}\n\tq.enqueue(w4)\n\tq.enqueue(w5)\n\n\t// Second call: should remove w3 and w4 (now both done)\n\tcount2 := q.discardDoneAtFront()\n\tif count2 != 2 {\n\t\tt.Errorf(\"second dropFrontDone() = %d, want 2\", count2)\n\t}\n\tif q.len() != 1 {\n\t\tt.Errorf(\"queue length after second drop = %d, want 1\", q.len())\n\t}\n\n\t// Verify remaining element is w5\n\tremaining, ok := q.dequeue()\n\tif !ok || remaining != w5 {\n\t\tt.Error(\"remaining element should be w5\")\n\t}\n}\n\n// TestWantConnQueue_dropFrontDone_ConcurrentWithEnqueue tests concurrent dropFrontDone and enqueue.\nfunc TestWantConnQueue_dropFrontDone_ConcurrentWithEnqueue(t *testing.T) {\n\tq := newWantConnQueue()\n\tconst numOperations = 1000\n\n\tvar wg sync.WaitGroup\n\tdone := make(chan struct{})\n\n\t// Goroutine 1: Continuously enqueue elements\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < numOperations; i++ {\n\t\t\tw := &wantConn{\n\t\t\t\tctx:    context.Background(),\n\t\t\t\tdone:   i%2 == 0, // Alternate between done and not-done\n\t\t\t\tresult: make(chan wantConnResult, 1),\n\t\t\t}\n\t\t\tq.enqueue(w)\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t}\n\t\tclose(done)\n\t}()\n\n\t// Goroutine 2: Continuously call dropFrontDone\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\ttotalDropped := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// Final cleanup\n\t\t\t\ttotalDropped += q.discardDoneAtFront()\n\t\t\t\tt.Logf(\"Total elements dropped: %d\", totalDropped)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tdropped := q.discardDoneAtFront()\n\t\t\t\ttotalDropped += dropped\n\t\t\t\ttime.Sleep(time.Microsecond)\n\t\t\t}\n\t\t}\n\t}()\n\n\twg.Wait()\n\n\t// No panic or race condition is success\n\tt.Logf(\"Final queue length: %d\", q.len())\n}\n\n// TestWantConnQueue_dropFrontDone_ConcurrentWithDequeue tests concurrent operations.\nfunc TestWantConnQueue_dropFrontDone_ConcurrentWithDequeue(t *testing.T) {\n\tq := newWantConnQueue()\n\tconst numOperations = 500\n\n\tvar wg sync.WaitGroup\n\n\t// Pre-populate queue\n\tfor i := 0; i < 100; i++ {\n\t\tw := &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tdone:   i%3 == 0,\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t\tq.enqueue(w)\n\t}\n\n\t// Goroutine 1: enqueue\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < numOperations; i++ {\n\t\t\tw := &wantConn{\n\t\t\t\tctx:    context.Background(),\n\t\t\t\tdone:   i%2 == 0,\n\t\t\t\tresult: make(chan wantConnResult, 1),\n\t\t\t}\n\t\t\tq.enqueue(w)\n\t\t}\n\t}()\n\n\t// Goroutine 2: dequeue\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < numOperations/2; i++ {\n\t\t\tq.dequeue()\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t}\n\t}()\n\n\t// Goroutine 3: dropFrontDone\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < numOperations/4; i++ {\n\t\t\tq.discardDoneAtFront()\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t}\n\t}()\n\n\twg.Wait()\n\n\t// No panic or race condition is success\n\tt.Logf(\"Final queue length: %d\", q.len())\n}\n\n// TestWantConnQueue_len tests the len() method.\nfunc TestWantConnQueue_len(t *testing.T) {\n\tq := newWantConnQueue()\n\n\t// Test empty queue\n\tif length := q.len(); length != 0 {\n\t\tt.Errorf(\"empty queue len() = %d, want 0\", length)\n\t}\n\n\t// Add elements and verify length\n\tfor i := 1; i <= 5; i++ {\n\t\tw := &wantConn{\n\t\t\tctx:    context.Background(),\n\t\t\tresult: make(chan wantConnResult, 1),\n\t\t}\n\t\tq.enqueue(w)\n\n\t\tif length := q.len(); length != i {\n\t\t\tt.Errorf(\"queue len() after %d enqueues = %d, want %d\", i, length, i)\n\t\t}\n\t}\n\n\t// Remove elements and verify length\n\tfor i := 4; i >= 0; i-- {\n\t\tq.dequeue()\n\t\tif length := q.len(); length != i {\n\t\t\tt.Errorf(\"queue len() after dequeue = %d, want %d\", length, i)\n\t\t}\n\t}\n}\n\n// TestWantConnQueue_len_Concurrent tests len() thread safety.\nfunc TestWantConnQueue_len_Concurrent(t *testing.T) {\n\tq := newWantConnQueue()\n\tconst numReaders = 10\n\tconst numWriters = 5\n\tconst operations = 100\n\n\tvar wg sync.WaitGroup\n\n\t// Multiple readers calling len()\n\tfor i := 0; i < numReaders; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < operations; j++ {\n\t\t\t\t_ = q.len() // Just read, don't care about value\n\t\t\t\ttime.Sleep(time.Microsecond)\n\t\t\t}\n\t\t}()\n\t}\n\n\t// Writers enqueueing\n\tfor i := 0; i < numWriters; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < operations; j++ {\n\t\t\t\tw := &wantConn{\n\t\t\t\t\tctx:    context.Background(),\n\t\t\t\t\tresult: make(chan wantConnResult, 1),\n\t\t\t\t}\n\t\t\t\tq.enqueue(w)\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\t// Verify final length is correct\n\texpectedLength := numWriters * operations\n\tif length := q.len(); length != expectedLength {\n\t\tt.Errorf(\"final queue len() = %d, want %d\", length, expectedLength)\n\t}\n}\n"
  },
  {
    "path": "internal/proto/peek_push_notification_test.go",
    "content": "package proto\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n)\n\n// TestPeekPushNotificationName tests the updated PeekPushNotificationName method\nfunc TestPeekPushNotificationName(t *testing.T) {\n\tt.Run(\"ValidPushNotifications\", func(t *testing.T) {\n\t\ttestCases := []struct {\n\t\t\tname         string\n\t\t\tnotification string\n\t\t\texpected     string\n\t\t}{\n\t\t\t{\"MOVING\", \"MOVING\", \"MOVING\"},\n\t\t\t{\"MIGRATING\", \"MIGRATING\", \"MIGRATING\"},\n\t\t\t{\"MIGRATED\", \"MIGRATED\", \"MIGRATED\"},\n\t\t\t{\"FAILING_OVER\", \"FAILING_OVER\", \"FAILING_OVER\"},\n\t\t\t{\"FAILED_OVER\", \"FAILED_OVER\", \"FAILED_OVER\"},\n\t\t\t{\"message\", \"message\", \"message\"},\n\t\t\t{\"pmessage\", \"pmessage\", \"pmessage\"},\n\t\t\t{\"subscribe\", \"subscribe\", \"subscribe\"},\n\t\t\t{\"unsubscribe\", \"unsubscribe\", \"unsubscribe\"},\n\t\t\t{\"psubscribe\", \"psubscribe\", \"psubscribe\"},\n\t\t\t{\"punsubscribe\", \"punsubscribe\", \"punsubscribe\"},\n\t\t\t{\"smessage\", \"smessage\", \"smessage\"},\n\t\t\t{\"ssubscribe\", \"ssubscribe\", \"ssubscribe\"},\n\t\t\t{\"sunsubscribe\", \"sunsubscribe\", \"sunsubscribe\"},\n\t\t\t{\"custom\", \"custom\", \"custom\"},\n\t\t\t{\"short\", \"a\", \"a\"},\n\t\t\t{\"empty\", \"\", \"\"},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tbuf := createValidPushNotification(tc.notification, \"data\")\n\t\t\t\treader := NewReader(buf)\n\n\t\t\t\t// Prime the buffer by peeking first\n\t\t\t\t_, _ = reader.rd.Peek(1)\n\n\t\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"PeekPushNotificationName should not error for valid notification: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif name != tc.expected {\n\t\t\t\t\tt.Errorf(\"Expected notification name '%s', got '%s'\", tc.expected, name)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"NotificationWithMultipleArguments\", func(t *testing.T) {\n\t\t// Create push notification with multiple arguments\n\t\tbuf := createPushNotificationWithArgs(\"MOVING\", \"slot\", \"123\", \"from\", \"node1\", \"to\", \"node2\")\n\t\treader := NewReader(buf)\n\n\t\t// Prime the buffer\n\t\t_, _ = reader.rd.Peek(1)\n\n\t\tname, err := reader.PeekPushNotificationName()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"PeekPushNotificationName should not error: %v\", err)\n\t\t}\n\n\t\tif name != \"MOVING\" {\n\t\t\tt.Errorf(\"Expected 'MOVING', got '%s'\", name)\n\t\t}\n\t})\n\n\tt.Run(\"SingleElementNotification\", func(t *testing.T) {\n\t\t// Create push notification with single element\n\t\tbuf := createSingleElementPushNotification(\"TEST\")\n\t\treader := NewReader(buf)\n\n\t\t// Prime the buffer\n\t\t_, _ = reader.rd.Peek(1)\n\n\t\tname, err := reader.PeekPushNotificationName()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"PeekPushNotificationName should not error: %v\", err)\n\t\t}\n\n\t\tif name != \"TEST\" {\n\t\t\tt.Errorf(\"Expected 'TEST', got '%s'\", name)\n\t\t}\n\t})\n\n\tt.Run(\"ErrorDetection\", func(t *testing.T) {\n\t\tt.Run(\"NotPushNotification\", func(t *testing.T) {\n\t\t\t// Test with regular array instead of push notification\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\tfmt.Fprint(buf, \"*2\\r\\n$6\\r\\nMOVING\\r\\n$4\\r\\ndata\\r\\n\")\n\t\t\treader := NewReader(buf)\n\n\t\t\t_, err := reader.PeekPushNotificationName()\n\t\t\tif err == nil {\n\t\t\t\tt.Error(\"PeekPushNotificationName should error for non-push notification\")\n\t\t\t}\n\n\t\t\t// The error might be \"no data available\" or \"can't parse push notification\"\n\t\t\tif !strings.Contains(err.Error(), \"can't peek push notification name\") {\n\t\t\t\tt.Errorf(\"Error should mention push notification parsing, got: %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"InsufficientData\", func(t *testing.T) {\n\t\t\t// Test with buffer smaller than peek size - this might panic due to bounds checking\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\tfmt.Fprint(buf, \">\")\n\t\t\treader := NewReader(buf)\n\n\t\t\tfunc() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tt.Logf(\"PeekPushNotificationName panicked as expected for insufficient data: %v\", r)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\t_, err := reader.PeekPushNotificationName()\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"PeekPushNotificationName should error for insufficient data\")\n\t\t\t\t}\n\t\t\t}()\n\t\t})\n\n\t\tt.Run(\"EmptyBuffer\", func(t *testing.T) {\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\treader := NewReader(buf)\n\n\t\t\t_, err := reader.PeekPushNotificationName()\n\t\t\tif err == nil {\n\t\t\t\tt.Error(\"PeekPushNotificationName should error for empty buffer\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"DifferentRESPTypes\", func(t *testing.T) {\n\t\t\t// Test with different RESP types that should be rejected\n\t\t\trespTypes := []byte{'+', '-', ':', '$', '*', '%', '~', '|', '('}\n\n\t\t\tfor _, respType := range respTypes {\n\t\t\t\tt.Run(fmt.Sprintf(\"Type_%c\", respType), func(t *testing.T) {\n\t\t\t\t\tbuf := &bytes.Buffer{}\n\t\t\t\t\tbuf.WriteByte(respType)\n\t\t\t\t\tfmt.Fprint(buf, \"test data that fills the buffer completely\")\n\t\t\t\t\treader := NewReader(buf)\n\n\t\t\t\t\t_, err := reader.PeekPushNotificationName()\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tt.Errorf(\"PeekPushNotificationName should error for RESP type '%c'\", respType)\n\t\t\t\t\t}\n\n\t\t\t\t\t// The error might be \"no data available\" or \"can't parse push notification\"\n\t\t\t\t\tif !strings.Contains(err.Error(), \"can't peek push notification name\") {\n\t\t\t\t\t\tt.Errorf(\"Error should mention push notification parsing, got: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"EdgeCases\", func(t *testing.T) {\n\t\tt.Run(\"ZeroLengthArray\", func(t *testing.T) {\n\t\t\t// Create push notification with zero elements: >0\\r\\n\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\tfmt.Fprint(buf, \">0\\r\\npadding_data_to_fill_buffer_completely\")\n\t\t\treader := NewReader(buf)\n\n\t\t\t_, err := reader.PeekPushNotificationName()\n\t\t\tif err == nil {\n\t\t\t\tt.Error(\"PeekPushNotificationName should error for zero-length array\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"EmptyNotificationName\", func(t *testing.T) {\n\t\t\t// Create push notification with empty name: >1\\r\\n$0\\r\\n\\r\\n\n\t\t\tbuf := createValidPushNotification(\"\", \"data\")\n\t\t\treader := NewReader(buf)\n\n\t\t\t// Prime the buffer\n\t\t\t_, _ = reader.rd.Peek(1)\n\n\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"PeekPushNotificationName should not error for empty name: %v\", err)\n\t\t\t}\n\n\t\t\tif name != \"\" {\n\t\t\t\tt.Errorf(\"Expected empty notification name, got '%s'\", name)\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"CorruptedData\", func(t *testing.T) {\n\t\t\tcorruptedCases := []struct {\n\t\t\t\tname string\n\t\t\t\tdata string\n\t\t\t}{\n\t\t\t\t{\"CorruptedLength\", \">abc\\r\\n$6\\r\\nMOVING\\r\\n\"},\n\t\t\t\t{\"MissingCRLF\", \">2$6\\r\\nMOVING\\r\\n$4\\r\\ndata\\r\\n\"},\n\t\t\t\t{\"InvalidStringLength\", \">2\\r\\n$abc\\r\\nMOVING\\r\\n$4\\r\\ndata\\r\\n\"},\n\t\t\t\t{\"NegativeStringLength\", \">2\\r\\n$-1\\r\\n$4\\r\\ndata\\r\\n\"},\n\t\t\t\t{\"IncompleteString\", \">1\\r\\n$6\\r\\nMOV\"},\n\t\t\t}\n\n\t\t\tfor _, tc := range corruptedCases {\n\t\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\t\tbuf := &bytes.Buffer{}\n\t\t\t\t\tfmt.Fprint(buf, tc.data)\n\t\t\t\t\treader := NewReader(buf)\n\n\t\t\t\t\t// Some corrupted data might not error but return unexpected results\n\t\t\t\t\t// This is acceptable behavior for malformed input\n\t\t\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Logf(\"PeekPushNotificationName errored for corrupted data %s: %v (DATA: %s)\", tc.name, err, tc.data)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Logf(\"PeekPushNotificationName returned '%s' for corrupted data NAME: %s, DATA: %s\", name, tc.name, tc.data)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"BoundaryConditions\", func(t *testing.T) {\n\t\tt.Run(\"ExactlyPeekSize\", func(t *testing.T) {\n\t\t\t// Create buffer that is exactly 36 bytes (the peek window size)\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\t// \">1\\r\\n$4\\r\\nTEST\\r\\n\" = 14 bytes, need 22 more\n\t\t\tfmt.Fprint(buf, \">1\\r\\n$4\\r\\nTEST\\r\\n1234567890123456789012\")\n\t\t\tif buf.Len() != 36 {\n\t\t\t\tt.Errorf(\"Expected buffer length 36, got %d\", buf.Len())\n\t\t\t}\n\n\t\t\treader := NewReader(buf)\n\t\t\t// Prime the buffer\n\t\t\t_, _ = reader.rd.Peek(1)\n\n\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"PeekPushNotificationName should work for exact peek size: %v\", err)\n\t\t\t}\n\n\t\t\tif name != \"TEST\" {\n\t\t\t\tt.Errorf(\"Expected 'TEST', got '%s'\", name)\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"LessThanPeekSize\", func(t *testing.T) {\n\t\t\t// Create buffer smaller than 36 bytes but with complete notification\n\t\t\tbuf := createValidPushNotification(\"TEST\", \"\")\n\t\t\treader := NewReader(buf)\n\n\t\t\t// Prime the buffer\n\t\t\t_, _ = reader.rd.Peek(1)\n\n\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"PeekPushNotificationName should work for complete notification: %v\", err)\n\t\t\t}\n\n\t\t\tif name != \"TEST\" {\n\t\t\t\tt.Errorf(\"Expected 'TEST', got '%s'\", name)\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"LongNotificationName\", func(t *testing.T) {\n\t\t\t// Test with notification name that might exceed peek window\n\t\t\tlongName := strings.Repeat(\"A\", 20) // 20 character name (safe size)\n\t\t\tbuf := createValidPushNotification(longName, \"data\")\n\t\t\treader := NewReader(buf)\n\n\t\t\t// Prime the buffer\n\t\t\t_, _ = reader.rd.Peek(1)\n\n\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"PeekPushNotificationName should work for long name: %v\", err)\n\t\t\t}\n\n\t\t\tif name != longName {\n\t\t\t\tt.Errorf(\"Expected '%s', got '%s'\", longName, name)\n\t\t\t}\n\t\t})\n\t})\n}\n\n// Helper functions to create test data\n\n// createValidPushNotification creates a valid RESP3 push notification\nfunc createValidPushNotification(notificationName, data string) *bytes.Buffer {\n\tbuf := &bytes.Buffer{}\n\n\tsimpleOrString := rand.Intn(2) == 0\n\tdefMsg := fmt.Sprintf(\"$%d\\r\\n%s\\r\\n\", len(notificationName), notificationName)\n\n\tif data == \"\" {\n\n\t\t// Single element notification\n\t\tfmt.Fprint(buf, \">1\\r\\n\")\n\t\tif simpleOrString {\n\t\t\tfmt.Fprintf(buf, \"+%s\\r\\n\", notificationName)\n\t\t} else {\n\t\t\tfmt.Fprint(buf, defMsg)\n\t\t}\n\t} else {\n\t\t// Two element notification\n\t\tfmt.Fprint(buf, \">2\\r\\n\")\n\t\tif simpleOrString {\n\t\t\tfmt.Fprintf(buf, \"+%s\\r\\n\", notificationName)\n\t\t\tfmt.Fprintf(buf, \"+%s\\r\\n\", data)\n\t\t} else {\n\t\t\tfmt.Fprint(buf, defMsg)\n\t\t\tfmt.Fprint(buf, defMsg)\n\t\t}\n\t}\n\n\treturn buf\n}\n\n// createReaderWithPrimedBuffer creates a reader and primes the buffer\nfunc createReaderWithPrimedBuffer(buf *bytes.Buffer) *Reader {\n\treader := NewReader(buf)\n\t// Prime the buffer by peeking first\n\t_, _ = reader.rd.Peek(1)\n\treturn reader\n}\n\n// createPushNotificationWithArgs creates a push notification with multiple arguments\nfunc createPushNotificationWithArgs(notificationName string, args ...string) *bytes.Buffer {\n\tbuf := &bytes.Buffer{}\n\n\ttotalElements := 1 + len(args)\n\tfmt.Fprintf(buf, \">%d\\r\\n\", totalElements)\n\n\t// Write notification name\n\tfmt.Fprintf(buf, \"$%d\\r\\n%s\\r\\n\", len(notificationName), notificationName)\n\n\t// Write arguments\n\tfor _, arg := range args {\n\t\tfmt.Fprintf(buf, \"$%d\\r\\n%s\\r\\n\", len(arg), arg)\n\t}\n\n\treturn buf\n}\n\n// createSingleElementPushNotification creates a push notification with single element\nfunc createSingleElementPushNotification(notificationName string) *bytes.Buffer {\n\tbuf := &bytes.Buffer{}\n\tfmt.Fprint(buf, \">1\\r\\n\")\n\tfmt.Fprintf(buf, \"$%d\\r\\n%s\\r\\n\", len(notificationName), notificationName)\n\treturn buf\n}\n\n// BenchmarkPeekPushNotificationName benchmarks the method performance\nfunc BenchmarkPeekPushNotificationName(b *testing.B) {\n\ttestCases := []struct {\n\t\tname         string\n\t\tnotification string\n\t}{\n\t\t{\"Short\", \"TEST\"},\n\t\t{\"Medium\", \"MOVING_NOTIFICATION\"},\n\t\t{\"Long\", \"VERY_LONG_NOTIFICATION_NAME_FOR_TESTING\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tb.Run(tc.name, func(b *testing.B) {\n\t\t\tbuf := createValidPushNotification(tc.notification, \"data\")\n\t\t\tdata := buf.Bytes()\n\n\t\t\t// Reuse both bytes.Reader and proto.Reader to avoid allocations\n\t\t\tbytesReader := bytes.NewReader(data)\n\t\t\treader := NewReader(bytesReader)\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t// Reset the bytes.Reader to the beginning without allocating\n\t\t\t\tbytesReader.Reset(data)\n\t\t\t\t// Reset the proto.Reader to reuse the bufio buffer\n\t\t\t\treader.Reset(bytesReader)\n\t\t\t\t_, err := reader.PeekPushNotificationName()\n\t\t\t\tif err != nil {\n\t\t\t\t\tb.Errorf(\"PeekPushNotificationName should not error: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestPeekPushNotificationNameSpecialCases tests special cases and realistic scenarios\nfunc TestPeekPushNotificationNameSpecialCases(t *testing.T) {\n\tt.Run(\"RealisticNotifications\", func(t *testing.T) {\n\t\t// Test realistic Redis push notifications\n\t\trealisticCases := []struct {\n\t\t\tname         string\n\t\t\tnotification []string\n\t\t\texpected     string\n\t\t}{\n\t\t\t{\"MovingSlot\", []string{\"MOVING\", \"slot\", \"123\", \"from\", \"127.0.0.1:7000\", \"to\", \"127.0.0.1:7001\"}, \"MOVING\"},\n\t\t\t{\"MigratingSlot\", []string{\"MIGRATING\", \"slot\", \"456\", \"from\", \"127.0.0.1:7001\", \"to\", \"127.0.0.1:7002\"}, \"MIGRATING\"},\n\t\t\t{\"MigratedSlot\", []string{\"MIGRATED\", \"slot\", \"789\", \"from\", \"127.0.0.1:7002\", \"to\", \"127.0.0.1:7000\"}, \"MIGRATED\"},\n\t\t\t{\"FailingOver\", []string{\"FAILING_OVER\", \"node\", \"127.0.0.1:7000\"}, \"FAILING_OVER\"},\n\t\t\t{\"FailedOver\", []string{\"FAILED_OVER\", \"node\", \"127.0.0.1:7000\"}, \"FAILED_OVER\"},\n\t\t\t{\"PubSubMessage\", []string{\"message\", \"mychannel\", \"hello world\"}, \"message\"},\n\t\t\t{\"PubSubPMessage\", []string{\"pmessage\", \"pattern*\", \"mychannel\", \"hello world\"}, \"pmessage\"},\n\t\t\t{\"Subscribe\", []string{\"subscribe\", \"mychannel\", \"1\"}, \"subscribe\"},\n\t\t\t{\"Unsubscribe\", []string{\"unsubscribe\", \"mychannel\", \"0\"}, \"unsubscribe\"},\n\t\t}\n\n\t\tfor _, tc := range realisticCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tbuf := createPushNotificationWithArgs(tc.notification[0], tc.notification[1:]...)\n\t\t\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"PeekPushNotificationName should not error for %s: %v\", tc.name, err)\n\t\t\t\t}\n\n\t\t\t\tif name != tc.expected {\n\t\t\t\t\tt.Errorf(\"Expected '%s', got '%s'\", tc.expected, name)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"SpecialCharactersInName\", func(t *testing.T) {\n\t\tspecialCases := []struct {\n\t\t\tname         string\n\t\t\tnotification string\n\t\t}{\n\t\t\t{\"WithUnderscore\", \"test_notification\"},\n\t\t\t{\"WithDash\", \"test-notification\"},\n\t\t\t{\"WithNumbers\", \"test123\"},\n\t\t\t{\"WithDots\", \"test.notification\"},\n\t\t\t{\"WithColon\", \"test:notification\"},\n\t\t\t{\"WithSlash\", \"test/notification\"},\n\t\t\t{\"MixedCase\", \"TestNotification\"},\n\t\t\t{\"AllCaps\", \"TESTNOTIFICATION\"},\n\t\t\t{\"AllLower\", \"testnotification\"},\n\t\t\t{\"Unicode\", \"tëst\"},\n\t\t}\n\n\t\tfor _, tc := range specialCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tbuf := createValidPushNotification(tc.notification, \"data\")\n\t\t\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"PeekPushNotificationName should not error for '%s': %v\", tc.notification, err)\n\t\t\t\t}\n\n\t\t\t\tif name != tc.notification {\n\t\t\t\t\tt.Errorf(\"Expected '%s', got '%s'\", tc.notification, name)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"IdempotentPeek\", func(t *testing.T) {\n\t\t// Test that multiple peeks return the same result\n\t\tbuf := createValidPushNotification(\"MOVING\", \"data\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\t// First peek\n\t\tname1, err1 := reader.PeekPushNotificationName()\n\t\tif err1 != nil {\n\t\t\tt.Errorf(\"First PeekPushNotificationName should not error: %v\", err1)\n\t\t}\n\n\t\t// Second peek should return the same result\n\t\tname2, err2 := reader.PeekPushNotificationName()\n\t\tif err2 != nil {\n\t\t\tt.Errorf(\"Second PeekPushNotificationName should not error: %v\", err2)\n\t\t}\n\n\t\tif name1 != name2 {\n\t\t\tt.Errorf(\"Peek should be idempotent: first='%s', second='%s'\", name1, name2)\n\t\t}\n\n\t\tif name1 != \"MOVING\" {\n\t\t\tt.Errorf(\"Expected 'MOVING', got '%s'\", name1)\n\t\t}\n\t})\n}\n\n// TestPeekPushNotificationNamePerformance tests performance characteristics\nfunc TestPeekPushNotificationNamePerformance(t *testing.T) {\n\tt.Run(\"RepeatedCalls\", func(t *testing.T) {\n\t\t// Test that repeated calls work correctly\n\t\tbuf := createValidPushNotification(\"TEST\", \"data\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\t// Call multiple times\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tname, err := reader.PeekPushNotificationName()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"PeekPushNotificationName should not error on call %d: %v\", i, err)\n\t\t\t}\n\t\t\tif name != \"TEST\" {\n\t\t\t\tt.Errorf(\"Expected 'TEST' on call %d, got '%s'\", i, name)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"LargeNotifications\", func(t *testing.T) {\n\t\t// Test with large notification data\n\t\tlargeData := strings.Repeat(\"x\", 1000)\n\t\tbuf := createValidPushNotification(\"LARGE\", largeData)\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tname, err := reader.PeekPushNotificationName()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"PeekPushNotificationName should not error for large notification: %v\", err)\n\t\t}\n\n\t\tif name != \"LARGE\" {\n\t\t\tt.Errorf(\"Expected 'LARGE', got '%s'\", name)\n\t\t}\n\t})\n}\n\n// TestPeekPushNotificationNameBehavior documents the method's behavior\nfunc TestPeekPushNotificationNameBehavior(t *testing.T) {\n\tt.Run(\"MethodBehavior\", func(t *testing.T) {\n\t\t// Test that the method works as intended:\n\t\t// 1. Peek at the buffer without consuming it\n\t\t// 2. Detect push notifications (RESP type '>')\n\t\t// 3. Extract the notification name from the first element\n\t\t// 4. Return the name for filtering decisions\n\n\t\tbuf := createValidPushNotification(\"MOVING\", \"slot_data\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\t// Peek should not consume the buffer\n\t\tname, err := reader.PeekPushNotificationName()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"PeekPushNotificationName should not error: %v\", err)\n\t\t}\n\n\t\tif name != \"MOVING\" {\n\t\t\tt.Errorf(\"Expected 'MOVING', got '%s'\", name)\n\t\t}\n\n\t\t// Buffer should still be available for normal reading\n\t\treplyType, err := reader.PeekReplyType()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"PeekReplyType should work after PeekPushNotificationName: %v\", err)\n\t\t}\n\n\t\tif replyType != RespPush {\n\t\t\tt.Errorf(\"Expected RespPush, got %v\", replyType)\n\t\t}\n\t})\n\n\tt.Run(\"BufferNotConsumed\", func(t *testing.T) {\n\t\t// Verify that peeking doesn't consume the buffer\n\t\tbuf := createValidPushNotification(\"TEST\", \"data\")\n\t\toriginalData := buf.Bytes()\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\t// Peek the notification name\n\t\tname, err := reader.PeekPushNotificationName()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"PeekPushNotificationName should not error: %v\", err)\n\t\t}\n\n\t\tif name != \"TEST\" {\n\t\t\tt.Errorf(\"Expected 'TEST', got '%s'\", name)\n\t\t}\n\n\t\t// Read the actual notification\n\t\treply, err := reader.ReadReply()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ReadReply should work after peek: %v\", err)\n\t\t}\n\n\t\t// Verify we got the complete notification\n\t\tif replySlice, ok := reply.([]interface{}); ok {\n\t\t\tif len(replySlice) != 2 {\n\t\t\t\tt.Errorf(\"Expected 2 elements, got %d\", len(replySlice))\n\t\t\t}\n\t\t\tif replySlice[0] != \"TEST\" {\n\t\t\t\tt.Errorf(\"Expected 'TEST', got %v\", replySlice[0])\n\t\t\t}\n\t\t} else {\n\t\t\tt.Errorf(\"Expected slice reply, got %T\", reply)\n\t\t}\n\n\t\t// Verify buffer was properly consumed\n\t\tif buf.Len() != 0 {\n\t\t\tt.Errorf(\"Buffer should be empty after reading, but has %d bytes: %q\", buf.Len(), buf.Bytes())\n\t\t}\n\n\t\tt.Logf(\"Original buffer size: %d bytes\", len(originalData))\n\t\tt.Logf(\"Successfully peeked and then read complete notification\")\n\t})\n\n\tt.Run(\"ImplementationSuccess\", func(t *testing.T) {\n\t\t// Document that the implementation is now working correctly\n\t\tt.Log(\"PeekPushNotificationName implementation status:\")\n\t\tt.Log(\"1. ✅ Correctly parses RESP3 push notifications\")\n\t\tt.Log(\"2. ✅ Extracts notification names properly\")\n\t\tt.Log(\"3. ✅ Handles buffer peeking without consumption\")\n\t\tt.Log(\"4. ✅ Works with various notification types\")\n\t\tt.Log(\"5. ✅ Supports empty notification names\")\n\t\tt.Log(\"\")\n\t\tt.Log(\"RESP3 format parsing:\")\n\t\tt.Log(\">2\\\\r\\\\n$6\\\\r\\\\nMOVING\\\\r\\\\n$4\\\\r\\\\ndata\\\\r\\\\n\")\n\t\tt.Log(\"✅ Correctly identifies push notification marker (>)\")\n\t\tt.Log(\"✅ Skips array length (2)\")\n\t\tt.Log(\"✅ Parses string marker ($) and length (6)\")\n\t\tt.Log(\"✅ Extracts notification name (MOVING)\")\n\t\tt.Log(\"✅ Returns name without consuming buffer\")\n\t\tt.Log(\"\")\n\t\tt.Log(\"Note: Buffer must be primed with a peek operation first\")\n\t})\n}\n"
  },
  {
    "path": "internal/proto/proto_test.go",
    "content": "package proto_test\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nfunc TestGinkgoSuite(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"proto\")\n}\n"
  },
  {
    "path": "internal/proto/reader.go",
    "content": "package proto\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"math/big\"\n\t\"strconv\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\n// DefaultBufferSize is the default size for read/write buffers (32 KiB).\nconst DefaultBufferSize = 32 * 1024\n\n// redis resp protocol data type.\nconst (\n\tRespStatus    = '+' // +<string>\\r\\n\n\tRespError     = '-' // -<string>\\r\\n\n\tRespString    = '$' // $<length>\\r\\n<bytes>\\r\\n\n\tRespInt       = ':' // :<number>\\r\\n\n\tRespNil       = '_' // _\\r\\n\n\tRespFloat     = ',' // ,<floating-point-number>\\r\\n (golang float)\n\tRespBool      = '#' // true: #t\\r\\n false: #f\\r\\n\n\tRespBlobError = '!' // !<length>\\r\\n<bytes>\\r\\n\n\tRespVerbatim  = '=' // =<length>\\r\\nFORMAT:<bytes>\\r\\n\n\tRespBigInt    = '(' // (<big number>\\r\\n\n\tRespArray     = '*' // *<len>\\r\\n... (same as resp2)\n\tRespMap       = '%' // %<len>\\r\\n(key)\\r\\n(value)\\r\\n... (golang map)\n\tRespSet       = '~' // ~<len>\\r\\n... (same as Array)\n\tRespAttr      = '|' // |<len>\\r\\n(key)\\r\\n(value)\\r\\n... + command reply\n\tRespPush      = '>' // ><len>\\r\\n... (same as Array)\n)\n\n// Not used temporarily.\n// Redis has not used these two data types for the time being, and will implement them later.\n// Streamed           = \"EOF:\"\n// StreamedAggregated = '?'\n\n//------------------------------------------------------------------------------\n\nconst Nil = RedisError(\"redis: nil\") // nolint:errname\n\ntype RedisError string\n\nfunc (e RedisError) Error() string { return string(e) }\n\nfunc (RedisError) RedisError() {}\n\nfunc ParseErrorReply(line []byte) error {\n\tmsg := string(line[1:])\n\treturn parseTypedRedisError(msg)\n}\n\n//------------------------------------------------------------------------------\n\ntype Reader struct {\n\trd *bufio.Reader\n}\n\nfunc NewReader(rd io.Reader) *Reader {\n\treturn &Reader{\n\t\trd: bufio.NewReaderSize(rd, DefaultBufferSize),\n\t}\n}\n\nfunc NewReaderSize(rd io.Reader, size int) *Reader {\n\treturn &Reader{\n\t\trd: bufio.NewReaderSize(rd, size),\n\t}\n}\n\nfunc (r *Reader) Buffered() int {\n\treturn r.rd.Buffered()\n}\n\nfunc (r *Reader) Peek(n int) ([]byte, error) {\n\treturn r.rd.Peek(n)\n}\n\nfunc (r *Reader) Reset(rd io.Reader) {\n\tr.rd.Reset(rd)\n}\n\n// PeekReplyType returns the data type of the next response without advancing the Reader,\n// and discard the attribute type.\nfunc (r *Reader) PeekReplyType() (byte, error) {\n\tb, err := r.rd.Peek(1)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif b[0] == RespAttr {\n\t\tif err = r.DiscardNext(); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn r.PeekReplyType()\n\t}\n\treturn b[0], nil\n}\n\nfunc (r *Reader) PeekPushNotificationName() (string, error) {\n\t// \"prime\" the buffer by peeking at the next byte\n\tc, err := r.Peek(1)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif c[0] != RespPush {\n\t\treturn \"\", fmt.Errorf(\"redis: can't peek push notification name, next reply is not a push notification\")\n\t}\n\n\t// peek 36 bytes at most, should be enough to read the push notification name\n\ttoPeek := 36\n\tbuffered := r.Buffered()\n\tif buffered == 0 {\n\t\treturn \"\", fmt.Errorf(\"redis: can't peek push notification name, no data available\")\n\t}\n\tif buffered < toPeek {\n\t\ttoPeek = buffered\n\t}\n\tbuf, err := r.rd.Peek(toPeek)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif buf[0] != RespPush {\n\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification: %q\", buf)\n\t}\n\n\tif len(buf) < 3 {\n\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification: %q\", buf)\n\t}\n\n\t// remove push notification type\n\tbuf = buf[1:]\n\t// remove first line - e.g. >2\\r\\n\n\tfor i := 0; i < len(buf)-1; i++ {\n\t\tif buf[i] == '\\r' && buf[i+1] == '\\n' {\n\t\t\tbuf = buf[i+2:]\n\t\t\tbreak\n\t\t} else {\n\t\t\tif buf[i] < '0' || buf[i] > '9' {\n\t\t\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification: %q\", buf)\n\t\t\t}\n\t\t}\n\t}\n\tif len(buf) < 2 {\n\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification: %q\", buf)\n\t}\n\t// next line should be $<length><string>\\r\\n or +<length><string>\\r\\n\n\t// should have the type of the push notification name and it's length\n\tif buf[0] != RespString && buf[0] != RespStatus {\n\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification name: %q\", buf)\n\t}\n\ttypeOfName := buf[0]\n\t// remove the type of the push notification name\n\tbuf = buf[1:]\n\tif typeOfName == RespString {\n\t\t// remove the length of the string\n\t\tif len(buf) < 2 {\n\t\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification name: %q\", buf)\n\t\t}\n\t\tfor i := 0; i < len(buf)-1; i++ {\n\t\t\tif buf[i] == '\\r' && buf[i+1] == '\\n' {\n\t\t\t\tbuf = buf[i+2:]\n\t\t\t\tbreak\n\t\t\t} else {\n\t\t\t\tif buf[i] < '0' || buf[i] > '9' {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification name: %q\", buf)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(buf) < 2 {\n\t\treturn \"\", fmt.Errorf(\"redis: can't parse push notification name: %q\", buf)\n\t}\n\t// keep only the notification name\n\tfor i := 0; i < len(buf)-1; i++ {\n\t\tif buf[i] == '\\r' && buf[i+1] == '\\n' {\n\t\t\tbuf = buf[:i]\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn util.BytesToString(buf), nil\n}\n\n// ReadLine Return a valid reply, it will check the protocol or redis error,\n// and discard the attribute type.\nfunc (r *Reader) ReadLine() ([]byte, error) {\n\tline, err := r.readLine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch line[0] {\n\tcase RespError:\n\t\treturn nil, ParseErrorReply(line)\n\tcase RespNil:\n\t\treturn nil, Nil\n\tcase RespBlobError:\n\t\tvar blobErr string\n\t\tblobErr, err = r.readStringReply(line)\n\t\tif err == nil {\n\t\t\terr = parseTypedRedisError(blobErr)\n\t\t}\n\t\treturn nil, err\n\tcase RespAttr:\n\t\tif err = r.Discard(line); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn r.ReadLine()\n\t}\n\n\t// Compatible with RESP2\n\tif IsNilReply(line) {\n\t\treturn nil, Nil\n\t}\n\n\treturn line, nil\n}\n\n// readLine returns an error if:\n//   - there is a pending read error;\n//   - or line does not end with \\r\\n.\nfunc (r *Reader) readLine() ([]byte, error) {\n\tb, err := r.rd.ReadSlice('\\n')\n\tif err != nil {\n\t\tif err != bufio.ErrBufferFull {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfull := make([]byte, len(b))\n\t\tcopy(full, b)\n\n\t\tb, err = r.rd.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfull = append(full, b...) //nolint:makezero\n\t\tb = full\n\t}\n\tif len(b) <= 2 || b[len(b)-1] != '\\n' || b[len(b)-2] != '\\r' {\n\t\treturn nil, fmt.Errorf(\"redis: invalid reply: %q\", b)\n\t}\n\treturn b[:len(b)-2], nil\n}\n\nfunc (r *Reader) ReadReply() (interface{}, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch line[0] {\n\tcase RespStatus:\n\t\treturn string(line[1:]), nil\n\tcase RespInt:\n\t\treturn util.ParseInt(line[1:], 10, 64)\n\tcase RespFloat:\n\t\treturn r.readFloat(line)\n\tcase RespBool:\n\t\treturn r.readBool(line)\n\tcase RespBigInt:\n\t\treturn r.readBigInt(line)\n\n\tcase RespString:\n\t\treturn r.readStringReply(line)\n\tcase RespVerbatim:\n\t\treturn r.readVerb(line)\n\n\tcase RespArray, RespSet, RespPush:\n\t\treturn r.readSlice(line)\n\tcase RespMap:\n\t\treturn r.readMap(line)\n\t}\n\treturn nil, fmt.Errorf(\"redis: can't parse %.100q\", line)\n}\n\nfunc (r *Reader) readFloat(line []byte) (float64, error) {\n\tv := string(line[1:])\n\tswitch string(line[1:]) {\n\tcase \"inf\":\n\t\treturn math.Inf(1), nil\n\tcase \"-inf\":\n\t\treturn math.Inf(-1), nil\n\tcase \"nan\", \"-nan\":\n\t\treturn math.NaN(), nil\n\t}\n\treturn strconv.ParseFloat(v, 64)\n}\n\nfunc (r *Reader) readBool(line []byte) (bool, error) {\n\tswitch string(line[1:]) {\n\tcase \"t\":\n\t\treturn true, nil\n\tcase \"f\":\n\t\treturn false, nil\n\t}\n\treturn false, fmt.Errorf(\"redis: can't parse bool reply: %q\", line)\n}\n\nfunc (r *Reader) readBigInt(line []byte) (*big.Int, error) {\n\ti := new(big.Int)\n\tif i, ok := i.SetString(string(line[1:]), 10); ok {\n\t\treturn i, nil\n\t}\n\treturn nil, fmt.Errorf(\"redis: can't parse bigInt reply: %q\", line)\n}\n\nfunc (r *Reader) readStringReply(line []byte) (string, error) {\n\tn, err := replyLen(line)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tb := make([]byte, n+2)\n\t_, err = io.ReadFull(r.rd, b)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn util.BytesToString(b[:n]), nil\n}\n\nfunc (r *Reader) readVerb(line []byte) (string, error) {\n\ts, err := r.readStringReply(line)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(s) < 4 || s[3] != ':' {\n\t\treturn \"\", fmt.Errorf(\"redis: can't parse verbatim string reply: %q\", line)\n\t}\n\treturn s[4:], nil\n}\n\nfunc (r *Reader) readSlice(line []byte) ([]interface{}, error) {\n\tn, err := replyLen(line)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tval := make([]interface{}, n)\n\tfor i := 0; i < len(val); i++ {\n\t\tv, err := r.ReadReply()\n\t\tif err != nil {\n\t\t\tif err == Nil {\n\t\t\t\tval[i] = nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err, ok := err.(RedisError); ok {\n\t\t\t\tval[i] = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tval[i] = v\n\t}\n\treturn val, nil\n}\n\nfunc (r *Reader) readMap(line []byte) (map[interface{}]interface{}, error) {\n\tn, err := replyLen(line)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tm := make(map[interface{}]interface{}, n)\n\tfor i := 0; i < n; i++ {\n\t\tk, err := r.ReadReply()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tv, err := r.ReadReply()\n\t\tif err != nil {\n\t\t\tif err == Nil {\n\t\t\t\tm[k] = nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err, ok := err.(RedisError); ok {\n\t\t\t\tm[k] = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tm[k] = v\n\t}\n\treturn m, nil\n}\n\n// -------------------------------\n\nfunc (r *Reader) ReadInt() (int64, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tswitch line[0] {\n\tcase RespInt, RespStatus:\n\t\treturn util.ParseInt(line[1:], 10, 64)\n\tcase RespString:\n\t\ts, err := r.readStringReply(line)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn util.ParseInt([]byte(s), 10, 64)\n\tcase RespBigInt:\n\t\tb, err := r.readBigInt(line)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif !b.IsInt64() {\n\t\t\treturn 0, fmt.Errorf(\"bigInt(%s) value out of range\", b.String())\n\t\t}\n\t\treturn b.Int64(), nil\n\t}\n\treturn 0, fmt.Errorf(\"redis: can't parse int reply: %.100q\", line)\n}\n\nfunc (r *Reader) ReadUint() (uint64, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tswitch line[0] {\n\tcase RespInt, RespStatus:\n\t\treturn util.ParseUint(line[1:], 10, 64)\n\tcase RespString:\n\t\ts, err := r.readStringReply(line)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn util.ParseUint([]byte(s), 10, 64)\n\tcase RespBigInt:\n\t\tb, err := r.readBigInt(line)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif !b.IsUint64() {\n\t\t\treturn 0, fmt.Errorf(\"bigInt(%s) value out of range\", b.String())\n\t\t}\n\t\treturn b.Uint64(), nil\n\t}\n\treturn 0, fmt.Errorf(\"redis: can't parse uint reply: %.100q\", line)\n}\n\nfunc (r *Reader) ReadFloat() (float64, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tswitch line[0] {\n\tcase RespFloat:\n\t\treturn r.readFloat(line)\n\tcase RespStatus:\n\t\treturn strconv.ParseFloat(string(line[1:]), 64)\n\tcase RespString:\n\t\ts, err := r.readStringReply(line)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn strconv.ParseFloat(s, 64)\n\t}\n\treturn 0, fmt.Errorf(\"redis: can't parse float reply: %.100q\", line)\n}\n\nfunc (r *Reader) ReadString() (string, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tswitch line[0] {\n\tcase RespStatus, RespInt, RespFloat:\n\t\treturn string(line[1:]), nil\n\tcase RespString:\n\t\treturn r.readStringReply(line)\n\tcase RespBool:\n\t\tb, err := r.readBool(line)\n\t\treturn strconv.FormatBool(b), err\n\tcase RespVerbatim:\n\t\treturn r.readVerb(line)\n\tcase RespBigInt:\n\t\tb, err := r.readBigInt(line)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn b.String(), nil\n\t}\n\treturn \"\", fmt.Errorf(\"redis: can't parse reply=%.100q reading string\", line)\n}\n\nfunc (r *Reader) ReadBool() (bool, error) {\n\ts, err := r.ReadString()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn s == \"OK\" || s == \"1\" || s == \"true\", nil\n}\n\nfunc (r *Reader) ReadSlice() ([]interface{}, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn r.readSlice(line)\n}\n\n// ReadFixedArrayLen read fixed array length.\nfunc (r *Reader) ReadFixedArrayLen(fixedLen int) error {\n\tn, err := r.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif n != fixedLen {\n\t\treturn fmt.Errorf(\"redis: got %d elements in the array, wanted %d\", n, fixedLen)\n\t}\n\treturn nil\n}\n\n// ReadArrayLen Read and return the length of the array.\nfunc (r *Reader) ReadArrayLen() (int, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tswitch line[0] {\n\tcase RespArray, RespSet, RespPush:\n\t\treturn replyLen(line)\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"redis: can't parse array/set/push reply: %.100q\", line)\n\t}\n}\n\n// ReadFixedMapLen reads fixed map length.\nfunc (r *Reader) ReadFixedMapLen(fixedLen int) error {\n\tn, err := r.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif n != fixedLen {\n\t\treturn fmt.Errorf(\"redis: got %d elements in the map, wanted %d\", n, fixedLen)\n\t}\n\treturn nil\n}\n\n// ReadMapLen reads the length of the map type.\n// If responding to the array type (RespArray/RespSet/RespPush),\n// it must be a multiple of 2 and return n/2.\n// Other types will return an error.\nfunc (r *Reader) ReadMapLen() (int, error) {\n\tline, err := r.ReadLine()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tswitch line[0] {\n\tcase RespMap:\n\t\treturn replyLen(line)\n\tcase RespArray, RespSet, RespPush:\n\t\t// Some commands and RESP2 protocol may respond to array types.\n\t\tn, err := replyLen(line)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif n%2 != 0 {\n\t\t\treturn 0, fmt.Errorf(\"redis: the length of the array must be a multiple of 2, got: %d\", n)\n\t\t}\n\t\treturn n / 2, nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"redis: can't parse map reply: %.100q\", line)\n\t}\n}\n\n// DiscardNext read and discard the data represented by the next line.\nfunc (r *Reader) DiscardNext() error {\n\tline, err := r.readLine()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn r.Discard(line)\n}\n\n// Discard the data represented by line.\nfunc (r *Reader) Discard(line []byte) (err error) {\n\tif len(line) == 0 {\n\t\treturn errors.New(\"redis: invalid line\")\n\t}\n\tswitch line[0] {\n\tcase RespStatus, RespError, RespInt, RespNil, RespFloat, RespBool, RespBigInt:\n\t\treturn nil\n\t}\n\n\tn, err := replyLen(line)\n\tif err != nil && err != Nil {\n\t\treturn err\n\t}\n\n\tswitch line[0] {\n\tcase RespBlobError, RespString, RespVerbatim:\n\t\t// +\\r\\n\n\t\t_, err = r.rd.Discard(n + 2)\n\t\treturn err\n\tcase RespArray, RespSet, RespPush:\n\t\tfor i := 0; i < n; i++ {\n\t\t\tif err = r.DiscardNext(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase RespMap, RespAttr:\n\t\t// Read key & value.\n\t\tfor i := 0; i < n*2; i++ {\n\t\t\tif err = r.DiscardNext(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"redis: can't parse %.100q\", line)\n}\n\nfunc replyLen(line []byte) (n int, err error) {\n\tn, err = util.Atoi(line[1:])\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif n < -1 {\n\t\treturn 0, fmt.Errorf(\"redis: invalid reply: %q\", line)\n\t}\n\n\tswitch line[0] {\n\tcase RespString, RespVerbatim, RespBlobError,\n\t\tRespArray, RespSet, RespPush, RespMap, RespAttr:\n\t\tif n == -1 {\n\t\t\treturn 0, Nil\n\t\t}\n\t}\n\treturn n, nil\n}\n\n// IsNilReply detects redis.Nil of RESP2.\nfunc IsNilReply(line []byte) bool {\n\treturn len(line) == 3 &&\n\t\t(line[0] == RespString || line[0] == RespArray) &&\n\t\tline[1] == '-' && line[2] == '1'\n}\n"
  },
  {
    "path": "internal/proto/reader_test.go",
    "content": "package proto_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\nfunc BenchmarkReader_ParseReply_Status(b *testing.B) {\n\tbenchmarkParseReply(b, \"+OK\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Int(b *testing.B) {\n\tbenchmarkParseReply(b, \":1\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Float(b *testing.B) {\n\tbenchmarkParseReply(b, \",123.456\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Bool(b *testing.B) {\n\tbenchmarkParseReply(b, \"#t\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_BigInt(b *testing.B) {\n\tbenchmarkParseReply(b, \"(3492890328409238509324850943850943825024385\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Error(b *testing.B) {\n\tbenchmarkParseReply(b, \"-Error message\\r\\n\", true)\n}\n\nfunc BenchmarkReader_ParseReply_Nil(b *testing.B) {\n\tbenchmarkParseReply(b, \"_\\r\\n\", true)\n}\n\nfunc BenchmarkReader_ParseReply_BlobError(b *testing.B) {\n\tbenchmarkParseReply(b, \"!21\\r\\nSYNTAX invalid syntax\", true)\n}\n\nfunc BenchmarkReader_ParseReply_String(b *testing.B) {\n\tbenchmarkParseReply(b, \"$5\\r\\nhello\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Verb(b *testing.B) {\n\tbenchmarkParseReply(b, \"$9\\r\\ntxt:hello\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Slice(b *testing.B) {\n\tbenchmarkParseReply(b, \"*2\\r\\n$5\\r\\nhello\\r\\n$5\\r\\nworld\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Set(b *testing.B) {\n\tbenchmarkParseReply(b, \"~2\\r\\n$5\\r\\nhello\\r\\n$5\\r\\nworld\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Push(b *testing.B) {\n\tbenchmarkParseReply(b, \">2\\r\\n$5\\r\\nhello\\r\\n$5\\r\\nworld\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Map(b *testing.B) {\n\tbenchmarkParseReply(b, \"%2\\r\\n$5\\r\\nhello\\r\\n$5\\r\\nworld\\r\\n+key\\r\\n+value\\r\\n\", false)\n}\n\nfunc BenchmarkReader_ParseReply_Attr(b *testing.B) {\n\tbenchmarkParseReply(b, \"%1\\r\\n+key\\r\\n+value\\r\\n+hello\\r\\n\", false)\n}\n\nfunc TestReader_ReadLine(t *testing.T) {\n\toriginal := bytes.Repeat([]byte(\"a\"), 8192)\n\toriginal[len(original)-2] = '\\r'\n\toriginal[len(original)-1] = '\\n'\n\tr := proto.NewReader(bytes.NewReader(original))\n\tread, err := r.ReadLine()\n\tif err != nil && err != io.EOF {\n\t\tt.Errorf(\"Should be able to read the full buffer: %v\", err)\n\t}\n\n\tif !bytes.Equal(read, original[:len(original)-2]) {\n\t\tt.Errorf(\"Values must be equal: %d expected %d\", len(read), len(original[:len(original)-2]))\n\t}\n}\n\nfunc benchmarkParseReply(b *testing.B, reply string, wanterr bool) {\n\tbuf := new(bytes.Buffer)\n\tfor i := 0; i < b.N; i++ {\n\t\tfmt.Fprint(buf, reply)\n\t}\n\tp := proto.NewReader(buf)\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := p.ReadReply()\n\t\tif !wanterr && err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/proto/redis_errors.go",
    "content": "package proto\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\n// Typed Redis errors for better error handling with wrapping support.\n// These errors maintain backward compatibility by keeping the same error messages.\n\n// LoadingError is returned when Redis is loading the dataset in memory.\ntype LoadingError struct {\n\tmsg string\n}\n\nfunc (e *LoadingError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *LoadingError) RedisError() {}\n\n// NewLoadingError creates a new LoadingError with the given message.\nfunc NewLoadingError(msg string) *LoadingError {\n\treturn &LoadingError{msg: msg}\n}\n\n// ReadOnlyError is returned when trying to write to a read-only replica.\ntype ReadOnlyError struct {\n\tmsg string\n}\n\nfunc (e *ReadOnlyError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *ReadOnlyError) RedisError() {}\n\n// NewReadOnlyError creates a new ReadOnlyError with the given message.\nfunc NewReadOnlyError(msg string) *ReadOnlyError {\n\treturn &ReadOnlyError{msg: msg}\n}\n\n// MovedError is returned when a key has been moved to a different node in a cluster.\ntype MovedError struct {\n\tmsg  string\n\taddr string\n}\n\nfunc (e *MovedError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *MovedError) RedisError() {}\n\n// Addr returns the address of the node where the key has been moved.\nfunc (e *MovedError) Addr() string {\n\treturn e.addr\n}\n\n// NewMovedError creates a new MovedError with the given message and address.\nfunc NewMovedError(msg string, addr string) *MovedError {\n\treturn &MovedError{msg: msg, addr: addr}\n}\n\n// AskError is returned when a key is being migrated and the client should ask another node.\ntype AskError struct {\n\tmsg  string\n\taddr string\n}\n\nfunc (e *AskError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *AskError) RedisError() {}\n\n// Addr returns the address of the node to ask.\nfunc (e *AskError) Addr() string {\n\treturn e.addr\n}\n\n// NewAskError creates a new AskError with the given message and address.\nfunc NewAskError(msg string, addr string) *AskError {\n\treturn &AskError{msg: msg, addr: addr}\n}\n\n// ClusterDownError is returned when the cluster is down.\ntype ClusterDownError struct {\n\tmsg string\n}\n\nfunc (e *ClusterDownError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *ClusterDownError) RedisError() {}\n\n// NewClusterDownError creates a new ClusterDownError with the given message.\nfunc NewClusterDownError(msg string) *ClusterDownError {\n\treturn &ClusterDownError{msg: msg}\n}\n\n// TryAgainError is returned when a command cannot be processed and should be retried.\ntype TryAgainError struct {\n\tmsg string\n}\n\nfunc (e *TryAgainError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *TryAgainError) RedisError() {}\n\n// NewTryAgainError creates a new TryAgainError with the given message.\nfunc NewTryAgainError(msg string) *TryAgainError {\n\treturn &TryAgainError{msg: msg}\n}\n\n// MasterDownError is returned when the master is down.\ntype MasterDownError struct {\n\tmsg string\n}\n\nfunc (e *MasterDownError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *MasterDownError) RedisError() {}\n\n// NewMasterDownError creates a new MasterDownError with the given message.\nfunc NewMasterDownError(msg string) *MasterDownError {\n\treturn &MasterDownError{msg: msg}\n}\n\n// MaxClientsError is returned when the maximum number of clients has been reached.\ntype MaxClientsError struct {\n\tmsg string\n}\n\nfunc (e *MaxClientsError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *MaxClientsError) RedisError() {}\n\n// NewMaxClientsError creates a new MaxClientsError with the given message.\nfunc NewMaxClientsError(msg string) *MaxClientsError {\n\treturn &MaxClientsError{msg: msg}\n}\n\n// AuthError is returned when authentication fails.\ntype AuthError struct {\n\tmsg string\n}\n\nfunc (e *AuthError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *AuthError) RedisError() {}\n\n// NewAuthError creates a new AuthError with the given message.\nfunc NewAuthError(msg string) *AuthError {\n\treturn &AuthError{msg: msg}\n}\n\n// PermissionError is returned when a user lacks required permissions.\ntype PermissionError struct {\n\tmsg string\n}\n\nfunc (e *PermissionError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *PermissionError) RedisError() {}\n\n// NewPermissionError creates a new PermissionError with the given message.\nfunc NewPermissionError(msg string) *PermissionError {\n\treturn &PermissionError{msg: msg}\n}\n\n// ExecAbortError is returned when a transaction is aborted.\ntype ExecAbortError struct {\n\tmsg string\n}\n\nfunc (e *ExecAbortError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *ExecAbortError) RedisError() {}\n\n// NewExecAbortError creates a new ExecAbortError with the given message.\nfunc NewExecAbortError(msg string) *ExecAbortError {\n\treturn &ExecAbortError{msg: msg}\n}\n\n// OOMError is returned when Redis is out of memory.\ntype OOMError struct {\n\tmsg string\n}\n\nfunc (e *OOMError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *OOMError) RedisError() {}\n\n// NewOOMError creates a new OOMError with the given message.\nfunc NewOOMError(msg string) *OOMError {\n\treturn &OOMError{msg: msg}\n}\n\n// NoReplicasError is returned when not enough replicas acknowledge a write.\n// This error occurs when using WAIT/WAITAOF commands or CLUSTER SETSLOT with\n// synchronous replication, and the required number of replicas cannot confirm\n// the write within the timeout period.\ntype NoReplicasError struct {\n\tmsg string\n}\n\nfunc (e *NoReplicasError) Error() string {\n\treturn e.msg\n}\n\nfunc (e *NoReplicasError) RedisError() {}\n\n// NewNoReplicasError creates a new NoReplicasError with the given message.\nfunc NewNoReplicasError(msg string) *NoReplicasError {\n\treturn &NoReplicasError{msg: msg}\n}\n\n// parseTypedRedisError parses a Redis error message and returns a typed error if applicable.\n// This function maintains backward compatibility by keeping the same error messages.\nfunc parseTypedRedisError(msg string) error {\n\t// Check for specific error patterns and return typed errors\n\tswitch {\n\tcase strings.HasPrefix(msg, \"LOADING \"):\n\t\treturn NewLoadingError(msg)\n\tcase strings.HasPrefix(msg, \"READONLY \"):\n\t\treturn NewReadOnlyError(msg)\n\tcase strings.HasPrefix(msg, \"MOVED \"):\n\t\t// Extract address from \"MOVED <slot> <addr>\"\n\t\taddr := extractAddr(msg)\n\t\treturn NewMovedError(msg, addr)\n\tcase strings.HasPrefix(msg, \"ASK \"):\n\t\t// Extract address from \"ASK <slot> <addr>\"\n\t\taddr := extractAddr(msg)\n\t\treturn NewAskError(msg, addr)\n\tcase strings.HasPrefix(msg, \"CLUSTERDOWN \"):\n\t\treturn NewClusterDownError(msg)\n\tcase strings.HasPrefix(msg, \"TRYAGAIN \"):\n\t\treturn NewTryAgainError(msg)\n\tcase strings.HasPrefix(msg, \"MASTERDOWN \"):\n\t\treturn NewMasterDownError(msg)\n\tcase strings.HasPrefix(msg, \"NOREPLICAS \"):\n\t\treturn NewNoReplicasError(msg)\n\tcase msg == \"ERR max number of clients reached\":\n\t\treturn NewMaxClientsError(msg)\n\tcase strings.HasPrefix(msg, \"NOAUTH \"), strings.HasPrefix(msg, \"WRONGPASS \"), strings.Contains(msg, \"unauthenticated\"):\n\t\treturn NewAuthError(msg)\n\tcase strings.HasPrefix(msg, \"NOPERM \"):\n\t\treturn NewPermissionError(msg)\n\tcase strings.HasPrefix(msg, \"EXECABORT \"):\n\t\treturn NewExecAbortError(msg)\n\tcase strings.HasPrefix(msg, \"OOM \"):\n\t\treturn NewOOMError(msg)\n\tdefault:\n\t\t// Return generic RedisError for unknown error types\n\t\treturn RedisError(msg)\n\t}\n}\n\n// extractAddr extracts the address from MOVED/ASK error messages.\n// Format: \"MOVED <slot> <addr>\" or \"ASK <slot> <addr>\"\nfunc extractAddr(msg string) string {\n\tind := strings.LastIndex(msg, \" \")\n\tif ind == -1 {\n\t\treturn \"\"\n\t}\n\treturn msg[ind+1:]\n}\n\n// IsLoadingError checks if an error is a LoadingError, even if wrapped.\nfunc IsLoadingError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar loadingErr *LoadingError\n\tif errors.As(err, &loadingErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with LOADING prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"LOADING \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"LOADING \")\n}\n\n// IsReadOnlyError checks if an error is a ReadOnlyError, even if wrapped.\nfunc IsReadOnlyError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar readOnlyErr *ReadOnlyError\n\tif errors.As(err, &readOnlyErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with READONLY prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"READONLY \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"READONLY \")\n}\n\n// IsMovedError checks if an error is a MovedError, even if wrapped.\n// Returns the error and a boolean indicating if it's a MovedError.\nfunc IsMovedError(err error) (*MovedError, bool) {\n\tif err == nil {\n\t\treturn nil, false\n\t}\n\tvar movedErr *MovedError\n\tif errors.As(err, &movedErr) {\n\t\treturn movedErr, true\n\t}\n\t// Fallback to string checking for backward compatibility\n\ts := err.Error()\n\tif strings.HasPrefix(s, \"MOVED \") {\n\t\t// Parse: MOVED 3999 127.0.0.1:6381\n\t\tparts := strings.Split(s, \" \")\n\t\tif len(parts) == 3 {\n\t\t\treturn &MovedError{msg: s, addr: parts[2]}, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// IsAskError checks if an error is an AskError, even if wrapped.\n// Returns the error and a boolean indicating if it's an AskError.\nfunc IsAskError(err error) (*AskError, bool) {\n\tif err == nil {\n\t\treturn nil, false\n\t}\n\tvar askErr *AskError\n\tif errors.As(err, &askErr) {\n\t\treturn askErr, true\n\t}\n\t// Fallback to string checking for backward compatibility\n\ts := err.Error()\n\tif strings.HasPrefix(s, \"ASK \") {\n\t\t// Parse: ASK 3999 127.0.0.1:6381\n\t\tparts := strings.Split(s, \" \")\n\t\tif len(parts) == 3 {\n\t\t\treturn &AskError{msg: s, addr: parts[2]}, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// IsClusterDownError checks if an error is a ClusterDownError, even if wrapped.\nfunc IsClusterDownError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar clusterDownErr *ClusterDownError\n\tif errors.As(err, &clusterDownErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with CLUSTERDOWN prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"CLUSTERDOWN \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"CLUSTERDOWN \")\n}\n\n// IsTryAgainError checks if an error is a TryAgainError, even if wrapped.\nfunc IsTryAgainError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar tryAgainErr *TryAgainError\n\tif errors.As(err, &tryAgainErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with TRYAGAIN prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"TRYAGAIN \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"TRYAGAIN \")\n}\n\n// IsMasterDownError checks if an error is a MasterDownError, even if wrapped.\nfunc IsMasterDownError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar masterDownErr *MasterDownError\n\tif errors.As(err, &masterDownErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with MASTERDOWN prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"MASTERDOWN \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"MASTERDOWN \")\n}\n\n// IsMaxClientsError checks if an error is a MaxClientsError, even if wrapped.\nfunc IsMaxClientsError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar maxClientsErr *MaxClientsError\n\tif errors.As(err, &maxClientsErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with max clients prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"ERR max number of clients reached\") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"ERR max number of clients reached\")\n}\n\n// IsAuthError checks if an error is an AuthError, even if wrapped.\nfunc IsAuthError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar authErr *AuthError\n\tif errors.As(err, &authErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with auth error prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) {\n\t\ts := redisErr.Error()\n\t\treturn strings.HasPrefix(s, \"NOAUTH \") || strings.HasPrefix(s, \"WRONGPASS \") || strings.Contains(s, \"unauthenticated\")\n\t}\n\t// Fallback to string checking for backward compatibility\n\ts := err.Error()\n\treturn strings.HasPrefix(s, \"NOAUTH \") || strings.HasPrefix(s, \"WRONGPASS \") || strings.Contains(s, \"unauthenticated\")\n}\n\n// IsPermissionError checks if an error is a PermissionError, even if wrapped.\nfunc IsPermissionError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar permErr *PermissionError\n\tif errors.As(err, &permErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with NOPERM prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"NOPERM \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"NOPERM \")\n}\n\n// IsExecAbortError checks if an error is an ExecAbortError, even if wrapped.\nfunc IsExecAbortError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar execAbortErr *ExecAbortError\n\tif errors.As(err, &execAbortErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with EXECABORT prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"EXECABORT \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"EXECABORT \")\n}\n\n// IsOOMError checks if an error is an OOMError, even if wrapped.\nfunc IsOOMError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar oomErr *OOMError\n\tif errors.As(err, &oomErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with OOM prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"OOM \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"OOM \")\n}\n\n// IsNoReplicasError checks if an error is a NoReplicasError, even if wrapped.\nfunc IsNoReplicasError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar noReplicasErr *NoReplicasError\n\tif errors.As(err, &noReplicasErr) {\n\t\treturn true\n\t}\n\t// Check if wrapped error is a RedisError with NOREPLICAS prefix\n\tvar redisErr RedisError\n\tif errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), \"NOREPLICAS \") {\n\t\treturn true\n\t}\n\t// Fallback to string checking for backward compatibility\n\treturn strings.HasPrefix(err.Error(), \"NOREPLICAS \")\n}\n"
  },
  {
    "path": "internal/proto/redis_errors_test.go",
    "content": "package proto\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\n// TestTypedRedisErrors tests that typed Redis errors are created correctly\nfunc TestTypedRedisErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\terrorMsg     string\n\t\texpectedType interface{}\n\t\texpectedMsg  string\n\t\tcheckFunc    func(error) bool\n\t\textractAddr  func(error) string\n\t}{\n\t\t{\n\t\t\tname:         \"LOADING error\",\n\t\t\terrorMsg:     \"LOADING Redis is loading the dataset in memory\",\n\t\t\texpectedType: &LoadingError{},\n\t\t\texpectedMsg:  \"LOADING Redis is loading the dataset in memory\",\n\t\t\tcheckFunc:    IsLoadingError,\n\t\t},\n\t\t{\n\t\t\tname:         \"READONLY error\",\n\t\t\terrorMsg:     \"READONLY You can't write against a read only replica\",\n\t\t\texpectedType: &ReadOnlyError{},\n\t\t\texpectedMsg:  \"READONLY You can't write against a read only replica\",\n\t\t\tcheckFunc:    IsReadOnlyError,\n\t\t},\n\t\t{\n\t\t\tname:         \"MOVED error\",\n\t\t\terrorMsg:     \"MOVED 3999 127.0.0.1:6381\",\n\t\t\texpectedType: &MovedError{},\n\t\t\texpectedMsg:  \"MOVED 3999 127.0.0.1:6381\",\n\t\t\tcheckFunc: func(err error) bool {\n\t\t\t\t_, ok := IsMovedError(err)\n\t\t\t\treturn ok\n\t\t\t},\n\t\t\textractAddr: func(err error) string {\n\t\t\t\tif movedErr, ok := IsMovedError(err); ok {\n\t\t\t\t\treturn movedErr.Addr()\n\t\t\t\t}\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"ASK error\",\n\t\t\terrorMsg:     \"ASK 3999 127.0.0.1:6381\",\n\t\t\texpectedType: &AskError{},\n\t\t\texpectedMsg:  \"ASK 3999 127.0.0.1:6381\",\n\t\t\tcheckFunc: func(err error) bool {\n\t\t\t\t_, ok := IsAskError(err)\n\t\t\t\treturn ok\n\t\t\t},\n\t\t\textractAddr: func(err error) string {\n\t\t\t\tif askErr, ok := IsAskError(err); ok {\n\t\t\t\t\treturn askErr.Addr()\n\t\t\t\t}\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"CLUSTERDOWN error\",\n\t\t\terrorMsg:     \"CLUSTERDOWN The cluster is down\",\n\t\t\texpectedType: &ClusterDownError{},\n\t\t\texpectedMsg:  \"CLUSTERDOWN The cluster is down\",\n\t\t\tcheckFunc:    IsClusterDownError,\n\t\t},\n\t\t{\n\t\t\tname:         \"TRYAGAIN error\",\n\t\t\terrorMsg:     \"TRYAGAIN Multiple keys request during rehashing of slot\",\n\t\t\texpectedType: &TryAgainError{},\n\t\t\texpectedMsg:  \"TRYAGAIN Multiple keys request during rehashing of slot\",\n\t\t\tcheckFunc:    IsTryAgainError,\n\t\t},\n\t\t{\n\t\t\tname:         \"MASTERDOWN error\",\n\t\t\terrorMsg:     \"MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'\",\n\t\t\texpectedType: &MasterDownError{},\n\t\t\texpectedMsg:  \"MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'\",\n\t\t\tcheckFunc:    IsMasterDownError,\n\t\t},\n\t\t{\n\t\t\tname:         \"Max clients error\",\n\t\t\terrorMsg:     \"ERR max number of clients reached\",\n\t\t\texpectedType: &MaxClientsError{},\n\t\t\texpectedMsg:  \"ERR max number of clients reached\",\n\t\t\tcheckFunc:    IsMaxClientsError,\n\t\t},\n\t\t{\n\t\t\tname:         \"NOAUTH error\",\n\t\t\terrorMsg:     \"NOAUTH Authentication required\",\n\t\t\texpectedType: &AuthError{},\n\t\t\texpectedMsg:  \"NOAUTH Authentication required\",\n\t\t\tcheckFunc:    IsAuthError,\n\t\t},\n\t\t{\n\t\t\tname:         \"WRONGPASS error\",\n\t\t\terrorMsg:     \"WRONGPASS invalid username-password pair\",\n\t\t\texpectedType: &AuthError{},\n\t\t\texpectedMsg:  \"WRONGPASS invalid username-password pair\",\n\t\t\tcheckFunc:    IsAuthError,\n\t\t},\n\t\t{\n\t\t\tname:         \"unauthenticated error\",\n\t\t\terrorMsg:     \"ERR unauthenticated\",\n\t\t\texpectedType: &AuthError{},\n\t\t\texpectedMsg:  \"ERR unauthenticated\",\n\t\t\tcheckFunc:    IsAuthError,\n\t\t},\n\t\t{\n\t\t\tname:         \"NOPERM error\",\n\t\t\terrorMsg:     \"NOPERM this user has no permissions to run the 'flushdb' command\",\n\t\t\texpectedType: &PermissionError{},\n\t\t\texpectedMsg:  \"NOPERM this user has no permissions to run the 'flushdb' command\",\n\t\t\tcheckFunc:    IsPermissionError,\n\t\t},\n\t\t{\n\t\t\tname:         \"EXECABORT error\",\n\t\t\terrorMsg:     \"EXECABORT Transaction discarded because of previous errors\",\n\t\t\texpectedType: &ExecAbortError{},\n\t\t\texpectedMsg:  \"EXECABORT Transaction discarded because of previous errors\",\n\t\t\tcheckFunc:    IsExecAbortError,\n\t\t},\n\t\t{\n\t\t\tname:         \"OOM error\",\n\t\t\terrorMsg:     \"OOM command not allowed when used memory > 'maxmemory'\",\n\t\t\texpectedType: &OOMError{},\n\t\t\texpectedMsg:  \"OOM command not allowed when used memory > 'maxmemory'\",\n\t\t\tcheckFunc:    IsOOMError,\n\t\t},\n\t\t{\n\t\t\tname:         \"NOREPLICAS error\",\n\t\t\terrorMsg:     \"NOREPLICAS Not enough good replicas to write\",\n\t\t\texpectedType: &NoReplicasError{},\n\t\t\texpectedMsg:  \"NOREPLICAS Not enough good replicas to write\",\n\t\t\tcheckFunc:    IsNoReplicasError,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := parseTypedRedisError(tt.errorMsg)\n\n\t\t\t// Check error message is preserved\n\t\t\tif err.Error() != tt.expectedMsg {\n\t\t\t\tt.Errorf(\"Error message mismatch: got %q, want %q\", err.Error(), tt.expectedMsg)\n\t\t\t}\n\n\t\t\t// Check error type using errors.As\n\t\t\tif !errors.As(err, &tt.expectedType) {\n\t\t\t\tt.Errorf(\"Error type mismatch: expected %T, got %T\", tt.expectedType, err)\n\t\t\t}\n\n\t\t\t// Check using the helper function\n\t\t\tif tt.checkFunc != nil && !tt.checkFunc(err) {\n\t\t\t\tt.Errorf(\"Helper function returned false for error: %v\", err)\n\t\t\t}\n\n\t\t\t// Check address extraction for MOVED/ASK errors\n\t\t\tif tt.extractAddr != nil {\n\t\t\t\taddr := tt.extractAddr(err)\n\t\t\t\tif addr == \"\" {\n\t\t\t\t\tt.Errorf(\"Failed to extract address from error: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestWrappedTypedErrors tests that typed errors work correctly when wrapped\nfunc TestWrappedTypedErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\terrorMsg  string\n\t\tcheckFunc func(error) bool\n\t}{\n\t\t{\n\t\t\tname:      \"Wrapped LOADING error\",\n\t\t\terrorMsg:  \"LOADING Redis is loading the dataset in memory\",\n\t\t\tcheckFunc: IsLoadingError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped READONLY error\",\n\t\t\terrorMsg:  \"READONLY You can't write against a read only replica\",\n\t\t\tcheckFunc: IsReadOnlyError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped CLUSTERDOWN error\",\n\t\t\terrorMsg:  \"CLUSTERDOWN The cluster is down\",\n\t\t\tcheckFunc: IsClusterDownError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped TRYAGAIN error\",\n\t\t\terrorMsg:  \"TRYAGAIN Multiple keys request during rehashing of slot\",\n\t\t\tcheckFunc: IsTryAgainError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped MASTERDOWN error\",\n\t\t\terrorMsg:  \"MASTERDOWN Link with MASTER is down\",\n\t\t\tcheckFunc: IsMasterDownError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped Max clients error\",\n\t\t\terrorMsg:  \"ERR max number of clients reached\",\n\t\t\tcheckFunc: IsMaxClientsError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped NOAUTH error\",\n\t\t\terrorMsg:  \"NOAUTH Authentication required\",\n\t\t\tcheckFunc: IsAuthError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped WRONGPASS error\",\n\t\t\terrorMsg:  \"WRONGPASS invalid username-password pair\",\n\t\t\tcheckFunc: IsAuthError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped unauthenticated error\",\n\t\t\terrorMsg:  \"ERR unauthenticated\",\n\t\t\tcheckFunc: IsAuthError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped NOPERM error\",\n\t\t\terrorMsg:  \"NOPERM this user has no permissions to run the 'flushdb' command\",\n\t\t\tcheckFunc: IsPermissionError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped EXECABORT error\",\n\t\t\terrorMsg:  \"EXECABORT Transaction discarded because of previous errors\",\n\t\t\tcheckFunc: IsExecAbortError,\n\t\t},\n\t\t{\n\t\t\tname:      \"Wrapped OOM error\",\n\t\t\terrorMsg:  \"OOM command not allowed when used memory > 'maxmemory'\",\n\t\t\tcheckFunc: IsOOMError,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create the typed error\n\t\t\terr := parseTypedRedisError(tt.errorMsg)\n\n\t\t\t// Wrap it multiple times (simulating hook wrapping)\n\t\t\twrappedErr := fmt.Errorf(\"hook error: %w\", err)\n\t\t\tdoubleWrappedErr := fmt.Errorf(\"another wrapper: %w\", wrappedErr)\n\n\t\t\t// Check that the helper function still works with wrapped errors\n\t\t\tif !tt.checkFunc(doubleWrappedErr) {\n\t\t\t\tt.Errorf(\"Helper function failed to detect wrapped error: %v\", doubleWrappedErr)\n\t\t\t}\n\n\t\t\t// Verify the original error message is still accessible\n\t\t\tif !errors.Is(doubleWrappedErr, err) {\n\t\t\t\tt.Errorf(\"errors.Is failed to match wrapped error\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestMovedAndAskErrorAddressExtraction tests address extraction from MOVED/ASK errors\nfunc TestMovedAndAskErrorAddressExtraction(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\terrorMsg     string\n\t\texpectedAddr string\n\t}{\n\t\t{\n\t\t\tname:         \"MOVED with IP address\",\n\t\t\terrorMsg:     \"MOVED 3999 127.0.0.1:6381\",\n\t\t\texpectedAddr: \"127.0.0.1:6381\",\n\t\t},\n\t\t{\n\t\t\tname:         \"MOVED with hostname\",\n\t\t\terrorMsg:     \"MOVED 3999 redis-node-1:6379\",\n\t\t\texpectedAddr: \"redis-node-1:6379\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ASK with IP address\",\n\t\t\terrorMsg:     \"ASK 3999 192.168.1.100:6380\",\n\t\t\texpectedAddr: \"192.168.1.100:6380\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ASK with hostname\",\n\t\t\terrorMsg:     \"ASK 3999 redis-node-2:6379\",\n\t\t\texpectedAddr: \"redis-node-2:6379\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := parseTypedRedisError(tt.errorMsg)\n\n\t\t\tvar addr string\n\t\t\tif movedErr, ok := IsMovedError(err); ok {\n\t\t\t\taddr = movedErr.Addr()\n\t\t\t} else if askErr, ok := IsAskError(err); ok {\n\t\t\t\taddr = askErr.Addr()\n\t\t\t} else {\n\t\t\t\tt.Fatalf(\"Error is neither MOVED nor ASK: %v\", err)\n\t\t\t}\n\n\t\t\tif addr != tt.expectedAddr {\n\t\t\t\tt.Errorf(\"Address mismatch: got %q, want %q\", addr, tt.expectedAddr)\n\t\t\t}\n\n\t\t\t// Test with wrapped error\n\t\t\twrappedErr := fmt.Errorf(\"wrapped: %w\", err)\n\t\t\tif movedErr, ok := IsMovedError(wrappedErr); ok {\n\t\t\t\taddr = movedErr.Addr()\n\t\t\t} else if askErr, ok := IsAskError(wrappedErr); ok {\n\t\t\t\taddr = askErr.Addr()\n\t\t\t} else {\n\t\t\t\tt.Fatalf(\"Wrapped error is neither MOVED nor ASK: %v\", wrappedErr)\n\t\t\t}\n\n\t\t\tif addr != tt.expectedAddr {\n\t\t\t\tt.Errorf(\"Address mismatch in wrapped error: got %q, want %q\", addr, tt.expectedAddr)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestGenericRedisError tests that unknown Redis errors fall back to generic RedisError\nfunc TestGenericRedisError(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\terrorMsg string\n\t}{\n\t\t{\n\t\t\tname:     \"Generic error\",\n\t\t\terrorMsg: \"ERR unknown command\",\n\t\t},\n\t\t{\n\t\t\tname:     \"WRONGTYPE error\",\n\t\t\terrorMsg: \"WRONGTYPE Operation against a key holding the wrong kind of value\",\n\t\t},\n\t\t{\n\t\t\tname:     \"BUSYKEY error\",\n\t\t\terrorMsg: \"BUSYKEY Target key name already exists\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := parseTypedRedisError(tt.errorMsg)\n\n\t\t\t// Should be a generic RedisError\n\t\t\tif _, ok := err.(RedisError); !ok {\n\t\t\t\tt.Errorf(\"Expected RedisError, got %T\", err)\n\t\t\t}\n\n\t\t\t// Should preserve the error message\n\t\t\tif err.Error() != tt.errorMsg {\n\t\t\t\tt.Errorf(\"Error message mismatch: got %q, want %q\", err.Error(), tt.errorMsg)\n\t\t\t}\n\n\t\t\t// Should not match any typed error checks\n\t\t\tif IsLoadingError(err) || IsReadOnlyError(err) || IsClusterDownError(err) ||\n\t\t\t\tIsTryAgainError(err) || IsMasterDownError(err) || IsMaxClientsError(err) ||\n\t\t\t\tIsAuthError(err) || IsPermissionError(err) || IsExecAbortError(err) || IsOOMError(err) {\n\t\t\t\tt.Errorf(\"Generic error incorrectly matched a typed error check\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestBackwardCompatibility tests that error messages remain unchanged\nfunc TestBackwardCompatibility(t *testing.T) {\n\t// This test ensures that the error messages are exactly the same as before\n\t// to maintain backward compatibility with code that checks error messages\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"LOADING Redis is loading the dataset in memory\", \"LOADING Redis is loading the dataset in memory\"},\n\t\t{\"READONLY You can't write against a read only replica\", \"READONLY You can't write against a read only replica\"},\n\t\t{\"MOVED 3999 127.0.0.1:6381\", \"MOVED 3999 127.0.0.1:6381\"},\n\t\t{\"ASK 3999 127.0.0.1:6381\", \"ASK 3999 127.0.0.1:6381\"},\n\t\t{\"CLUSTERDOWN The cluster is down\", \"CLUSTERDOWN The cluster is down\"},\n\t\t{\"TRYAGAIN Multiple keys request during rehashing of slot\", \"TRYAGAIN Multiple keys request during rehashing of slot\"},\n\t\t{\"MASTERDOWN Link with MASTER is down\", \"MASTERDOWN Link with MASTER is down\"},\n\t\t{\"ERR max number of clients reached\", \"ERR max number of clients reached\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\terr := parseTypedRedisError(tt.input)\n\t\t\tif err.Error() != tt.expected {\n\t\t\t\tt.Errorf(\"Error message changed! Got %q, want %q\", err.Error(), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/proto/scan.go",
    "content": "package proto\n\nimport (\n\t\"encoding\"\n\t\"fmt\"\n\t\"net\"\n\t\"reflect\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\n// Scan parses bytes `b` to `v` with appropriate type.\n//\n//nolint:gocyclo\nfunc Scan(b []byte, v interface{}) error {\n\tswitch v := v.(type) {\n\tcase nil:\n\t\treturn fmt.Errorf(\"redis: Scan(nil)\")\n\tcase *string:\n\t\t*v = util.BytesToString(b)\n\t\treturn nil\n\tcase *[]byte:\n\t\t*v = b\n\t\treturn nil\n\tcase *int:\n\t\tvar err error\n\t\t*v, err = util.Atoi(b)\n\t\treturn err\n\tcase *int8:\n\t\tn, err := util.ParseInt(b, 10, 8)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = int8(n)\n\t\treturn nil\n\tcase *int16:\n\t\tn, err := util.ParseInt(b, 10, 16)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = int16(n)\n\t\treturn nil\n\tcase *int32:\n\t\tn, err := util.ParseInt(b, 10, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = int32(n)\n\t\treturn nil\n\tcase *int64:\n\t\tn, err := util.ParseInt(b, 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = n\n\t\treturn nil\n\tcase *uint:\n\t\tn, err := util.ParseUint(b, 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = uint(n)\n\t\treturn nil\n\tcase *uint8:\n\t\tn, err := util.ParseUint(b, 10, 8)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = uint8(n)\n\t\treturn nil\n\tcase *uint16:\n\t\tn, err := util.ParseUint(b, 10, 16)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = uint16(n)\n\t\treturn nil\n\tcase *uint32:\n\t\tn, err := util.ParseUint(b, 10, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = uint32(n)\n\t\treturn nil\n\tcase *uint64:\n\t\tn, err := util.ParseUint(b, 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = n\n\t\treturn nil\n\tcase *float32:\n\t\tn, err := util.ParseFloat(b, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = float32(n)\n\t\treturn err\n\tcase *float64:\n\t\tvar err error\n\t\t*v, err = util.ParseFloat(b, 64)\n\t\treturn err\n\tcase *bool:\n\t\t*v = len(b) == 1 && b[0] == '1'\n\t\treturn nil\n\tcase *time.Time:\n\t\tvar err error\n\t\t*v, err = time.Parse(time.RFC3339Nano, util.BytesToString(b))\n\t\treturn err\n\tcase *time.Duration:\n\t\tn, err := util.ParseInt(b, 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*v = time.Duration(n)\n\t\treturn nil\n\tcase encoding.BinaryUnmarshaler:\n\t\treturn v.UnmarshalBinary(b)\n\tcase *net.IP:\n\t\t*v = b\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t\"redis: can't unmarshal %T (consider implementing BinaryUnmarshaler)\", v)\n\t}\n}\n\nfunc ScanSlice(data []string, slice interface{}) error {\n\tv := reflect.ValueOf(slice)\n\tif !v.IsValid() {\n\t\treturn fmt.Errorf(\"redis: ScanSlice(nil)\")\n\t}\n\tif v.Kind() != reflect.Ptr {\n\t\treturn fmt.Errorf(\"redis: ScanSlice(non-pointer %T)\", slice)\n\t}\n\tv = v.Elem()\n\tif v.Kind() != reflect.Slice {\n\t\treturn fmt.Errorf(\"redis: ScanSlice(non-slice %T)\", slice)\n\t}\n\n\tnext := makeSliceNextElemFunc(v)\n\tfor i, s := range data {\n\t\telem := next()\n\t\tif err := Scan([]byte(s), elem.Addr().Interface()); err != nil {\n\t\t\terr = fmt.Errorf(\"redis: ScanSlice index=%d value=%q failed: %w\", i, s, err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc makeSliceNextElemFunc(v reflect.Value) func() reflect.Value {\n\telemType := v.Type().Elem()\n\n\tif elemType.Kind() == reflect.Ptr {\n\t\telemType = elemType.Elem()\n\t\treturn func() reflect.Value {\n\t\t\tif v.Len() < v.Cap() {\n\t\t\t\tv.Set(v.Slice(0, v.Len()+1))\n\t\t\t\telem := v.Index(v.Len() - 1)\n\t\t\t\tif elem.IsNil() {\n\t\t\t\t\telem.Set(reflect.New(elemType))\n\t\t\t\t}\n\t\t\t\treturn elem.Elem()\n\t\t\t}\n\n\t\t\telem := reflect.New(elemType)\n\t\t\tv.Set(reflect.Append(v, elem))\n\t\t\treturn elem.Elem()\n\t\t}\n\t}\n\n\tzero := reflect.Zero(elemType)\n\treturn func() reflect.Value {\n\t\tif v.Len() < v.Cap() {\n\t\t\tv.Set(v.Slice(0, v.Len()+1))\n\t\t\treturn v.Index(v.Len() - 1)\n\t\t}\n\n\t\tv.Set(reflect.Append(v, zero))\n\t\treturn v.Index(v.Len() - 1)\n\t}\n}\n"
  },
  {
    "path": "internal/proto/scan_test.go",
    "content": "package proto_test\n\nimport (\n\t\"encoding/json\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\ntype testScanSliceStruct struct {\n\tID   int\n\tName string\n}\n\nfunc (s *testScanSliceStruct) MarshalBinary() ([]byte, error) {\n\treturn json.Marshal(s)\n}\n\nfunc (s *testScanSliceStruct) UnmarshalBinary(b []byte) error {\n\treturn json.Unmarshal(b, s)\n}\n\nvar _ = Describe(\"ScanSlice\", func() {\n\tdata := []string{\n\t\t`{\"ID\":-1,\"Name\":\"Back Yu\"}`,\n\t\t`{\"ID\":1,\"Name\":\"szyhf\"}`,\n\t}\n\n\tIt(\"[]testScanSliceStruct\", func() {\n\t\tvar slice []testScanSliceStruct\n\t\terr := proto.ScanSlice(data, &slice)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(slice).To(Equal([]testScanSliceStruct{\n\t\t\t{-1, \"Back Yu\"},\n\t\t\t{1, \"szyhf\"},\n\t\t}))\n\t})\n\n\tIt(\"var testContainer []*testScanSliceStruct\", func() {\n\t\tvar slice []*testScanSliceStruct\n\t\terr := proto.ScanSlice(data, &slice)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(slice).To(Equal([]*testScanSliceStruct{\n\t\t\t{-1, \"Back Yu\"},\n\t\t\t{1, \"szyhf\"},\n\t\t}))\n\t})\n})\n"
  },
  {
    "path": "internal/proto/writer.go",
    "content": "package proto\n\nimport (\n\t\"encoding\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\ntype writer interface {\n\tio.Writer\n\tio.ByteWriter\n\t// WriteString implement io.StringWriter.\n\tWriteString(s string) (n int, err error)\n}\n\ntype Writer struct {\n\twriter\n\n\tlenBuf []byte\n\tnumBuf []byte\n}\n\nfunc NewWriter(wr writer) *Writer {\n\treturn &Writer{\n\t\twriter: wr,\n\n\t\tlenBuf: make([]byte, 64),\n\t\tnumBuf: make([]byte, 64),\n\t}\n}\n\nfunc (w *Writer) WriteArgs(args []interface{}) error {\n\tif err := w.WriteByte(RespArray); err != nil {\n\t\treturn err\n\t}\n\n\tif err := w.writeLen(len(args)); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, arg := range args {\n\t\tif err := w.WriteArg(arg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (w *Writer) writeLen(n int) error {\n\tw.lenBuf = strconv.AppendUint(w.lenBuf[:0], uint64(n), 10)\n\tw.lenBuf = append(w.lenBuf, '\\r', '\\n')\n\t_, err := w.Write(w.lenBuf)\n\treturn err\n}\n\nfunc (w *Writer) WriteArg(v interface{}) error {\n\tswitch v := v.(type) {\n\tcase nil:\n\t\treturn w.string(\"\")\n\tcase string:\n\t\treturn w.string(v)\n\tcase *string:\n\t\tif v == nil {\n\t\t\treturn w.string(\"\")\n\t\t}\n\t\treturn w.string(*v)\n\tcase []byte:\n\t\treturn w.bytes(v)\n\tcase int:\n\t\treturn w.int(int64(v))\n\tcase *int:\n\t\tif v == nil {\n\t\t\treturn w.int(0)\n\t\t}\n\t\treturn w.int(int64(*v))\n\tcase int8:\n\t\treturn w.int(int64(v))\n\tcase *int8:\n\t\tif v == nil {\n\t\t\treturn w.int(0)\n\t\t}\n\t\treturn w.int(int64(*v))\n\tcase int16:\n\t\treturn w.int(int64(v))\n\tcase *int16:\n\t\tif v == nil {\n\t\t\treturn w.int(0)\n\t\t}\n\t\treturn w.int(int64(*v))\n\tcase int32:\n\t\treturn w.int(int64(v))\n\tcase *int32:\n\t\tif v == nil {\n\t\t\treturn w.int(0)\n\t\t}\n\t\treturn w.int(int64(*v))\n\tcase int64:\n\t\treturn w.int(v)\n\tcase *int64:\n\t\tif v == nil {\n\t\t\treturn w.int(0)\n\t\t}\n\t\treturn w.int(*v)\n\tcase uint:\n\t\treturn w.uint(uint64(v))\n\tcase *uint:\n\t\tif v == nil {\n\t\t\treturn w.uint(0)\n\t\t}\n\t\treturn w.uint(uint64(*v))\n\tcase uint8:\n\t\treturn w.uint(uint64(v))\n\tcase *uint8:\n\t\tif v == nil {\n\t\t\treturn w.string(\"\")\n\t\t}\n\t\treturn w.uint(uint64(*v))\n\tcase uint16:\n\t\treturn w.uint(uint64(v))\n\tcase *uint16:\n\t\tif v == nil {\n\t\t\treturn w.uint(0)\n\t\t}\n\t\treturn w.uint(uint64(*v))\n\tcase uint32:\n\t\treturn w.uint(uint64(v))\n\tcase *uint32:\n\t\tif v == nil {\n\t\t\treturn w.uint(0)\n\t\t}\n\t\treturn w.uint(uint64(*v))\n\tcase uint64:\n\t\treturn w.uint(v)\n\tcase *uint64:\n\t\tif v == nil {\n\t\t\treturn w.uint(0)\n\t\t}\n\t\treturn w.uint(*v)\n\tcase float32:\n\t\treturn w.float(float64(v))\n\tcase *float32:\n\t\tif v == nil {\n\t\t\treturn w.float(0)\n\t\t}\n\t\treturn w.float(float64(*v))\n\tcase float64:\n\t\treturn w.float(v)\n\tcase *float64:\n\t\tif v == nil {\n\t\t\treturn w.float(0)\n\t\t}\n\t\treturn w.float(*v)\n\tcase bool:\n\t\tif v {\n\t\t\treturn w.int(1)\n\t\t}\n\t\treturn w.int(0)\n\tcase *bool:\n\t\tif v == nil {\n\t\t\treturn w.int(0)\n\t\t}\n\t\tif *v {\n\t\t\treturn w.int(1)\n\t\t}\n\t\treturn w.int(0)\n\tcase time.Time:\n\t\tw.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)\n\t\treturn w.bytes(w.numBuf)\n\tcase *time.Time:\n\t\tif v == nil {\n\t\t\tv = &time.Time{}\n\t\t}\n\t\tw.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)\n\t\treturn w.bytes(w.numBuf)\n\tcase time.Duration:\n\t\treturn w.int(v.Nanoseconds())\n\tcase *time.Duration:\n\t\tif v == nil {\n\t\t\treturn w.int(0)\n\t\t}\n\t\treturn w.int(v.Nanoseconds())\n\tcase encoding.BinaryMarshaler:\n\t\tb, err := v.MarshalBinary()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn w.bytes(b)\n\tcase net.IP:\n\t\treturn w.bytes(v)\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t\"redis: can't marshal %T (implement encoding.BinaryMarshaler)\", v)\n\t}\n}\n\nfunc (w *Writer) bytes(b []byte) error {\n\tif err := w.WriteByte(RespString); err != nil {\n\t\treturn err\n\t}\n\n\tif err := w.writeLen(len(b)); err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := w.Write(b); err != nil {\n\t\treturn err\n\t}\n\n\treturn w.crlf()\n}\n\nfunc (w *Writer) string(s string) error {\n\treturn w.bytes(util.StringToBytes(s))\n}\n\nfunc (w *Writer) uint(n uint64) error {\n\tw.numBuf = strconv.AppendUint(w.numBuf[:0], n, 10)\n\treturn w.bytes(w.numBuf)\n}\n\nfunc (w *Writer) int(n int64) error {\n\tw.numBuf = strconv.AppendInt(w.numBuf[:0], n, 10)\n\treturn w.bytes(w.numBuf)\n}\n\nfunc (w *Writer) float(f float64) error {\n\tw.numBuf = strconv.AppendFloat(w.numBuf[:0], f, 'f', -1, 64)\n\treturn w.bytes(w.numBuf)\n}\n\nfunc (w *Writer) crlf() error {\n\tif err := w.WriteByte('\\r'); err != nil {\n\t\treturn err\n\t}\n\treturn w.WriteByte('\\n')\n}\n"
  },
  {
    "path": "internal/proto/writer_test.go",
    "content": "package proto_test\n\nimport (\n\t\"bytes\"\n\t\"encoding\"\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\ntype MyType struct{}\n\nvar _ encoding.BinaryMarshaler = (*MyType)(nil)\n\nfunc (t *MyType) MarshalBinary() ([]byte, error) {\n\treturn []byte(\"hello\"), nil\n}\n\nvar _ = Describe(\"WriteBuffer\", func() {\n\tvar buf *bytes.Buffer\n\tvar wr *proto.Writer\n\n\tBeforeEach(func() {\n\t\tbuf = new(bytes.Buffer)\n\t\twr = proto.NewWriter(buf)\n\t})\n\n\tIt(\"should write args\", func() {\n\t\terr := wr.WriteArgs([]interface{}{\n\t\t\t\"string\",\n\t\t\t12,\n\t\t\t34.56,\n\t\t\t[]byte{'b', 'y', 't', 'e', 's'},\n\t\t\ttrue,\n\t\t\tnil,\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(buf.Bytes()).To(Equal([]byte(\"*6\\r\\n\" +\n\t\t\t\"$6\\r\\nstring\\r\\n\" +\n\t\t\t\"$2\\r\\n12\\r\\n\" +\n\t\t\t\"$5\\r\\n34.56\\r\\n\" +\n\t\t\t\"$5\\r\\nbytes\\r\\n\" +\n\t\t\t\"$1\\r\\n1\\r\\n\" +\n\t\t\t\"$0\\r\\n\" +\n\t\t\t\"\\r\\n\")))\n\t})\n\n\tIt(\"should append time\", func() {\n\t\ttm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC)\n\t\terr := wr.WriteArgs([]interface{}{tm})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(buf.Len()).To(Equal(41))\n\t})\n\n\tIt(\"should append marshalable args\", func() {\n\t\terr := wr.WriteArgs([]interface{}{&MyType{}})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(buf.Len()).To(Equal(15))\n\t})\n\n\tIt(\"should append net.IP\", func() {\n\t\tip := net.ParseIP(\"192.168.1.1\")\n\t\terr := wr.WriteArgs([]interface{}{ip})\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(buf.String()).To(Equal(fmt.Sprintf(\"*1\\r\\n$16\\r\\n%s\\r\\n\", bytes.NewBuffer(ip))))\n\t})\n})\n\ntype discard struct{}\n\nfunc (discard) Write(b []byte) (int, error) {\n\treturn len(b), nil\n}\n\nfunc (discard) WriteString(s string) (int, error) {\n\treturn len(s), nil\n}\n\nfunc (discard) WriteByte(c byte) error {\n\treturn nil\n}\n\nfunc BenchmarkWriteBuffer_Append(b *testing.B) {\n\tbuf := proto.NewWriter(discard{})\n\targs := []interface{}{\"hello\", \"world\", \"foo\", \"bar\"}\n\n\tfor i := 0; i < b.N; i++ {\n\t\terr := buf.WriteArgs(args)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nvar _ = Describe(\"WriteArg\", func() {\n\tvar buf *bytes.Buffer\n\tvar wr *proto.Writer\n\n\tBeforeEach(func() {\n\t\tbuf = new(bytes.Buffer)\n\t\twr = proto.NewWriter(buf)\n\t})\n\n\tt := time.Date(2025, 2, 8, 00, 00, 00, 0, time.UTC)\n\n\targs := map[any]string{\n\t\t\"hello\":                                \"$5\\r\\nhello\\r\\n\",\n\t\tutil.ToPtr(\"hello\"):                    \"$5\\r\\nhello\\r\\n\",\n\t\t(*string)(nil):                         \"$0\\r\\n\\r\\n\",\n\t\tint(10):                                \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(int(10)):                    \"$2\\r\\n10\\r\\n\",\n\t\t(*int)(nil):                            \"$1\\r\\n0\\r\\n\",\n\t\tint8(10):                               \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(int8(10)):                   \"$2\\r\\n10\\r\\n\",\n\t\t(*int8)(nil):                           \"$1\\r\\n0\\r\\n\",\n\t\tint16(10):                              \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(int16(10)):                  \"$2\\r\\n10\\r\\n\",\n\t\t(*int16)(nil):                          \"$1\\r\\n0\\r\\n\",\n\t\tint32(10):                              \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(int32(10)):                  \"$2\\r\\n10\\r\\n\",\n\t\t(*int32)(nil):                          \"$1\\r\\n0\\r\\n\",\n\t\tint64(10):                              \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(int64(10)):                  \"$2\\r\\n10\\r\\n\",\n\t\t(*int64)(nil):                          \"$1\\r\\n0\\r\\n\",\n\t\tuint(10):                               \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(uint(10)):                   \"$2\\r\\n10\\r\\n\",\n\t\t(*uint)(nil):                           \"$1\\r\\n0\\r\\n\",\n\t\tuint8(10):                              \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(uint8(10)):                  \"$2\\r\\n10\\r\\n\",\n\t\t(*uint8)(nil):                          \"$0\\r\\n\\r\\n\",\n\t\tuint16(10):                             \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(uint16(10)):                 \"$2\\r\\n10\\r\\n\",\n\t\t(*uint16)(nil):                         \"$1\\r\\n0\\r\\n\",\n\t\tuint32(10):                             \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(uint32(10)):                 \"$2\\r\\n10\\r\\n\",\n\t\t(*uint32)(nil):                         \"$1\\r\\n0\\r\\n\",\n\t\tuint64(10):                             \"$2\\r\\n10\\r\\n\",\n\t\tutil.ToPtr(uint64(10)):                 \"$2\\r\\n10\\r\\n\",\n\t\t(*uint64)(nil):                         \"$1\\r\\n0\\r\\n\",\n\t\tfloat32(10.3):                          \"$18\\r\\n10.300000190734863\\r\\n\",\n\t\tutil.ToPtr(float32(10.3)):              \"$18\\r\\n10.300000190734863\\r\\n\",\n\t\t(*float32)(nil):                        \"$1\\r\\n0\\r\\n\",\n\t\tfloat64(10.3):                          \"$4\\r\\n10.3\\r\\n\",\n\t\tutil.ToPtr(float64(10.3)):              \"$4\\r\\n10.3\\r\\n\",\n\t\t(*float64)(nil):                        \"$1\\r\\n0\\r\\n\",\n\t\tbool(true):                             \"$1\\r\\n1\\r\\n\",\n\t\tbool(false):                            \"$1\\r\\n0\\r\\n\",\n\t\tutil.ToPtr(bool(true)):                 \"$1\\r\\n1\\r\\n\",\n\t\tutil.ToPtr(bool(false)):                \"$1\\r\\n0\\r\\n\",\n\t\t(*bool)(nil):                           \"$1\\r\\n0\\r\\n\",\n\t\ttime.Time(t):                           \"$20\\r\\n2025-02-08T00:00:00Z\\r\\n\",\n\t\tutil.ToPtr(time.Time(t)):               \"$20\\r\\n2025-02-08T00:00:00Z\\r\\n\",\n\t\t(*time.Time)(nil):                      \"$20\\r\\n0001-01-01T00:00:00Z\\r\\n\",\n\t\ttime.Duration(time.Second):             \"$10\\r\\n1000000000\\r\\n\",\n\t\tutil.ToPtr(time.Duration(time.Second)): \"$10\\r\\n1000000000\\r\\n\",\n\t\t(*time.Duration)(nil):                  \"$1\\r\\n0\\r\\n\",\n\t\t(encoding.BinaryMarshaler)(&MyType{}):  \"$5\\r\\nhello\\r\\n\",\n\t\t(encoding.BinaryMarshaler)(nil):        \"$0\\r\\n\\r\\n\",\n\t}\n\n\tfor arg, expect := range args {\n\t\targ, expect := arg, expect\n\t\tIt(fmt.Sprintf(\"should write arg of type %T\", arg), func() {\n\t\t\terr := wr.WriteArg(arg)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(buf.String()).To(Equal(expect))\n\t\t})\n\t}\n})\n"
  },
  {
    "path": "internal/rand/rand.go",
    "content": "package rand\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n)\n\n// Int returns a non-negative pseudo-random int.\nfunc Int() int { return pseudo.Int() }\n\n// Intn returns, as an int, a non-negative pseudo-random number in [0,n).\n// It panics if n <= 0.\nfunc Intn(n int) int { return pseudo.Intn(n) }\n\n// Int63n returns, as an int64, a non-negative pseudo-random number in [0,n).\n// It panics if n <= 0.\nfunc Int63n(n int64) int64 { return pseudo.Int63n(n) }\n\n// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n).\nfunc Perm(n int) []int { return pseudo.Perm(n) }\n\n// Seed uses the provided seed value to initialize the default Source to a\n// deterministic state. If Seed is not called, the generator behaves as if\n// seeded by Seed(1).\nfunc Seed(n int64) { pseudo.Seed(n) }\n\nvar pseudo = rand.New(&source{src: rand.NewSource(1)})\n\ntype source struct {\n\tsrc rand.Source\n\tmu  sync.Mutex\n}\n\nfunc (s *source) Int63() int64 {\n\ts.mu.Lock()\n\tn := s.src.Int63()\n\ts.mu.Unlock()\n\treturn n\n}\n\nfunc (s *source) Seed(seed int64) {\n\ts.mu.Lock()\n\ts.src.Seed(seed)\n\ts.mu.Unlock()\n}\n\n// Shuffle pseudo-randomizes the order of elements.\n// n is the number of elements.\n// swap swaps the elements with indexes i and j.\nfunc Shuffle(n int, swap func(i, j int)) { pseudo.Shuffle(n, swap) }\n"
  },
  {
    "path": "internal/redis.go",
    "content": "package internal\n\nconst RedisNull = \"<nil>\"\n"
  },
  {
    "path": "internal/routing/aggregator.go",
    "content": "package routing\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\n\t\"sync/atomic\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n\tuberAtomic \"go.uber.org/atomic\"\n)\n\nvar (\n\tErrMaxAggregation = errors.New(\"redis: no valid results to aggregate for max operation\")\n\tErrMinAggregation = errors.New(\"redis: no valid results to aggregate for min operation\")\n\tErrAndAggregation = errors.New(\"redis: no valid results to aggregate for logical AND operation\")\n\tErrOrAggregation  = errors.New(\"redis: no valid results to aggregate for logical OR operation\")\n)\n\n// ResponseAggregator defines the interface for aggregating responses from multiple shards.\ntype ResponseAggregator interface {\n\t// Add processes a single shard response.\n\tAdd(result interface{}, err error) error\n\n\t// AddWithKey processes a single shard response for a specific key (used by keyed aggregators).\n\tAddWithKey(key string, result interface{}, err error) error\n\n\tBatchAdd(map[string]AggregatorResErr) error\n\n\tBatchSlice([]AggregatorResErr) error\n\n\t// Result returns the final aggregated result and any error.\n\tResult() (interface{}, error)\n}\n\ntype AggregatorResErr struct {\n\tResult interface{}\n\tErr    error\n}\n\n// NewResponseAggregator creates an aggregator based on the response policy.\nfunc NewResponseAggregator(policy ResponsePolicy, cmdName string) ResponseAggregator {\n\tswitch policy {\n\tcase RespDefaultKeyless:\n\t\treturn &DefaultKeylessAggregator{results: make([]interface{}, 0)}\n\tcase RespDefaultHashSlot:\n\t\treturn &DefaultKeyedAggregator{results: make(map[string]interface{})}\n\tcase RespAllSucceeded:\n\t\treturn &AllSucceededAggregator{}\n\tcase RespOneSucceeded:\n\t\treturn &OneSucceededAggregator{}\n\tcase RespAggSum:\n\t\treturn &AggSumAggregator{\n\t\t\t// res:\n\t\t}\n\tcase RespAggMin:\n\t\treturn &AggMinAggregator{\n\t\t\tres: util.NewAtomicMin(),\n\t\t}\n\tcase RespAggMax:\n\t\treturn &AggMaxAggregator{\n\t\t\tres: util.NewAtomicMax(),\n\t\t}\n\tcase RespAggLogicalAnd:\n\t\tandAgg := &AggLogicalAndAggregator{}\n\t\tandAgg.res.Store(true)\n\n\t\treturn andAgg\n\tcase RespAggLogicalOr:\n\t\treturn &AggLogicalOrAggregator{}\n\tcase RespSpecial:\n\t\treturn NewSpecialAggregator(cmdName)\n\tdefault:\n\t\treturn &AllSucceededAggregator{}\n\t}\n}\n\nfunc NewDefaultAggregator(isKeyed bool) ResponseAggregator {\n\tif isKeyed {\n\t\treturn &DefaultKeyedAggregator{\n\t\t\tresults: make(map[string]interface{}),\n\t\t}\n\t}\n\treturn &DefaultKeylessAggregator{}\n}\n\n// AllSucceededAggregator returns one non-error reply if every shard succeeded,\n// propagates the first error otherwise.\ntype AllSucceededAggregator struct {\n\terr atomic.Value\n\tres atomic.Value\n}\n\nfunc (a *AllSucceededAggregator) Add(result interface{}, err error) error {\n\tif err != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\tif result != nil {\n\t\ta.res.CompareAndSwap(nil, result)\n\t}\n\n\treturn nil\n}\n\nfunc (a *AllSucceededAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\tfor _, res := range results {\n\t\terr := a.Add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *AllSucceededAggregator) BatchSlice(results []AggregatorResErr) error {\n\tfor _, res := range results {\n\t\terr := a.Add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *AllSucceededAggregator) Result() (interface{}, error) {\n\tvar err error\n\tres, e := a.res.Load(), a.err.Load()\n\tif e != nil {\n\t\terr = e.(error)\n\t}\n\n\treturn res, err\n}\n\nfunc (a *AllSucceededAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\n// OneSucceededAggregator returns the first non-error reply,\n// if all shards errored, returns any one of those errors.\ntype OneSucceededAggregator struct {\n\terr atomic.Value\n\tres atomic.Value\n}\n\nfunc (a *OneSucceededAggregator) Add(result interface{}, err error) error {\n\tif err != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\tif result != nil {\n\t\ta.res.CompareAndSwap(nil, result)\n\t}\n\n\treturn nil\n}\n\nfunc (a *OneSucceededAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\tfor _, res := range results {\n\t\terr := a.Add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *OneSucceededAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *OneSucceededAggregator) BatchSlice(results []AggregatorResErr) error {\n\tfor _, res := range results {\n\t\terr := a.Add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *OneSucceededAggregator) Result() (interface{}, error) {\n\tres, e := a.res.Load(), a.err.Load()\n\tif res == nil {\n\t\treturn nil, e.(error)\n\t}\n\n\treturn res, nil\n}\n\n// AggSumAggregator sums numeric replies from all shards.\ntype AggSumAggregator struct {\n\terr atomic.Value\n\tres uberAtomic.Float64\n}\n\nfunc (a *AggSumAggregator) Add(result interface{}, err error) error {\n\tif err != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t}\n\n\tif result != nil {\n\t\tval, err := toFloat64(result)\n\t\tif err != nil {\n\t\t\ta.err.CompareAndSwap(nil, err)\n\t\t\treturn err\n\t\t}\n\t\ta.res.Add(val)\n\t}\n\n\treturn nil\n}\n\nfunc (a *AggSumAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\tvar sum int64\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\treturn a.Add(res.Result, res.Err)\n\t\t}\n\n\t\tintRes, err := toInt64(res.Result)\n\t\tif err != nil {\n\t\t\treturn a.Add(nil, err)\n\t\t}\n\n\t\tsum += intRes\n\t}\n\n\treturn a.Add(sum, nil)\n}\n\nfunc (a *AggSumAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *AggSumAggregator) BatchSlice(results []AggregatorResErr) error {\n\tvar sum int64\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\treturn a.Add(res.Result, res.Err)\n\t\t}\n\n\t\tintRes, err := toInt64(res.Result)\n\t\tif err != nil {\n\t\t\treturn a.Add(nil, err)\n\t\t}\n\n\t\tsum += intRes\n\t}\n\n\treturn a.Add(sum, nil)\n}\n\nfunc (a *AggSumAggregator) Result() (interface{}, error) {\n\tres, err := a.res.Load(), a.err.Load()\n\tif err != nil {\n\t\treturn nil, err.(error)\n\t}\n\n\treturn res, nil\n}\n\n// AggMinAggregator returns the minimum numeric value from all shards.\ntype AggMinAggregator struct {\n\terr atomic.Value\n\tres *util.AtomicMin\n}\n\nfunc (a *AggMinAggregator) Add(result interface{}, err error) error {\n\tif err != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\tfloatVal, e := toFloat64(result)\n\tif e != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\ta.res.Value(floatVal)\n\n\treturn nil\n}\n\nfunc (a *AggMinAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\tmin := int64(math.MaxInt64)\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tresInt, err := toInt64(res.Result)\n\t\tif err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tif resInt < min {\n\t\t\tmin = resInt\n\t\t}\n\n\t}\n\n\treturn a.Add(min, nil)\n}\n\nfunc (a *AggMinAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *AggMinAggregator) BatchSlice(results []AggregatorResErr) error {\n\tmin := float64(math.MaxFloat64)\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tfloatVal, err := toFloat64(res.Result)\n\t\tif err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tif floatVal < min {\n\t\t\tmin = floatVal\n\t\t}\n\n\t}\n\n\treturn a.Add(min, nil)\n}\n\nfunc (a *AggMinAggregator) Result() (interface{}, error) {\n\terr := a.err.Load()\n\tif err != nil {\n\t\treturn nil, err.(error)\n\t}\n\n\tval, hasVal := a.res.Min()\n\tif !hasVal {\n\t\treturn nil, ErrMinAggregation\n\t}\n\treturn val, nil\n}\n\n// AggMaxAggregator returns the maximum numeric value from all shards.\ntype AggMaxAggregator struct {\n\terr atomic.Value\n\tres *util.AtomicMax\n}\n\nfunc (a *AggMaxAggregator) Add(result interface{}, err error) error {\n\tif err != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\tfloatVal, e := toFloat64(result)\n\tif e != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\ta.res.Value(floatVal)\n\n\treturn nil\n}\n\nfunc (a *AggMaxAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\tmax := int64(math.MinInt64)\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tresInt, err := toInt64(res.Result)\n\t\tif err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tif resInt > max {\n\t\t\tmax = resInt\n\t\t}\n\n\t}\n\n\treturn a.Add(max, nil)\n}\n\nfunc (a *AggMaxAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *AggMaxAggregator) BatchSlice(results []AggregatorResErr) error {\n\tmax := int64(math.MinInt64)\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tresInt, err := toInt64(res.Result)\n\t\tif err != nil {\n\t\t\t_ = a.Add(nil, res.Err)\n\t\t\treturn nil\n\t\t}\n\n\t\tif resInt > max {\n\t\t\tmax = resInt\n\t\t}\n\n\t}\n\n\treturn a.Add(max, nil)\n}\n\nfunc (a *AggMaxAggregator) Result() (interface{}, error) {\n\terr := a.err.Load()\n\tif err != nil {\n\t\treturn nil, err.(error)\n\t}\n\n\tval, hasVal := a.res.Max()\n\tif !hasVal {\n\t\treturn nil, ErrMaxAggregation\n\t}\n\treturn val, nil\n}\n\n// AggLogicalAndAggregator performs logical AND on boolean values.\ntype AggLogicalAndAggregator struct {\n\terr       atomic.Value\n\tres       atomic.Bool\n\thasResult atomic.Bool\n}\n\nfunc (a *AggLogicalAndAggregator) Add(result interface{}, err error) error {\n\tif err != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\tval, e := toBool(result)\n\tif e != nil {\n\t\ta.err.CompareAndSwap(nil, e)\n\t\treturn e\n\t}\n\n\t// Atomic AND operation: if val is false, result is always false\n\tif !val {\n\t\ta.res.Store(false)\n\t}\n\n\ta.hasResult.Store(true)\n\n\treturn nil\n}\n\nfunc (a *AggLogicalAndAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\tresult := true\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\treturn a.Add(nil, res.Err)\n\t\t}\n\n\t\tboolRes, err := toBool(res.Result)\n\t\tif err != nil {\n\t\t\treturn a.Add(nil, err)\n\t\t}\n\n\t\tresult = result && boolRes\n\t}\n\n\treturn a.Add(result, nil)\n}\n\nfunc (a *AggLogicalAndAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *AggLogicalAndAggregator) BatchSlice(results []AggregatorResErr) error {\n\tresult := true\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\treturn a.Add(nil, res.Err)\n\t\t}\n\n\t\tboolRes, err := toBool(res.Result)\n\t\tif err != nil {\n\t\t\treturn a.Add(nil, err)\n\t\t}\n\n\t\tresult = result && boolRes\n\t}\n\n\treturn a.Add(result, nil)\n}\n\nfunc (a *AggLogicalAndAggregator) Result() (interface{}, error) {\n\terr := a.err.Load()\n\tif err != nil {\n\t\treturn nil, err.(error)\n\t}\n\n\tif !a.hasResult.Load() {\n\t\treturn nil, ErrAndAggregation\n\t}\n\treturn a.res.Load(), nil\n}\n\n// AggLogicalOrAggregator performs logical OR on boolean values.\ntype AggLogicalOrAggregator struct {\n\terr       atomic.Value\n\tres       atomic.Bool\n\thasResult atomic.Bool\n}\n\nfunc (a *AggLogicalOrAggregator) Add(result interface{}, err error) error {\n\tif err != nil {\n\t\ta.err.CompareAndSwap(nil, err)\n\t\treturn nil\n\t}\n\n\tval, e := toBool(result)\n\tif e != nil {\n\t\ta.err.CompareAndSwap(nil, e)\n\t\treturn e\n\t}\n\n\t// Atomic OR operation: if val is true, result is always true\n\tif val {\n\t\ta.res.Store(true)\n\t}\n\n\ta.hasResult.Store(true)\n\n\treturn nil\n}\n\nfunc (a *AggLogicalOrAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\tresult := false\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\treturn a.Add(nil, res.Err)\n\t\t}\n\n\t\tboolRes, err := toBool(res.Result)\n\t\tif err != nil {\n\t\t\treturn a.Add(nil, err)\n\t\t}\n\n\t\tresult = result || boolRes\n\t}\n\n\treturn a.Add(result, nil)\n}\n\nfunc (a *AggLogicalOrAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *AggLogicalOrAggregator) BatchSlice(results []AggregatorResErr) error {\n\tresult := false\n\n\tfor _, res := range results {\n\t\tif res.Err != nil {\n\t\t\treturn a.Add(nil, res.Err)\n\t\t}\n\n\t\tboolRes, err := toBool(res.Result)\n\t\tif err != nil {\n\t\t\treturn a.Add(nil, err)\n\t\t}\n\n\t\tresult = result || boolRes\n\t}\n\n\treturn a.Add(result, nil)\n}\n\nfunc (a *AggLogicalOrAggregator) Result() (interface{}, error) {\n\terr := a.err.Load()\n\tif err != nil {\n\t\treturn nil, err.(error)\n\t}\n\n\tif !a.hasResult.Load() {\n\t\treturn nil, ErrOrAggregation\n\t}\n\treturn a.res.Load(), nil\n}\n\nfunc toInt64(val interface{}) (int64, error) {\n\tif val == nil {\n\t\treturn 0, nil\n\t}\n\tswitch v := val.(type) {\n\tcase int64:\n\t\treturn v, nil\n\tcase int:\n\t\treturn int64(v), nil\n\tcase int32:\n\t\treturn int64(v), nil\n\tcase float64:\n\t\tif v != math.Trunc(v) {\n\t\t\treturn 0, fmt.Errorf(\"cannot convert float %f to int64\", v)\n\t\t}\n\t\treturn int64(v), nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"cannot convert %T to int64\", val)\n\t}\n}\n\nfunc toFloat64(val interface{}) (float64, error) {\n\tif val == nil {\n\t\treturn 0, nil\n\t}\n\n\tswitch v := val.(type) {\n\tcase float64:\n\t\treturn v, nil\n\tcase int:\n\t\treturn float64(v), nil\n\tcase int32:\n\t\treturn float64(v), nil\n\tcase int64:\n\t\treturn float64(v), nil\n\tcase float32:\n\t\treturn float64(v), nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"cannot convert %T to float64\", val)\n\t}\n}\n\nfunc toBool(val interface{}) (bool, error) {\n\tif val == nil {\n\t\treturn false, nil\n\t}\n\tswitch v := val.(type) {\n\tcase bool:\n\t\treturn v, nil\n\tcase int64:\n\t\treturn v != 0, nil\n\tcase int:\n\t\treturn v != 0, nil\n\tdefault:\n\t\treturn false, fmt.Errorf(\"cannot convert %T to bool\", val)\n\t}\n}\n\n// DefaultKeylessAggregator collects all results in an array, order doesn't matter.\ntype DefaultKeylessAggregator struct {\n\tmu       sync.Mutex\n\tresults  []interface{}\n\tfirstErr error\n}\n\nfunc (a *DefaultKeylessAggregator) add(result interface{}, err error) error {\n\tif err != nil && a.firstErr == nil {\n\t\ta.firstErr = err\n\t\treturn nil\n\t}\n\tif err == nil {\n\t\ta.results = append(a.results, result)\n\t}\n\treturn nil\n}\n\nfunc (a *DefaultKeylessAggregator) Add(result interface{}, err error) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\treturn a.add(result, err)\n}\n\nfunc (a *DefaultKeylessAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tfor _, res := range results {\n\t\terr := a.add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *DefaultKeylessAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *DefaultKeylessAggregator) BatchSlice(results []AggregatorResErr) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tfor _, res := range results {\n\t\terr := a.add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *DefaultKeylessAggregator) Result() (interface{}, error) {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tif a.firstErr != nil {\n\t\treturn nil, a.firstErr\n\t}\n\treturn a.results, nil\n}\n\n// DefaultKeyedAggregator reassembles replies in the exact key order of the original request.\ntype DefaultKeyedAggregator struct {\n\tmu       sync.Mutex\n\tresults  map[string]interface{}\n\tkeyOrder []string\n\tfirstErr error\n}\n\nfunc NewDefaultKeyedAggregator(keyOrder []string) *DefaultKeyedAggregator {\n\treturn &DefaultKeyedAggregator{\n\t\tresults:  make(map[string]interface{}),\n\t\tkeyOrder: keyOrder,\n\t}\n}\n\nfunc (a *DefaultKeyedAggregator) add(result interface{}, err error) error {\n\tif err != nil && a.firstErr == nil {\n\t\ta.firstErr = err\n\t\treturn nil\n\t}\n\t// For non-keyed Add, just collect the result without ordering\n\tif err == nil {\n\t\ta.results[\"__default__\"] = result\n\t}\n\treturn nil\n}\n\nfunc (a *DefaultKeyedAggregator) Add(result interface{}, err error) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\treturn a.add(result, err)\n}\n\nfunc (a *DefaultKeyedAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tfor _, res := range results {\n\t\terr := a.add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *DefaultKeyedAggregator) addWithKey(key string, result interface{}, err error) error {\n\tif err != nil && a.firstErr == nil {\n\t\ta.firstErr = err\n\t\treturn nil\n\t}\n\tif err == nil {\n\t\ta.results[key] = result\n\t}\n\treturn nil\n}\n\nfunc (a *DefaultKeyedAggregator) AddWithKey(key string, result interface{}, err error) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\treturn a.addWithKey(key, result, err)\n}\n\nfunc (a *DefaultKeyedAggregator) BatchAddWithKeyOrder(results map[string]AggregatorResErr, keyOrder []string) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\ta.keyOrder = keyOrder\n\tfor key, res := range results {\n\t\terr := a.addWithKey(key, res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *DefaultKeyedAggregator) SetKeyOrder(keyOrder []string) {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\ta.keyOrder = keyOrder\n}\n\nfunc (a *DefaultKeyedAggregator) BatchSlice(results []AggregatorResErr) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tfor _, res := range results {\n\t\terr := a.add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *DefaultKeyedAggregator) Result() (interface{}, error) {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tif a.firstErr != nil {\n\t\treturn nil, a.firstErr\n\t}\n\n\t// If no explicit key order is set, return results in any order\n\tif len(a.keyOrder) == 0 {\n\t\torderedResults := make([]interface{}, 0, len(a.results))\n\t\tfor _, result := range a.results {\n\t\t\torderedResults = append(orderedResults, result)\n\t\t}\n\t\treturn orderedResults, nil\n\t}\n\n\t// Return results in the exact key order\n\torderedResults := make([]interface{}, len(a.keyOrder))\n\tfor i, key := range a.keyOrder {\n\t\tif result, exists := a.results[key]; exists {\n\t\t\torderedResults[i] = result\n\t\t}\n\t}\n\treturn orderedResults, nil\n}\n\n// SpecialAggregator provides a registry for command-specific aggregation logic.\ntype SpecialAggregator struct {\n\tmu             sync.Mutex\n\taggregatorFunc func([]interface{}, []error) (interface{}, error)\n\tresults        []interface{}\n\terrors         []error\n}\n\nfunc (a *SpecialAggregator) add(result interface{}, err error) error {\n\ta.results = append(a.results, result)\n\ta.errors = append(a.errors, err)\n\treturn nil\n}\n\nfunc (a *SpecialAggregator) Add(result interface{}, err error) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\treturn a.add(result, err)\n}\n\nfunc (a *SpecialAggregator) BatchAdd(results map[string]AggregatorResErr) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tfor _, res := range results {\n\t\terr := a.add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *SpecialAggregator) AddWithKey(key string, result interface{}, err error) error {\n\treturn a.Add(result, err)\n}\n\nfunc (a *SpecialAggregator) BatchSlice(results []AggregatorResErr) error {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tfor _, res := range results {\n\t\terr := a.add(res.Result, res.Err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *SpecialAggregator) Result() (interface{}, error) {\n\ta.mu.Lock()\n\tdefer a.mu.Unlock()\n\n\tif a.aggregatorFunc != nil {\n\t\treturn a.aggregatorFunc(a.results, a.errors)\n\t}\n\t// Default behavior: return first non-error result or first error\n\tfor i, err := range a.errors {\n\t\tif err == nil {\n\t\t\treturn a.results[i], nil\n\t\t}\n\t}\n\tif len(a.errors) > 0 {\n\t\treturn nil, a.errors[0]\n\t}\n\treturn nil, nil\n}\n\n// SpecialAggregatorRegistry holds custom aggregation functions for specific commands.\nvar SpecialAggregatorRegistry = make(map[string]func([]interface{}, []error) (interface{}, error))\n\n// RegisterSpecialAggregator registers a custom aggregation function for a command.\nfunc RegisterSpecialAggregator(cmdName string, fn func([]interface{}, []error) (interface{}, error)) {\n\tSpecialAggregatorRegistry[cmdName] = fn\n}\n\n// NewSpecialAggregator creates a special aggregator with command-specific logic if available.\nfunc NewSpecialAggregator(cmdName string) *SpecialAggregator {\n\tagg := &SpecialAggregator{}\n\tif fn, exists := SpecialAggregatorRegistry[cmdName]; exists {\n\t\tagg.aggregatorFunc = fn\n\t}\n\treturn agg\n}\n"
  },
  {
    "path": "internal/routing/aggregator_test.go",
    "content": "package routing\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestAggLogicalAndAggregator(t *testing.T) {\n\tt.Run(\"all true values\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalAnd, \"\")\n\n\t\terr := agg.Add(true, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(int64(1), nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(1, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != true {\n\t\t\tt.Errorf(\"expected true, got %v\", result)\n\t\t}\n\t})\n\n\tt.Run(\"one false value\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalAnd, \"\")\n\n\t\terr := agg.Add(true, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(false, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(true, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != false {\n\t\t\tt.Errorf(\"expected false, got %v\", result)\n\t\t}\n\t})\n\n\tt.Run(\"no results\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalAnd, \"\")\n\n\t\t_, err := agg.Result()\n\t\tif err != ErrAndAggregation {\n\t\t\tt.Errorf(\"expected ErrAndAggregation, got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"with error\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalAnd, \"\")\n\n\t\ttestErr := errors.New(\"test error\")\n\t\terr := agg.Add(nil, testErr)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\t_, err = agg.Result()\n\t\tif err != testErr {\n\t\t\tt.Errorf(\"expected test error, got %v\", err)\n\t\t}\n\t})\n}\n\nfunc TestAggLogicalOrAggregator(t *testing.T) {\n\tt.Run(\"all false values\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalOr, \"\")\n\n\t\terr := agg.Add(false, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(int64(0), nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(0, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != false {\n\t\t\tt.Errorf(\"expected false, got %v\", result)\n\t\t}\n\t})\n\n\tt.Run(\"one true value\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalOr, \"\")\n\n\t\terr := agg.Add(false, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(true, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\terr = agg.Add(false, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != true {\n\t\t\tt.Errorf(\"expected true, got %v\", result)\n\t\t}\n\t})\n\n\tt.Run(\"no results\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalOr, \"\")\n\n\t\t_, err := agg.Result()\n\t\tif err != ErrOrAggregation {\n\t\t\tt.Errorf(\"expected ErrOrAggregation, got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"with error\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalOr, \"\")\n\n\t\ttestErr := errors.New(\"test error\")\n\t\terr := agg.Add(nil, testErr)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\t_, err = agg.Result()\n\t\tif err != testErr {\n\t\t\tt.Errorf(\"expected test error, got %v\", err)\n\t\t}\n\t})\n}\n\nfunc TestAggLogicalAndBatchAdd(t *testing.T) {\n\tt.Run(\"batch add all true\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalAnd, \"\")\n\n\t\tresults := map[string]AggregatorResErr{\n\t\t\t\"key1\": {Result: true, Err: nil},\n\t\t\t\"key2\": {Result: int64(1), Err: nil},\n\t\t\t\"key3\": {Result: 1, Err: nil},\n\t\t}\n\n\t\terr := agg.BatchAdd(results)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != true {\n\t\t\tt.Errorf(\"expected true, got %v\", result)\n\t\t}\n\t})\n\n\tt.Run(\"batch add with false\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalAnd, \"\")\n\n\t\tresults := map[string]AggregatorResErr{\n\t\t\t\"key1\": {Result: true, Err: nil},\n\t\t\t\"key2\": {Result: false, Err: nil},\n\t\t\t\"key3\": {Result: true, Err: nil},\n\t\t}\n\n\t\terr := agg.BatchAdd(results)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != false {\n\t\t\tt.Errorf(\"expected false, got %v\", result)\n\t\t}\n\t})\n}\n\nfunc TestAggLogicalOrBatchAdd(t *testing.T) {\n\tt.Run(\"batch add all false\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalOr, \"\")\n\n\t\tresults := map[string]AggregatorResErr{\n\t\t\t\"key1\": {Result: false, Err: nil},\n\t\t\t\"key2\": {Result: int64(0), Err: nil},\n\t\t\t\"key3\": {Result: 0, Err: nil},\n\t\t}\n\n\t\terr := agg.BatchAdd(results)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != false {\n\t\t\tt.Errorf(\"expected false, got %v\", result)\n\t\t}\n\t})\n\n\tt.Run(\"batch add with true\", func(t *testing.T) {\n\t\tagg := NewResponseAggregator(RespAggLogicalOr, \"\")\n\n\t\tresults := map[string]AggregatorResErr{\n\t\t\t\"key1\": {Result: false, Err: nil},\n\t\t\t\"key2\": {Result: true, Err: nil},\n\t\t\t\"key3\": {Result: false, Err: nil},\n\t\t}\n\n\t\terr := agg.BatchAdd(results)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tresult, err := agg.Result()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif result != true {\n\t\t\tt.Errorf(\"expected true, got %v\", result)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/routing/policy.go",
    "content": "package routing\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype RequestPolicy uint8\n\nconst (\n\tReqDefault RequestPolicy = iota\n\n\tReqAllNodes\n\n\tReqAllShards\n\n\tReqMultiShard\n\n\tReqSpecial\n)\n\nconst (\n\tReadOnlyCMD string = \"readonly\"\n)\n\nfunc (p RequestPolicy) String() string {\n\tswitch p {\n\tcase ReqDefault:\n\t\treturn \"default\"\n\tcase ReqAllNodes:\n\t\treturn \"all_nodes\"\n\tcase ReqAllShards:\n\t\treturn \"all_shards\"\n\tcase ReqMultiShard:\n\t\treturn \"multi_shard\"\n\tcase ReqSpecial:\n\t\treturn \"special\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"unknown_request_policy(%d)\", p)\n\t}\n}\n\nfunc ParseRequestPolicy(raw string) (RequestPolicy, error) {\n\tswitch strings.ToLower(raw) {\n\tcase \"\", \"default\", \"none\":\n\t\treturn ReqDefault, nil\n\tcase \"all_nodes\":\n\t\treturn ReqAllNodes, nil\n\tcase \"all_shards\":\n\t\treturn ReqAllShards, nil\n\tcase \"multi_shard\":\n\t\treturn ReqMultiShard, nil\n\tcase \"special\":\n\t\treturn ReqSpecial, nil\n\tdefault:\n\t\treturn ReqDefault, fmt.Errorf(\"routing: unknown request_policy %q\", raw)\n\t}\n}\n\ntype ResponsePolicy uint8\n\nconst (\n\tRespDefaultKeyless ResponsePolicy = iota\n\tRespDefaultHashSlot\n\tRespAllSucceeded\n\tRespOneSucceeded\n\tRespAggSum\n\tRespAggMin\n\tRespAggMax\n\tRespAggLogicalAnd\n\tRespAggLogicalOr\n\tRespSpecial\n)\n\nfunc (p ResponsePolicy) String() string {\n\tswitch p {\n\tcase RespDefaultKeyless:\n\t\treturn \"default(keyless)\"\n\tcase RespDefaultHashSlot:\n\t\treturn \"default(hashslot)\"\n\tcase RespAllSucceeded:\n\t\treturn \"all_succeeded\"\n\tcase RespOneSucceeded:\n\t\treturn \"one_succeeded\"\n\tcase RespAggSum:\n\t\treturn \"agg_sum\"\n\tcase RespAggMin:\n\t\treturn \"agg_min\"\n\tcase RespAggMax:\n\t\treturn \"agg_max\"\n\tcase RespAggLogicalAnd:\n\t\treturn \"agg_logical_and\"\n\tcase RespAggLogicalOr:\n\t\treturn \"agg_logical_or\"\n\tcase RespSpecial:\n\t\treturn \"special\"\n\tdefault:\n\t\treturn \"all_succeeded\"\n\t}\n}\n\nfunc ParseResponsePolicy(raw string) (ResponsePolicy, error) {\n\tswitch strings.ToLower(raw) {\n\tcase \"default(keyless)\":\n\t\treturn RespDefaultKeyless, nil\n\tcase \"default(hashslot)\":\n\t\treturn RespDefaultHashSlot, nil\n\tcase \"all_succeeded\":\n\t\treturn RespAllSucceeded, nil\n\tcase \"one_succeeded\":\n\t\treturn RespOneSucceeded, nil\n\tcase \"agg_sum\":\n\t\treturn RespAggSum, nil\n\tcase \"agg_min\":\n\t\treturn RespAggMin, nil\n\tcase \"agg_max\":\n\t\treturn RespAggMax, nil\n\tcase \"agg_logical_and\":\n\t\treturn RespAggLogicalAnd, nil\n\tcase \"agg_logical_or\":\n\t\treturn RespAggLogicalOr, nil\n\tcase \"special\":\n\t\treturn RespSpecial, nil\n\tdefault:\n\t\treturn RespDefaultKeyless, fmt.Errorf(\"routing: unknown response_policy %q\", raw)\n\t}\n}\n\ntype CommandPolicy struct {\n\tRequest  RequestPolicy\n\tResponse ResponsePolicy\n\t// Tips that are not request_policy or response_policy\n\t// e.g nondeterministic_output, nondeterministic_output_order.\n\tTips map[string]string\n}\n\nfunc (p *CommandPolicy) CanBeUsedInPipeline() bool {\n\treturn p.Request != ReqAllNodes && p.Request != ReqAllShards && p.Request != ReqMultiShard\n}\n\nfunc (p *CommandPolicy) IsReadOnly() bool {\n\t_, readOnly := p.Tips[ReadOnlyCMD]\n\treturn readOnly\n}\n"
  },
  {
    "path": "internal/routing/shard_picker.go",
    "content": "package routing\n\nimport (\n\t\"math/rand\"\n\t\"sync/atomic\"\n)\n\n// ShardPicker chooses “one arbitrary shard” when the request_policy is\n// ReqDefault and the command has no keys.\ntype ShardPicker interface {\n\tNext(total int) int // returns an index in [0,total)\n}\n\n// StaticShardPicker always returns the same shard index.\ntype StaticShardPicker struct {\n\tindex int\n}\n\nfunc NewStaticShardPicker(index int) *StaticShardPicker {\n\treturn &StaticShardPicker{index: index}\n}\n\nfunc (p *StaticShardPicker) Next(total int) int {\n\tif total == 0 || p.index >= total {\n\t\treturn 0\n\t}\n\treturn p.index\n}\n\n/*───────────────────────────────\n   Round-robin (default)\n────────────────────────────────*/\n\ntype RoundRobinPicker struct {\n\tcnt atomic.Uint32\n}\n\nfunc (p *RoundRobinPicker) Next(total int) int {\n\tif total == 0 {\n\t\treturn 0\n\t}\n\ti := p.cnt.Add(1)\n\treturn int(i-1) % total\n}\n\n/*───────────────────────────────\n   Random\n────────────────────────────────*/\n\ntype RandomPicker struct{}\n\nfunc (RandomPicker) Next(total int) int {\n\tif total == 0 {\n\t\treturn 0\n\t}\n\treturn rand.Intn(total)\n}\n"
  },
  {
    "path": "internal/semaphore.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar semTimers = sync.Pool{\n\tNew: func() interface{} {\n\t\tt := time.NewTimer(time.Hour)\n\t\tt.Stop()\n\t\treturn t\n\t},\n}\n\n// FastSemaphore is a channel-based semaphore optimized for performance.\n// It uses a fast path that avoids timer allocation when tokens are available.\n// The channel is pre-filled with tokens: Acquire = receive, Release = send.\n// Closing the semaphore unblocks all waiting goroutines.\n//\n// Performance: ~30 ns/op with zero allocations on fast path.\n// Fairness: Eventual fairness (no starvation) but not strict FIFO.\ntype FastSemaphore struct {\n\ttokens chan struct{}\n\tmax    int32\n}\n\n// NewFastSemaphore creates a new fast semaphore with the given capacity.\nfunc NewFastSemaphore(capacity int32) *FastSemaphore {\n\tch := make(chan struct{}, capacity)\n\t// Pre-fill with tokens\n\tfor i := int32(0); i < capacity; i++ {\n\t\tch <- struct{}{}\n\t}\n\treturn &FastSemaphore{\n\t\ttokens: ch,\n\t\tmax:    capacity,\n\t}\n}\n\n// TryAcquire attempts to acquire a token without blocking.\n// Returns true if successful, false if no tokens available.\nfunc (s *FastSemaphore) TryAcquire() bool {\n\tselect {\n\tcase <-s.tokens:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Acquire acquires a token, blocking if necessary until one is available.\n// Returns an error if the context is cancelled or the timeout expires.\n// Uses a fast path to avoid timer allocation when tokens are immediately available.\nfunc (s *FastSemaphore) Acquire(ctx context.Context, timeout time.Duration, timeoutErr error) error {\n\t// Check context first\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tdefault:\n\t}\n\n\t// Try fast path first (no timer needed)\n\tselect {\n\tcase <-s.tokens:\n\t\treturn nil\n\tdefault:\n\t}\n\n\t// Slow path: need to wait with timeout\n\ttimer := semTimers.Get().(*time.Timer)\n\tdefer semTimers.Put(timer)\n\ttimer.Reset(timeout)\n\n\tselect {\n\tcase <-s.tokens:\n\t\tif !timer.Stop() {\n\t\t\t<-timer.C\n\t\t}\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\tif !timer.Stop() {\n\t\t\t<-timer.C\n\t\t}\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn timeoutErr\n\t}\n}\n\n// AcquireBlocking acquires a token, blocking indefinitely until one is available.\nfunc (s *FastSemaphore) AcquireBlocking() {\n\t<-s.tokens\n}\n\n// Release releases a token back to the semaphore.\nfunc (s *FastSemaphore) Release() {\n\ts.tokens <- struct{}{}\n}\n\n// Close closes the semaphore, unblocking all waiting goroutines.\n// After close, all Acquire calls will receive a closed channel signal.\nfunc (s *FastSemaphore) Close() {\n\tclose(s.tokens)\n}\n\n// Len returns the current number of acquired tokens.\nfunc (s *FastSemaphore) Len() int32 {\n\treturn s.max - int32(len(s.tokens))\n}\n\n// FIFOSemaphore is a channel-based semaphore with strict FIFO ordering.\n// Unlike FastSemaphore, this guarantees that threads are served in the exact order they call Acquire().\n// The channel is pre-filled with tokens: Acquire = receive, Release = send.\n// Closing the semaphore unblocks all waiting goroutines.\n//\n// Performance: ~115 ns/op with zero allocations (slower than FastSemaphore due to timer allocation).\n// Fairness: Strict FIFO ordering guaranteed by Go runtime.\ntype FIFOSemaphore struct {\n\ttokens chan struct{}\n\tmax    int32\n}\n\n// NewFIFOSemaphore creates a new FIFO semaphore with the given capacity.\nfunc NewFIFOSemaphore(capacity int32) *FIFOSemaphore {\n\tch := make(chan struct{}, capacity)\n\t// Pre-fill with tokens\n\tfor i := int32(0); i < capacity; i++ {\n\t\tch <- struct{}{}\n\t}\n\treturn &FIFOSemaphore{\n\t\ttokens: ch,\n\t\tmax:    capacity,\n\t}\n}\n\n// TryAcquire attempts to acquire a token without blocking.\n// Returns true if successful, false if no tokens available.\nfunc (s *FIFOSemaphore) TryAcquire() bool {\n\tselect {\n\tcase <-s.tokens:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Acquire acquires a token, blocking if necessary until one is available.\n// Returns an error if the context is cancelled or the timeout expires.\n// Always uses timer to guarantee FIFO ordering (no fast path).\nfunc (s *FIFOSemaphore) Acquire(ctx context.Context, timeout time.Duration, timeoutErr error) error {\n\t// No fast path - always use timer to guarantee FIFO\n\ttimer := semTimers.Get().(*time.Timer)\n\tdefer semTimers.Put(timer)\n\ttimer.Reset(timeout)\n\n\tselect {\n\tcase <-s.tokens:\n\t\tif !timer.Stop() {\n\t\t\t<-timer.C\n\t\t}\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\tif !timer.Stop() {\n\t\t\t<-timer.C\n\t\t}\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn timeoutErr\n\t}\n}\n\n// AcquireBlocking acquires a token, blocking indefinitely until one is available.\nfunc (s *FIFOSemaphore) AcquireBlocking() {\n\t<-s.tokens\n}\n\n// Release releases a token back to the semaphore.\nfunc (s *FIFOSemaphore) Release() {\n\ts.tokens <- struct{}{}\n}\n\n// Close closes the semaphore, unblocking all waiting goroutines.\n// After close, all Acquire calls will receive a closed channel signal.\nfunc (s *FIFOSemaphore) Close() {\n\tclose(s.tokens)\n}\n\n// Len returns the current number of acquired tokens.\nfunc (s *FIFOSemaphore) Len() int32 {\n\treturn s.max - int32(len(s.tokens))\n}\n"
  },
  {
    "path": "internal/util/atomic_max.go",
    "content": "/*\n© 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)\nISC License\n\nModified by htemelski-redis\nRemoved the treshold, adapted it to work with float64\n*/\n\npackage util\n\nimport (\n\t\"math\"\n\n\t\"go.uber.org/atomic\"\n)\n\n// AtomicMax is a thread-safe max container\n//   - hasValue indicator true if a value was equal to or greater than threshold\n//   - optional threshold for minimum accepted max value\n//   - if threshold is not used, initialization-free\n//   - —\n//   - wait-free CompareAndSwap mechanic\ntype AtomicMax struct {\n\n\t// value is current max\n\tvalue atomic.Float64\n\t// whether [AtomicMax.Value] has been invoked\n\t// with value equal or greater to threshold\n\thasValue atomic.Bool\n}\n\n// NewAtomicMax returns a thread-safe max container\n//   - if threshold is not used, AtomicMax is initialization-free\nfunc NewAtomicMax() (atomicMax *AtomicMax) {\n\tm := AtomicMax{}\n\tm.value.Store((-math.MaxFloat64))\n\treturn &m\n}\n\n// Value updates the container with a possible max value\n//   - isNewMax is true if:\n//   - — value is equal to or greater than any threshold and\n//   - — invocation recorded the first 0 or\n//   - — a new max\n//   - upon return, Max and Max1 are guaranteed to reflect the invocation\n//   - the return order of concurrent Value invocations is not guaranteed\n//   - Thread-safe\nfunc (m *AtomicMax) Value(value float64) (isNewMax bool) {\n\t//  -math.MaxFloat64 as max case\n\tvar hasValue0 = m.hasValue.Load()\n\tif value == (-math.MaxFloat64) {\n\t\tif !hasValue0 {\n\t\t\tisNewMax = m.hasValue.CompareAndSwap(false, true)\n\t\t}\n\t\treturn // -math.MaxFloat64 as max: isNewMax true for first 0 writer\n\t}\n\n\t// check against present value\n\tvar current = m.value.Load()\n\tif isNewMax = value > current; !isNewMax {\n\t\treturn // not a new max return: isNewMax false\n\t}\n\n\t// store the new max\n\tfor {\n\n\t\t// try to write value to *max\n\t\tif isNewMax = m.value.CompareAndSwap(current, value); isNewMax {\n\t\t\tif !hasValue0 {\n\t\t\t\t// may be rarely written multiple times\n\t\t\t\t// still faster than CompareAndSwap\n\t\t\t\tm.hasValue.Store(true)\n\t\t\t}\n\t\t\treturn // new max written return: isNewMax true\n\t\t}\n\t\tif current = m.value.Load(); current >= value {\n\t\t\treturn // no longer a need to write return: isNewMax false\n\t\t}\n\t}\n}\n\n// Max returns current max and value-present flag\n//   - hasValue true indicates that value reflects a Value invocation\n//   - hasValue false: value is zero-value\n//   - Thread-safe\nfunc (m *AtomicMax) Max() (value float64, hasValue bool) {\n\tif hasValue = m.hasValue.Load(); !hasValue {\n\t\treturn\n\t}\n\tvalue = m.value.Load()\n\treturn\n}\n\n// Max1 returns current maximum whether zero-value or set by Value\n//   - threshold is ignored\n//   - Thread-safe\nfunc (m *AtomicMax) Max1() (value float64) { return m.value.Load() }\n"
  },
  {
    "path": "internal/util/atomic_min.go",
    "content": "package util\n\n/*\n© 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)\nISC License\n\nModified by htemelski-redis\nAdapted from the modified atomic_max, but with inverted logic\n*/\n\nimport (\n\t\"math\"\n\n\t\"go.uber.org/atomic\"\n)\n\n// AtomicMin is a thread-safe Min container\n//   - hasValue indicator true if a value was equal to or greater than threshold\n//   - optional threshold for minimum accepted Min value\n//   - —\n//   - wait-free CompareAndSwap mechanic\ntype AtomicMin struct {\n\n\t// value is current Min\n\tvalue atomic.Float64\n\t// whether [AtomicMin.Value] has been invoked\n\t// with value equal or greater to threshold\n\thasValue atomic.Bool\n}\n\n// NewAtomicMin returns a thread-safe Min container\n//   - if threshold is not used, AtomicMin is initialization-free\nfunc NewAtomicMin() (atomicMin *AtomicMin) {\n\tm := AtomicMin{}\n\tm.value.Store(math.MaxFloat64)\n\treturn &m\n}\n\n// Value updates the container with a possible Min value\n//   - isNewMin is true if:\n//   - — value is equal to or greater than any threshold and\n//   - — invocation recorded the first 0 or\n//   - — a new Min\n//   - upon return, Min and Min1 are guaranteed to reflect the invocation\n//   - the return order of concurrent Value invocations is not guaranteed\n//   - Thread-safe\nfunc (m *AtomicMin) Value(value float64) (isNewMin bool) {\n\t//  math.MaxFloat64 as Min case\n\tvar hasValue0 = m.hasValue.Load()\n\tif value == math.MaxFloat64 {\n\t\tif !hasValue0 {\n\t\t\tisNewMin = m.hasValue.CompareAndSwap(false, true)\n\t\t}\n\t\treturn // math.MaxFloat64 as Min: isNewMin true for first 0 writer\n\t}\n\n\t// check against present value\n\tvar current = m.value.Load()\n\tif isNewMin = value < current; !isNewMin {\n\t\treturn // not a new Min return: isNewMin false\n\t}\n\n\t// store the new Min\n\tfor {\n\n\t\t// try to write value to *Min\n\t\tif isNewMin = m.value.CompareAndSwap(current, value); isNewMin {\n\t\t\tif !hasValue0 {\n\t\t\t\t// may be rarely written multiple times\n\t\t\t\t// still faster than CompareAndSwap\n\t\t\t\tm.hasValue.Store(true)\n\t\t\t}\n\t\t\treturn // new Min written return: isNewMin true\n\t\t}\n\t\tif current = m.value.Load(); current <= value {\n\t\t\treturn // no longer a need to write return: isNewMin false\n\t\t}\n\t}\n}\n\n// Min returns current min and value-present flag\n//   - hasValue true indicates that value reflects a Value invocation\n//   - hasValue false: value is zero-value\n//   - Thread-safe\nfunc (m *AtomicMin) Min() (value float64, hasValue bool) {\n\tif hasValue = m.hasValue.Load(); !hasValue {\n\t\treturn\n\t}\n\tvalue = m.value.Load()\n\treturn\n}\n\n// Min1 returns current Minimum whether zero-value or set by Value\n//   - threshold is ignored\n//   - Thread-safe\nfunc (m *AtomicMin) Min1() (value float64) { return m.value.Load() }\n"
  },
  {
    "path": "internal/util/convert.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n)\n\n// ParseFloat parses a Redis RESP3 float reply into a Go float64,\n// handling \"inf\", \"-inf\", \"nan\" per Redis conventions.\nfunc ParseStringToFloat(s string) (float64, error) {\n\tswitch s {\n\tcase \"inf\":\n\t\treturn math.Inf(1), nil\n\tcase \"-inf\":\n\t\treturn math.Inf(-1), nil\n\tcase \"nan\", \"-nan\":\n\t\treturn math.NaN(), nil\n\t}\n\treturn strconv.ParseFloat(s, 64)\n}\n\n// MustParseFloat is like ParseFloat but panics on parse errors.\nfunc MustParseFloat(s string) float64 {\n\tf, err := ParseStringToFloat(s)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"redis: failed to parse float %q: %v\", s, err))\n\t}\n\treturn f\n}\n\n// SafeIntToInt32 safely converts an int to int32, returning an error if overflow would occur.\nfunc SafeIntToInt32(value int, fieldName string) (int32, error) {\n\tif value > math.MaxInt32 {\n\t\treturn 0, fmt.Errorf(\"redis: %s value %d exceeds maximum allowed value %d\", fieldName, value, math.MaxInt32)\n\t}\n\tif value < math.MinInt32 {\n\t\treturn 0, fmt.Errorf(\"redis: %s value %d is below minimum allowed value %d\", fieldName, value, math.MinInt32)\n\t}\n\treturn int32(value), nil\n}\n"
  },
  {
    "path": "internal/util/convert_test.go",
    "content": "package util\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc TestParseStringToFloat(t *testing.T) {\n\ttests := []struct {\n\t\tin   string\n\t\twant float64\n\t\tok   bool\n\t}{\n\t\t{\"1.23\", 1.23, true},\n\t\t{\"inf\", math.Inf(1), true},\n\t\t{\"-inf\", math.Inf(-1), true},\n\t\t{\"nan\", math.NaN(), true},\n\t\t{\"oops\", 0, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tgot, err := ParseStringToFloat(tc.in)\n\t\tif tc.ok {\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"ParseFloat(%q) error: %v\", tc.in, err)\n\t\t\t}\n\t\t\tif math.IsNaN(tc.want) {\n\t\t\t\tif !math.IsNaN(got) {\n\t\t\t\t\tt.Errorf(\"ParseFloat(%q) = %v; want NaN\", tc.in, got)\n\t\t\t\t}\n\t\t\t} else if got != tc.want {\n\t\t\t\tt.Errorf(\"ParseFloat(%q) = %v; want %v\", tc.in, got, tc.want)\n\t\t\t}\n\t\t} else {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"ParseFloat(%q) expected error, got nil\", tc.in)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/util/safe.go",
    "content": "//go:build appengine\n\npackage util\n\nfunc BytesToString(b []byte) string {\n\treturn string(b)\n}\n\nfunc StringToBytes(s string) []byte {\n\treturn []byte(s)\n}\n"
  },
  {
    "path": "internal/util/strconv.go",
    "content": "package util\n\nimport \"strconv\"\n\nfunc Atoi(b []byte) (int, error) {\n\treturn strconv.Atoi(BytesToString(b))\n}\n\nfunc ParseInt(b []byte, base int, bitSize int) (int64, error) {\n\treturn strconv.ParseInt(BytesToString(b), base, bitSize)\n}\n\nfunc ParseUint(b []byte, base int, bitSize int) (uint64, error) {\n\treturn strconv.ParseUint(BytesToString(b), base, bitSize)\n}\n\nfunc ParseFloat(b []byte, bitSize int) (float64, error) {\n\treturn strconv.ParseFloat(BytesToString(b), bitSize)\n}\n"
  },
  {
    "path": "internal/util/strconv_test.go",
    "content": "package util\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc TestAtoi(t *testing.T) {\n\ttests := []struct {\n\t\tinput    []byte\n\t\texpected int\n\t\twantErr  bool\n\t}{\n\t\t{[]byte(\"123\"), 123, false},\n\t\t{[]byte(\"-456\"), -456, false},\n\t\t{[]byte(\"abc\"), 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult, err := Atoi(tt.input)\n\t\tif (err != nil) != tt.wantErr {\n\t\t\tt.Errorf(\"Atoi(%q) error = %v, wantErr %v\", tt.input, err, tt.wantErr)\n\t\t}\n\t\tif result != tt.expected && !tt.wantErr {\n\t\t\tt.Errorf(\"Atoi(%q) = %d, want %d\", tt.input, result, tt.expected)\n\t\t}\n\t}\n}\n\nfunc TestParseInt(t *testing.T) {\n\ttests := []struct {\n\t\tinput    []byte\n\t\tbase     int\n\t\tbitSize  int\n\t\texpected int64\n\t\twantErr  bool\n\t}{\n\t\t{[]byte(\"123\"), 10, 64, 123, false},\n\t\t{[]byte(\"-7F\"), 16, 64, -127, false},\n\t\t{[]byte(\"zzz\"), 36, 64, 46655, false},\n\t\t{[]byte(\"invalid\"), 10, 64, 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult, err := ParseInt(tt.input, tt.base, tt.bitSize)\n\t\tif (err != nil) != tt.wantErr {\n\t\t\tt.Errorf(\"ParseInt(%q, base=%d) error = %v, wantErr %v\", tt.input, tt.base, err, tt.wantErr)\n\t\t}\n\t\tif result != tt.expected && !tt.wantErr {\n\t\t\tt.Errorf(\"ParseInt(%q, base=%d) = %d, want %d\", tt.input, tt.base, result, tt.expected)\n\t\t}\n\t}\n}\n\nfunc TestParseUint(t *testing.T) {\n\ttests := []struct {\n\t\tinput    []byte\n\t\tbase     int\n\t\tbitSize  int\n\t\texpected uint64\n\t\twantErr  bool\n\t}{\n\t\t{[]byte(\"255\"), 10, 8, 255, false},\n\t\t{[]byte(\"FF\"), 16, 16, 255, false},\n\t\t{[]byte(\"-1\"), 10, 8, 0, true}, // negative should error for unsigned\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult, err := ParseUint(tt.input, tt.base, tt.bitSize)\n\t\tif (err != nil) != tt.wantErr {\n\t\t\tt.Errorf(\"ParseUint(%q, base=%d) error = %v, wantErr %v\", tt.input, tt.base, err, tt.wantErr)\n\t\t}\n\t\tif result != tt.expected && !tt.wantErr {\n\t\t\tt.Errorf(\"ParseUint(%q, base=%d) = %d, want %d\", tt.input, tt.base, result, tt.expected)\n\t\t}\n\t}\n}\n\nfunc TestParseFloat(t *testing.T) {\n\ttests := []struct {\n\t\tinput    []byte\n\t\tbitSize  int\n\t\texpected float64\n\t\twantErr  bool\n\t}{\n\t\t{[]byte(\"3.14\"), 64, 3.14, false},\n\t\t{[]byte(\"-2.71\"), 64, -2.71, false},\n\t\t{[]byte(\"NaN\"), 64, math.NaN(), false},\n\t\t{[]byte(\"invalid\"), 64, 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tresult, err := ParseFloat(tt.input, tt.bitSize)\n\t\tif (err != nil) != tt.wantErr {\n\t\t\tt.Errorf(\"ParseFloat(%q) error = %v, wantErr %v\", tt.input, err, tt.wantErr)\n\t\t}\n\t\tif !tt.wantErr && !(math.IsNaN(tt.expected) && math.IsNaN(result)) && result != tt.expected {\n\t\t\tt.Errorf(\"ParseFloat(%q) = %v, want %v\", tt.input, result, tt.expected)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/util/type.go",
    "content": "package util\n\nfunc ToPtr[T any](v T) *T {\n\treturn &v\n}\n"
  },
  {
    "path": "internal/util/unsafe.go",
    "content": "//go:build !appengine\n\npackage util\n\nimport (\n\t\"unsafe\"\n)\n\n// BytesToString converts byte slice to string.\nfunc BytesToString(b []byte) string {\n\treturn unsafe.String(unsafe.SliceData(b), len(b))\n}\n\n// StringToBytes converts string to byte slice.\nfunc StringToBytes(s string) []byte {\n\treturn unsafe.Slice(unsafe.StringData(s), len(s))\n}\n"
  },
  {
    "path": "internal/util/unsafe_test.go",
    "content": "package util\n\nimport (\n\t\"reflect\"\n\t\"runtime\"\n\t\"sync\"\n\t\"testing\"\n)\n\nvar (\n\t_tmpBytes  []byte\n\t_tmpString string\n)\n\nfunc TestBytesToString(t *testing.T) {\n\ttests := []struct {\n\t\tinput  string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tinput:  \"string\",\n\t\t\texpect: \"string\",\n\t\t},\n\t\t{\n\t\t\tinput:  \"\",\n\t\t\texpect: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tinput := []byte(tt.input)\n\t\t\tif result := BytesToString(input); !reflect.DeepEqual(tt.expect, result) {\n\t\t\t\tt.Errorf(\"BytesToString: Expected = %v, Got = %v\", tt.expect, result)\n\t\t\t}\n\n\t\t\tif len(tt.input) == 0 {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tinput[0] = 'x'\n\t\t\tif result := BytesToString(input); reflect.DeepEqual(tt.expect, result) {\n\t\t\t\tt.Errorf(\"BytesToString: expected not equal: %v\", tt.expect)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStringToBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput  string\n\t\texpect []byte\n\t}{\n\t\t{\n\t\t\tinput:  \"string\",\n\t\t\texpect: []byte(\"string\"),\n\t\t},\n\t\t{\n\t\t\tinput:  \"\",\n\t\t\texpect: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tif result := StringToBytes(tt.input); !reflect.DeepEqual(tt.expect, result) {\n\t\t\t\tt.Errorf(\"StringToBytes: Expected = %v, Got = %v\", tt.expect, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBytesToStringGC(t *testing.T) {\n\tvar (\n\t\texpect = t.Name()\n\t\tx      string\n\t\twg     sync.WaitGroup\n\t)\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\ttmp := append([]byte(nil), t.Name()...)\n\t\tx = BytesToString(tmp)\n\t}()\n\twg.Wait()\n\n\tfor i := 0; i < 100; i++ {\n\t\truntime.GC()\n\t}\n\n\tif !reflect.DeepEqual(expect, x) {\n\t\tt.Errorf(\"Expected = %v, Got = %v\", expect, x)\n\t}\n}\n\nfunc TestStringToBytesGC(t *testing.T) {\n\tvar (\n\t\texpect = []byte(t.Name())\n\t\tx      []byte\n\t\twg     sync.WaitGroup\n\t)\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\ttmp := append([]byte(nil), t.Name()...)\n\t\tx = StringToBytes(string(tmp))\n\t}()\n\twg.Wait()\n\n\tfor i := 0; i < 100; i++ {\n\t\truntime.GC()\n\t}\n\tif !reflect.DeepEqual(expect, x) {\n\t\tt.Errorf(\"Expected = %v, Got = %v\", expect, x)\n\t}\n}\n\nfunc BenchmarkStringToBytes(b *testing.B) {\n\tinput := b.Name()\n\n\tb.Run(\"copy\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_tmpBytes = []byte(input)\n\t\t}\n\t})\n\n\tb.Run(\"unsafe\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_tmpBytes = StringToBytes(input)\n\t\t}\n\t})\n}\n\nfunc BenchmarkBytesToString(b *testing.B) {\n\tinput := []byte(b.Name())\n\n\tb.Run(\"copy\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_tmpString = string(input)\n\t\t}\n\t})\n\n\tb.Run(\"unsafe\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_tmpString = BytesToString(input)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/util.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\nfunc Sleep(ctx context.Context, dur time.Duration) error {\n\tt := time.NewTimer(dur)\n\tdefer t.Stop()\n\n\tselect {\n\tcase <-t.C:\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}\n\nfunc ToLower(s string) string {\n\tif isLower(s) {\n\t\treturn s\n\t}\n\n\tb := make([]byte, len(s))\n\tfor i := range b {\n\t\tc := s[i]\n\t\tif c >= 'A' && c <= 'Z' {\n\t\t\tc += 'a' - 'A'\n\t\t}\n\t\tb[i] = c\n\t}\n\treturn util.BytesToString(b)\n}\n\nfunc isLower(s string) bool {\n\tfor i := 0; i < len(s); i++ {\n\t\tc := s[i]\n\t\tif c >= 'A' && c <= 'Z' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc ReplaceSpaces(s string) string {\n\treturn strings.ReplaceAll(s, \" \", \"-\")\n}\n\nfunc GetAddr(addr string) string {\n\tind := strings.LastIndexByte(addr, ':')\n\tif ind == -1 {\n\t\treturn \"\"\n\t}\n\n\tif strings.IndexByte(addr, '.') != -1 {\n\t\treturn addr\n\t}\n\n\tif addr[0] == '[' {\n\t\treturn addr\n\t}\n\treturn net.JoinHostPort(addr[:ind], addr[ind+1:])\n}\n\nfunc ToInteger(val interface{}) int {\n\tswitch v := val.(type) {\n\tcase int:\n\t\treturn v\n\tcase int64:\n\t\treturn int(v)\n\tcase string:\n\t\ti, _ := strconv.Atoi(v)\n\t\treturn i\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc ToFloat(val interface{}) float64 {\n\tswitch v := val.(type) {\n\tcase float64:\n\t\treturn v\n\tcase string:\n\t\tf, _ := strconv.ParseFloat(v, 64)\n\t\treturn f\n\tdefault:\n\t\treturn 0.0\n\t}\n}\n\nfunc ToString(val interface{}) string {\n\tif str, ok := val.(string); ok {\n\t\treturn str\n\t}\n\treturn \"\"\n}\n\nfunc ToStringSlice(val interface{}) []string {\n\tif arr, ok := val.([]interface{}); ok {\n\t\tresult := make([]string, len(arr))\n\t\tfor i, v := range arr {\n\t\t\tresult[i] = ToString(v)\n\t\t}\n\t\treturn result\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/util_test.go",
    "content": "package internal\n\nimport (\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nfunc BenchmarkToLowerStd(b *testing.B) {\n\tstr := \"AaBbCcDdEeFfGgHhIiJjKk\"\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = strings.ToLower(str)\n\t}\n}\n\n// util.ToLower is 3x faster than strings.ToLower.\nfunc BenchmarkToLowerInternal(b *testing.B) {\n\tstr := \"AaBbCcDdEeFfGgHhIiJjKk\"\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = ToLower(str)\n\t}\n}\n\nfunc TestToLower(t *testing.T) {\n\tIt(\"toLower\", func() {\n\t\tstr := \"AaBbCcDdEeFfGg\"\n\t\tExpect(ToLower(str)).To(Equal(strings.ToLower(str)))\n\n\t\tstr = \"ABCDE\"\n\t\tExpect(ToLower(str)).To(Equal(strings.ToLower(str)))\n\n\t\tstr = \"ABCDE\"\n\t\tExpect(ToLower(str)).To(Equal(strings.ToLower(str)))\n\n\t\tstr = \"abced\"\n\t\tExpect(ToLower(str)).To(Equal(strings.ToLower(str)))\n\t})\n}\n\nfunc TestIsLower(t *testing.T) {\n\tIt(\"isLower\", func() {\n\t\tstr := \"AaBbCcDdEeFfGg\"\n\t\tExpect(isLower(str)).To(BeFalse())\n\n\t\tstr = \"ABCDE\"\n\t\tExpect(isLower(str)).To(BeFalse())\n\n\t\tstr = \"abcdefg\"\n\t\tExpect(isLower(str)).To(BeTrue())\n\t})\n}\n\nfunc TestGetAddr(t *testing.T) {\n\tIt(\"getAddr\", func() {\n\t\tstr := \"127.0.0.1:1234\"\n\t\tExpect(GetAddr(str)).To(Equal(str))\n\n\t\tstr = \"[::1]:1234\"\n\t\tExpect(GetAddr(str)).To(Equal(str))\n\n\t\tstr = \"[fd01:abcd::7d03]:6379\"\n\t\tExpect(GetAddr(str)).To(Equal(str))\n\n\t\tExpect(GetAddr(\"::1:1234\")).To(Equal(\"[::1]:1234\"))\n\n\t\tExpect(GetAddr(\"fd01:abcd::7d03:6379\")).To(Equal(\"[fd01:abcd::7d03]:6379\"))\n\n\t\tExpect(GetAddr(\"127.0.0.1\")).To(Equal(\"\"))\n\n\t\tExpect(GetAddr(\"127\")).To(Equal(\"\"))\n\t})\n}\n\nfunc BenchmarkReplaceSpaces(b *testing.B) {\n\tversion := runtime.Version()\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = ReplaceSpaces(version)\n\t}\n}\n\nfunc ReplaceSpacesUseBuilder(s string) string {\n\t// Pre-allocate a builder with the same length as s to minimize allocations.\n\t// This is a basic optimization; adjust the initial size based on your use case.\n\tvar builder strings.Builder\n\tbuilder.Grow(len(s))\n\n\tfor _, char := range s {\n\t\tif char == ' ' {\n\t\t\t// Replace space with a hyphen.\n\t\t\tbuilder.WriteRune('-')\n\t\t} else {\n\t\t\t// Copy the character as-is.\n\t\t\tbuilder.WriteRune(char)\n\t\t}\n\t}\n\n\treturn builder.String()\n}\n\nfunc BenchmarkReplaceSpacesUseBuilder(b *testing.B) {\n\tversion := runtime.Version()\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = ReplaceSpacesUseBuilder(version)\n\t}\n}\n"
  },
  {
    "path": "internal_maint_notif_test.go",
    "content": "package redis\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// TestInitConnNilMaintNotificationsConfig is a regression test for\n// https://github.com/redis/go-redis/issues/3675\n//\n// initConn previously accessed MaintNotificationsConfig.EndpointType\n// unconditionally, even though the preceding line correctly nil-checked\n// MaintNotificationsConfig. This caused a nil pointer dereference panic\n// and left optLock.RLock held, leading to a subsequent deadlock.\nfunc TestInitConnNilMaintNotificationsConfig(t *testing.T) {\n\t// Start a minimal TCP server that speaks enough RESP to let\n\t// initConn get past the HELLO / AUTH / pipeline phases and reach\n\t// the MaintNotificationsConfig code path.\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to listen: %v\", err)\n\t}\n\tdefer ln.Close()\n\n\t// mockRedis responds to every RESP command with a Redis-protocol\n\t// error. This lets initConn fall through HELLO (Redis errors are\n\t// not fatal when there is no password) and the empty pipeline\n\t// succeeds trivially.\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgo func(c net.Conn) {\n\t\t\t\tdefer c.Close()\n\t\t\t\tscanner := bufio.NewScanner(c)\n\t\t\t\tfor scanner.Scan() {\n\t\t\t\t\tline := scanner.Text()\n\t\t\t\t\tif strings.HasPrefix(line, \"*\") {\n\t\t\t\t\t\t_, _ = c.Write([]byte(\"-ERR unknown command\\r\\n\"))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}(conn)\n\t\t}\n\t}()\n\n\topt := &Options{\n\t\tAddr: ln.Addr().String(),\n\t}\n\topt.init()\n\n\t// Force MaintNotificationsConfig to nil after init() to reproduce\n\t// the scenario from issue #3675.\n\topt.MaintNotificationsConfig = nil\n\n\tc := &baseClient{\n\t\topt: opt,\n\t}\n\tc.initHooks(hooks{\n\t\tdial:       c.dial,\n\t\tprocess:    c.process,\n\t\tpipeline:   c.processPipeline,\n\t\ttxPipeline: c.processTxPipeline,\n\t})\n\n\t// Dial a real connection to the mock server.\n\tnetConn, err := net.DialTimeout(\"tcp\", ln.Addr().String(), 2*time.Second)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to dial mock server: %v\", err)\n\t}\n\tdefer netConn.Close()\n\n\tcn := pool.NewConn(netConn)\n\t// Put the connection into INITIALIZING state so initConn proceeds\n\t// with the full initialization logic.\n\tcn.GetStateMachine().Transition(pool.StateInitializing)\n\n\t// initConn must not panic. Any returned error is acceptable (the\n\t// mock server does not implement a full Redis protocol), but a\n\t// nil-pointer panic on MaintNotificationsConfig is the bug we\n\t// guard against.\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tt.Fatalf(\"initConn panicked with nil MaintNotificationsConfig: %v\", r)\n\t\t}\n\t}()\n\n\t_ = c.initConn(context.Background(), cn)\n}\n"
  },
  {
    "path": "internal_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n)\n\nvar ctx = context.TODO()\n\nvar _ = Describe(\"newClusterState\", func() {\n\tvar state *clusterState\n\n\tcreateClusterState := func(slots []ClusterSlot) *clusterState {\n\t\topt := &ClusterOptions{}\n\t\topt.init()\n\t\tnodes := newClusterNodes(opt)\n\t\tstate, err := newClusterState(nodes, slots, \"10.10.10.10:1234\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\treturn state\n\t}\n\n\tDescribe(\"sorting\", func() {\n\t\tBeforeEach(func() {\n\t\t\tstate = createClusterState([]ClusterSlot{{\n\t\t\t\tStart: 1000,\n\t\t\t\tEnd:   1999,\n\t\t\t}, {\n\t\t\t\tStart: 0,\n\t\t\t\tEnd:   999,\n\t\t\t}, {\n\t\t\t\tStart: 2000,\n\t\t\t\tEnd:   2999,\n\t\t\t}})\n\t\t})\n\n\t\tIt(\"sorts slots\", func() {\n\t\t\tExpect(state.slots).To(Equal([]*clusterSlot{\n\t\t\t\t{start: 0, end: 999, nodes: nil},\n\t\t\t\t{start: 1000, end: 1999, nodes: nil},\n\t\t\t\t{start: 2000, end: 2999, nodes: nil},\n\t\t\t}))\n\t\t})\n\t})\n\n\tDescribe(\"loopback\", func() {\n\t\tBeforeEach(func() {\n\t\t\tstate = createClusterState([]ClusterSlot{{\n\t\t\t\tNodes: []ClusterNode{{Addr: \"127.0.0.1:7001\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \"127.0.0.1:7002\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \"1.2.3.4:1234\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \":1234\"}},\n\t\t\t}})\n\t\t})\n\n\t\tIt(\"replaces loopback hosts in addresses\", func() {\n\t\t\tslotAddr := func(slot *clusterSlot) string {\n\t\t\t\treturn slot.nodes[0].Client.Options().Addr\n\t\t\t}\n\n\t\t\tExpect(slotAddr(state.slots[0])).To(Equal(\"10.10.10.10:7001\"))\n\t\t\tExpect(slotAddr(state.slots[1])).To(Equal(\"10.10.10.10:7002\"))\n\t\t\tExpect(slotAddr(state.slots[2])).To(Equal(\"1.2.3.4:1234\"))\n\t\t\tExpect(slotAddr(state.slots[3])).To(Equal(\":1234\"))\n\t\t})\n\t})\n\n\tDescribe(\"NodeAddress\", func() {\n\t\tBeforeEach(func() {\n\t\t\tstate = createClusterState([]ClusterSlot{{\n\t\t\t\tNodes: []ClusterNode{{Addr: \"127.0.0.1:7001\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \"127.0.0.1:7002\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \"1.2.3.4:1234\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \":1234\"}},\n\t\t\t}})\n\t\t})\n\n\t\tIt(\"preserves node address from ClusterSlots before loopback replacement\", func() {\n\t\t\tslotNodeAddress := func(slot *clusterSlot) string {\n\t\t\t\treturn slot.nodes[0].Client.NodeAddress()\n\t\t\t}\n\n\t\t\t// Node addresses should be preserved even when Addr is transformed\n\t\t\tExpect(slotNodeAddress(state.slots[0])).To(Equal(\"127.0.0.1:7001\"))\n\t\t\tExpect(slotNodeAddress(state.slots[1])).To(Equal(\"127.0.0.1:7002\"))\n\t\t\tExpect(slotNodeAddress(state.slots[2])).To(Equal(\"1.2.3.4:1234\"))\n\t\t\tExpect(slotNodeAddress(state.slots[3])).To(Equal(\":1234\"))\n\t\t})\n\n\t\tIt(\"has different Addr and NodeAddress when loopback is replaced\", func() {\n\t\t\t// For loopback addresses, Addr should be transformed but NodeAddress preserved\n\t\t\tslot0 := state.slots[0]\n\t\t\tExpect(slot0.nodes[0].Client.Options().Addr).To(Equal(\"10.10.10.10:7001\"))\n\t\t\tExpect(slot0.nodes[0].Client.NodeAddress()).To(Equal(\"127.0.0.1:7001\"))\n\n\t\t\t// For non-loopback addresses, Addr and NodeAddress should be the same\n\t\t\tslot2 := state.slots[2]\n\t\t\tExpect(slot2.nodes[0].Client.Options().Addr).To(Equal(\"1.2.3.4:1234\"))\n\t\t\tExpect(slot2.nodes[0].Client.NodeAddress()).To(Equal(\"1.2.3.4:1234\"))\n\t\t})\n\t})\n\n\tDescribe(\"NodeAddress with FQDN\", func() {\n\t\tBeforeEach(func() {\n\t\t\t// Simulate FQDN endpoints that might be resolved/transformed\n\t\t\tstate = createClusterState([]ClusterSlot{{\n\t\t\t\tNodes: []ClusterNode{{Addr: \"redis-master.example.com:6379\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \"redis-replica-1.example.com:6379\"}},\n\t\t\t}, {\n\t\t\t\tNodes: []ClusterNode{{Addr: \"redis-replica-2.example.com:6379\"}},\n\t\t\t}})\n\t\t})\n\n\t\tIt(\"preserves FQDN node addresses\", func() {\n\t\t\tslotNodeAddress := func(slot *clusterSlot) string {\n\t\t\t\treturn slot.nodes[0].Client.NodeAddress()\n\t\t\t}\n\n\t\t\tExpect(slotNodeAddress(state.slots[0])).To(Equal(\"redis-master.example.com:6379\"))\n\t\t\tExpect(slotNodeAddress(state.slots[1])).To(Equal(\"redis-replica-1.example.com:6379\"))\n\t\t\tExpect(slotNodeAddress(state.slots[2])).To(Equal(\"redis-replica-2.example.com:6379\"))\n\t\t})\n\t})\n\n\tDescribe(\"NodeAddress with multiple nodes per slot\", func() {\n\t\tBeforeEach(func() {\n\t\t\tstate = createClusterState([]ClusterSlot{{\n\t\t\t\tStart: 0,\n\t\t\t\tEnd:   5460,\n\t\t\t\tNodes: []ClusterNode{\n\t\t\t\t\t{Addr: \"127.0.0.1:7001\"}, // master\n\t\t\t\t\t{Addr: \"127.0.0.1:7004\"}, // replica\n\t\t\t\t},\n\t\t\t}, {\n\t\t\t\tStart: 5461,\n\t\t\t\tEnd:   10922,\n\t\t\t\tNodes: []ClusterNode{\n\t\t\t\t\t{Addr: \"master-2.redis.local:6379\"},  // master\n\t\t\t\t\t{Addr: \"replica-2.redis.local:6379\"}, // replica\n\t\t\t\t},\n\t\t\t}})\n\t\t})\n\n\t\tIt(\"preserves node addresses for all nodes in a slot\", func() {\n\t\t\t// First slot - loopback addresses\n\t\t\tslot0 := state.slots[0]\n\t\t\tExpect(slot0.nodes[0].Client.NodeAddress()).To(Equal(\"127.0.0.1:7001\"))\n\t\t\tExpect(slot0.nodes[1].Client.NodeAddress()).To(Equal(\"127.0.0.1:7004\"))\n\n\t\t\t// Verify Addr is transformed for loopback\n\t\t\tExpect(slot0.nodes[0].Client.Options().Addr).To(Equal(\"10.10.10.10:7001\"))\n\t\t\tExpect(slot0.nodes[1].Client.Options().Addr).To(Equal(\"10.10.10.10:7004\"))\n\n\t\t\t// Second slot - FQDN addresses (no transformation)\n\t\t\tslot1 := state.slots[1]\n\t\t\tExpect(slot1.nodes[0].Client.NodeAddress()).To(Equal(\"master-2.redis.local:6379\"))\n\t\t\tExpect(slot1.nodes[1].Client.NodeAddress()).To(Equal(\"replica-2.redis.local:6379\"))\n\n\t\t\t// Verify Addr is same as NodeAddress for non-loopback\n\t\t\tExpect(slot1.nodes[0].Client.Options().Addr).To(Equal(\"master-2.redis.local:6379\"))\n\t\t\tExpect(slot1.nodes[1].Client.Options().Addr).To(Equal(\"replica-2.redis.local:6379\"))\n\t\t})\n\t})\n})\n\n// TestNodeAddress tests that NodeAddress is correctly preserved\n// when cluster nodes are created from ClusterSlots responses.\nfunc TestNodeAddress(t *testing.T) {\n\tt.Run(\"preserves node address when loopback is replaced\", func(t *testing.T) {\n\t\topt := &ClusterOptions{}\n\t\topt.init()\n\t\tnodes := newClusterNodes(opt)\n\t\tdefer nodes.Close()\n\n\t\t// Create cluster state with loopback addresses\n\t\t// Origin is non-loopback, so loopback addresses will be replaced\n\t\tslots := []ClusterSlot{{\n\t\t\tStart: 0,\n\t\t\tEnd:   5460,\n\t\t\tNodes: []ClusterNode{{Addr: \"127.0.0.1:7001\"}},\n\t\t}, {\n\t\t\tStart: 5461,\n\t\t\tEnd:   10922,\n\t\t\tNodes: []ClusterNode{{Addr: \"127.0.0.1:7002\"}},\n\t\t}}\n\n\t\tstate, err := newClusterState(nodes, slots, \"10.10.10.10:1234\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"newClusterState failed: %v\", err)\n\t\t}\n\n\t\t// Verify Addr is transformed (loopback replaced with origin host)\n\t\tif got := state.slots[0].nodes[0].Client.Options().Addr; got != \"10.10.10.10:7001\" {\n\t\t\tt.Errorf(\"Addr = %q, want %q\", got, \"10.10.10.10:7001\")\n\t\t}\n\t\tif got := state.slots[1].nodes[0].Client.Options().Addr; got != \"10.10.10.10:7002\" {\n\t\t\tt.Errorf(\"Addr = %q, want %q\", got, \"10.10.10.10:7002\")\n\t\t}\n\n\t\t// Verify NodeAddress is preserved (original from ClusterSlots)\n\t\tif got := state.slots[0].nodes[0].Client.NodeAddress(); got != \"127.0.0.1:7001\" {\n\t\t\tt.Errorf(\"NodeAddress = %q, want %q\", got, \"127.0.0.1:7001\")\n\t\t}\n\t\tif got := state.slots[1].nodes[0].Client.NodeAddress(); got != \"127.0.0.1:7002\" {\n\t\t\tt.Errorf(\"NodeAddress = %q, want %q\", got, \"127.0.0.1:7002\")\n\t\t}\n\t})\n\n\tt.Run(\"preserves FQDN node addresses\", func(t *testing.T) {\n\t\topt := &ClusterOptions{}\n\t\topt.init()\n\t\tnodes := newClusterNodes(opt)\n\t\tdefer nodes.Close()\n\n\t\tslots := []ClusterSlot{{\n\t\t\tStart: 0,\n\t\t\tEnd:   5460,\n\t\t\tNodes: []ClusterNode{{Addr: \"redis-master.example.com:6379\"}},\n\t\t}, {\n\t\t\tStart: 5461,\n\t\t\tEnd:   10922,\n\t\t\tNodes: []ClusterNode{{Addr: \"redis-replica.example.com:6379\"}},\n\t\t}}\n\n\t\tstate, err := newClusterState(nodes, slots, \"10.10.10.10:1234\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"newClusterState failed: %v\", err)\n\t\t}\n\n\t\t// For non-loopback addresses, Addr and NodeAddress should be the same\n\t\tif got := state.slots[0].nodes[0].Client.Options().Addr; got != \"redis-master.example.com:6379\" {\n\t\t\tt.Errorf(\"Addr = %q, want %q\", got, \"redis-master.example.com:6379\")\n\t\t}\n\t\tif got := state.slots[0].nodes[0].Client.NodeAddress(); got != \"redis-master.example.com:6379\" {\n\t\t\tt.Errorf(\"NodeAddress = %q, want %q\", got, \"redis-master.example.com:6379\")\n\t\t}\n\n\t\tif got := state.slots[1].nodes[0].Client.Options().Addr; got != \"redis-replica.example.com:6379\" {\n\t\t\tt.Errorf(\"Addr = %q, want %q\", got, \"redis-replica.example.com:6379\")\n\t\t}\n\t\tif got := state.slots[1].nodes[0].Client.NodeAddress(); got != \"redis-replica.example.com:6379\" {\n\t\t\tt.Errorf(\"NodeAddress = %q, want %q\", got, \"redis-replica.example.com:6379\")\n\t\t}\n\t})\n\n\tt.Run(\"preserves node address for multiple nodes per slot\", func(t *testing.T) {\n\t\topt := &ClusterOptions{}\n\t\topt.init()\n\t\tnodes := newClusterNodes(opt)\n\t\tdefer nodes.Close()\n\n\t\tslots := []ClusterSlot{{\n\t\t\tStart: 0,\n\t\t\tEnd:   5460,\n\t\t\tNodes: []ClusterNode{\n\t\t\t\t{Addr: \"127.0.0.1:7001\"}, // master\n\t\t\t\t{Addr: \"127.0.0.1:7004\"}, // replica\n\t\t\t},\n\t\t}}\n\n\t\tstate, err := newClusterState(nodes, slots, \"10.10.10.10:1234\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"newClusterState failed: %v\", err)\n\t\t}\n\n\t\t// Master node\n\t\tif got := state.slots[0].nodes[0].Client.Options().Addr; got != \"10.10.10.10:7001\" {\n\t\t\tt.Errorf(\"Master Addr = %q, want %q\", got, \"10.10.10.10:7001\")\n\t\t}\n\t\tif got := state.slots[0].nodes[0].Client.NodeAddress(); got != \"127.0.0.1:7001\" {\n\t\t\tt.Errorf(\"Master NodeAddress = %q, want %q\", got, \"127.0.0.1:7001\")\n\t\t}\n\n\t\t// Replica node\n\t\tif got := state.slots[0].nodes[1].Client.Options().Addr; got != \"10.10.10.10:7004\" {\n\t\t\tt.Errorf(\"Replica Addr = %q, want %q\", got, \"10.10.10.10:7004\")\n\t\t}\n\t\tif got := state.slots[0].nodes[1].Client.NodeAddress(); got != \"127.0.0.1:7004\" {\n\t\t\tt.Errorf(\"Replica NodeAddress = %q, want %q\", got, \"127.0.0.1:7004\")\n\t\t}\n\t})\n\n\tt.Run(\"GetOrCreate without node address defaults to Addr\", func(t *testing.T) {\n\t\topt := &ClusterOptions{}\n\t\topt.init()\n\t\tnodes := newClusterNodes(opt)\n\t\tdefer nodes.Close()\n\n\t\t// GetOrCreate without node address (e.g., from MOVED/ASK error)\n\t\tnode, err := nodes.GetOrCreate(\"10.10.10.10:7001\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"GetOrCreate failed: %v\", err)\n\t\t}\n\n\t\t// NodeAddress should default to Addr when not provided\n\t\tif got := node.Client.NodeAddress(); got != \"10.10.10.10:7001\" {\n\t\t\tt.Errorf(\"NodeAddress = %q, want %q\", got, \"10.10.10.10:7001\")\n\t\t}\n\n\t\t// Addr should be set correctly\n\t\tif got := node.Client.Options().Addr; got != \"10.10.10.10:7001\" {\n\t\t\tt.Errorf(\"Addr = %q, want %q\", got, \"10.10.10.10:7001\")\n\t\t}\n\t})\n\n\tt.Run(\"GetOrCreateWithNodeAddress sets node address\", func(t *testing.T) {\n\t\topt := &ClusterOptions{}\n\t\topt.init()\n\t\tnodes := newClusterNodes(opt)\n\t\tdefer nodes.Close()\n\n\t\t// GetOrCreateWithNodeAddress with node address\n\t\tnode, err := nodes.GetOrCreateWithNodeAddress(\"10.10.10.10:7001\", \"127.0.0.1:7001\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"GetOrCreateWithNodeAddress failed: %v\", err)\n\t\t}\n\n\t\t// NodeAddress should be set\n\t\tif got := node.Client.NodeAddress(); got != \"127.0.0.1:7001\" {\n\t\t\tt.Errorf(\"NodeAddress = %q, want %q\", got, \"127.0.0.1:7001\")\n\t\t}\n\n\t\t// Addr should be the transformed address\n\t\tif got := node.Client.Options().Addr; got != \"10.10.10.10:7001\" {\n\t\t\tt.Errorf(\"Addr = %q, want %q\", got, \"10.10.10.10:7001\")\n\t\t}\n\t})\n}\n\ntype fixedHash string\n\nfunc (h fixedHash) Get(string) string {\n\treturn string(h)\n}\n\nfunc TestRingSetAddrsAndRebalanceRace(t *testing.T) {\n\tconst (\n\t\tringShard1Name = \"ringShardOne\"\n\t\tringShard2Name = \"ringShardTwo\"\n\n\t\tringShard1Port = \"6390\"\n\t\tringShard2Port = \"6391\"\n\t)\n\n\tring := NewRing(&RingOptions{\n\t\tAddrs: map[string]string{\n\t\t\tringShard1Name: \":\" + ringShard1Port,\n\t\t},\n\t\t// Disable heartbeat\n\t\tHeartbeatFrequency: 1 * time.Hour,\n\t\tNewConsistentHash: func(shards []string) ConsistentHash {\n\t\t\tswitch len(shards) {\n\t\t\tcase 1:\n\t\t\t\treturn fixedHash(ringShard1Name)\n\t\t\tcase 2:\n\t\t\t\treturn fixedHash(ringShard2Name)\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"Unexpected number of shards: %v\", shards)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t},\n\t})\n\tdefer ring.Close()\n\n\t// Continuously update addresses by adding and removing one address\n\tupdatesDone := make(chan struct{})\n\tdefer func() { close(updatesDone) }()\n\tgo func() {\n\t\tfor i := 0; ; i++ {\n\t\t\tselect {\n\t\t\tcase <-updatesDone:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tif i%2 == 0 {\n\t\t\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\t\t\tringShard1Name: \":\" + ringShard1Port,\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\t\t\tringShard1Name: \":\" + ringShard1Port,\n\t\t\t\t\t\tringShard2Name: \":\" + ringShard2Port,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\ttimer := time.NewTimer(1 * time.Second)\n\tfor running := true; running; {\n\t\tselect {\n\t\tcase <-timer.C:\n\t\t\trunning = false\n\t\tdefault:\n\t\t\tshard, err := ring.sharding.GetByKey(\"whatever\")\n\t\t\tif err == nil && shard == nil {\n\t\t\t\tt.Fatal(\"shard is nil\")\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc BenchmarkRingShardingRebalanceLocked(b *testing.B) {\n\topts := &RingOptions{\n\t\tAddrs: make(map[string]string),\n\t\t// Disable heartbeat\n\t\tHeartbeatFrequency: 1 * time.Hour,\n\t}\n\tfor i := 0; i < 100; i++ {\n\t\topts.Addrs[fmt.Sprintf(\"shard%d\", i)] = fmt.Sprintf(\":63%02d\", i)\n\t}\n\n\tring := NewRing(opts)\n\tdefer ring.Close()\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tring.sharding.rebalanceLocked()\n\t}\n}\n\ntype testCounter struct {\n\tmu sync.Mutex\n\tt  *testing.T\n\tm  map[string]int\n}\n\nfunc newTestCounter(t *testing.T) *testCounter {\n\treturn &testCounter{t: t, m: make(map[string]int)}\n}\n\nfunc (ct *testCounter) increment(key string) {\n\tct.mu.Lock()\n\tdefer ct.mu.Unlock()\n\tct.m[key]++\n}\n\nfunc (ct *testCounter) expect(values map[string]int) {\n\tct.mu.Lock()\n\tdefer ct.mu.Unlock()\n\tct.t.Helper()\n\tif !reflect.DeepEqual(values, ct.m) {\n\t\tct.t.Errorf(\"expected %v != actual %v\", values, ct.m)\n\t}\n}\n\nfunc TestRingShardsCleanup(t *testing.T) {\n\tconst (\n\t\tringShard1Name = \"ringShardOne\"\n\t\tringShard2Name = \"ringShardTwo\"\n\n\t\tringShard1Addr = \"shard1.test\"\n\t\tringShard2Addr = \"shard2.test\"\n\t)\n\n\tt.Run(\"closes unused shards\", func(t *testing.T) {\n\t\tcloseCounter := newTestCounter(t)\n\n\t\tring := NewRing(&RingOptions{\n\t\t\tAddrs: map[string]string{\n\t\t\t\tringShard1Name: ringShard1Addr,\n\t\t\t\tringShard2Name: ringShard2Addr,\n\t\t\t},\n\t\t\tNewClient: func(opt *Options) *Client {\n\t\t\t\tc := NewClient(opt)\n\t\t\t\tc.baseClient.onClose = c.baseClient.wrappedOnClose(func() error {\n\t\t\t\t\tcloseCounter.increment(opt.Addr)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t})\n\t\tcloseCounter.expect(map[string]int{})\n\n\t\t// no change due to the same addresses\n\t\tring.SetAddrs(map[string]string{\n\t\t\tringShard1Name: ringShard1Addr,\n\t\t\tringShard2Name: ringShard2Addr,\n\t\t})\n\t\tcloseCounter.expect(map[string]int{})\n\n\t\tring.SetAddrs(map[string]string{\n\t\t\tringShard1Name: ringShard1Addr,\n\t\t})\n\t\tcloseCounter.expect(map[string]int{ringShard2Addr: 1})\n\n\t\tring.SetAddrs(map[string]string{\n\t\t\tringShard2Name: ringShard2Addr,\n\t\t})\n\t\tcloseCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 1})\n\n\t\tring.Close()\n\t\tcloseCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 2})\n\t})\n\n\tt.Run(\"closes created shards if ring was closed\", func(t *testing.T) {\n\t\tcreateCounter := newTestCounter(t)\n\t\tcloseCounter := newTestCounter(t)\n\n\t\tvar (\n\t\t\tring        *Ring\n\t\t\tshouldClose int32\n\t\t)\n\n\t\tring = NewRing(&RingOptions{\n\t\t\tAddrs: map[string]string{\n\t\t\t\tringShard1Name: ringShard1Addr,\n\t\t\t},\n\t\t\tNewClient: func(opt *Options) *Client {\n\t\t\t\tif atomic.LoadInt32(&shouldClose) != 0 {\n\t\t\t\t\tring.Close()\n\t\t\t\t}\n\t\t\t\tcreateCounter.increment(opt.Addr)\n\t\t\t\tc := NewClient(opt)\n\t\t\t\tc.baseClient.onClose = c.baseClient.wrappedOnClose(func() error {\n\t\t\t\t\tcloseCounter.increment(opt.Addr)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t})\n\t\tcreateCounter.expect(map[string]int{ringShard1Addr: 1})\n\t\tcloseCounter.expect(map[string]int{})\n\n\t\tatomic.StoreInt32(&shouldClose, 1)\n\n\t\tring.SetAddrs(map[string]string{\n\t\t\tringShard2Name: ringShard2Addr,\n\t\t})\n\t\tcreateCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 1})\n\t\tcloseCounter.expect(map[string]int{ringShard1Addr: 1, ringShard2Addr: 1})\n\t})\n}\n\n//------------------------------------------------------------------------------\n\ntype timeoutErr struct {\n\terror\n}\n\nfunc (e timeoutErr) Timeout() bool {\n\treturn true\n}\n\nfunc (e timeoutErr) Temporary() bool {\n\treturn true\n}\n\nfunc (e timeoutErr) Error() string {\n\treturn \"i/o timeout\"\n}\n\nvar _ = Describe(\"withConn\", func() {\n\tvar client *Client\n\n\tBeforeEach(func() {\n\t\tclient = NewClient(&Options{\n\t\t\tPoolSize: 1,\n\t\t})\n\t})\n\n\tAfterEach(func() {\n\t\tclient.Close()\n\t})\n\n\tIt(\"should replace the connection in the pool when there is no error\", func() {\n\t\tvar conn *pool.Conn\n\n\t\tclient.withConn(ctx, func(ctx context.Context, c *pool.Conn) error {\n\t\t\tconn = c\n\t\t\treturn nil\n\t\t})\n\n\t\tnewConn, err := client.connPool.Get(ctx)\n\t\tExpect(err).To(BeNil())\n\t\tExpect(newConn).To(Equal(conn))\n\t})\n\n\tIt(\"should replace the connection in the pool when there is an error not related to a bad connection\", func() {\n\t\tvar conn *pool.Conn\n\n\t\tclient.withConn(ctx, func(ctx context.Context, c *pool.Conn) error {\n\t\t\tconn = c\n\t\t\treturn proto.RedisError(\"LOADING\")\n\t\t})\n\n\t\tnewConn, err := client.connPool.Get(ctx)\n\t\tExpect(err).To(BeNil())\n\t\tExpect(newConn).To(Equal(conn))\n\t})\n\n\tIt(\"should remove the connection from the pool when it times out\", func() {\n\t\tvar conn *pool.Conn\n\n\t\tclient.withConn(ctx, func(ctx context.Context, c *pool.Conn) error {\n\t\t\tconn = c\n\t\t\treturn timeoutErr{}\n\t\t})\n\n\t\tnewConn, err := client.connPool.Get(ctx)\n\t\tExpect(err).To(BeNil())\n\t\tExpect(newConn).NotTo(Equal(conn))\n\t\tExpect(client.connPool.Len()).To(Equal(1))\n\t})\n})\n\nvar _ = Describe(\"ClusterClient\", func() {\n\tvar client *ClusterClient\n\n\tBeforeEach(func() {\n\t\tclient = &ClusterClient{}\n\t})\n\n\tDescribe(\"cmdSlot\", func() {\n\t\tIt(\"select slot from args for GETKEYSINSLOT command\", func() {\n\t\t\tcmd := NewStringSliceCmd(ctx, \"cluster\", \"getkeysinslot\", 100, 200)\n\n\t\t\tslot := client.cmdSlot(cmd, -1)\n\t\t\tExpect(slot).To(Equal(100))\n\t\t})\n\n\t\tIt(\"select slot from args for COUNTKEYSINSLOT command\", func() {\n\t\t\tcmd := NewStringSliceCmd(ctx, \"cluster\", \"countkeysinslot\", 100)\n\n\t\t\tslot := client.cmdSlot(cmd, -1)\n\t\t\tExpect(slot).To(Equal(100))\n\t\t})\n\n\t\tIt(\"follows preferred random slot\", func() {\n\t\t\tcmd := NewStatusCmd(ctx, \"ping\")\n\n\t\t\tslot := client.cmdSlot(cmd, 101)\n\t\t\tExpect(slot).To(Equal(101))\n\t\t})\n\t})\n})\n\nvar _ = Describe(\"isLoopback\", func() {\n\tDescribeTable(\"should correctly identify loopback addresses\",\n\t\tfunc(host string, expected bool) {\n\t\t\tresult := isLoopback(host)\n\t\t\tExpect(result).To(Equal(expected))\n\t\t},\n\t\t// IP addresses\n\t\tEntry(\"IPv4 loopback\", \"127.0.0.1\", true),\n\t\tEntry(\"IPv6 loopback\", \"::1\", true),\n\t\tEntry(\"IPv4 non-loopback\", \"192.168.1.1\", false),\n\t\tEntry(\"IPv6 non-loopback\", \"2001:db8::1\", false),\n\n\t\t// Well-known loopback hostnames\n\t\tEntry(\"localhost lowercase\", \"localhost\", true),\n\t\tEntry(\"localhost uppercase\", \"LOCALHOST\", true),\n\t\tEntry(\"localhost mixed case\", \"LocalHost\", true),\n\n\t\t// Docker-specific loopbacks\n\t\tEntry(\"host.docker.internal\", \"host.docker.internal\", true),\n\t\tEntry(\"HOST.DOCKER.INTERNAL\", \"HOST.DOCKER.INTERNAL\", true),\n\t\tEntry(\"custom.docker.internal\", \"custom.docker.internal\", true),\n\t\tEntry(\"app.docker.internal\", \"app.docker.internal\", true),\n\n\t\t// Non-loopback hostnames\n\t\tEntry(\"redis hostname\", \"redis-cluster\", false),\n\t\tEntry(\"FQDN\", \"redis.example.com\", false),\n\t\tEntry(\"docker but not internal\", \"redis.docker.com\", false),\n\n\t\t// Edge cases\n\t\tEntry(\"empty string\", \"\", false),\n\t\tEntry(\"invalid IP\", \"256.256.256.256\", false),\n\t\tEntry(\"partial docker internal\", \"docker.internal\", false),\n\t)\n})\n"
  },
  {
    "path": "iterator.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n)\n\n// ScanIterator is used to incrementally iterate over a collection of elements.\ntype ScanIterator struct {\n\tcmd *ScanCmd\n\tpos int\n}\n\n// Err returns the last iterator error, if any.\nfunc (it *ScanIterator) Err() error {\n\treturn it.cmd.Err()\n}\n\n// Next advances the cursor and returns true if more values can be read.\nfunc (it *ScanIterator) Next(ctx context.Context) bool {\n\t// Instantly return on errors.\n\tif it.cmd.Err() != nil {\n\t\treturn false\n\t}\n\n\t// Advance cursor, check if we are still within range.\n\tif it.pos < len(it.cmd.page) {\n\t\tit.pos++\n\t\treturn true\n\t}\n\n\tfor {\n\t\t// Return if there is no more data to fetch.\n\t\tif it.cmd.cursor == 0 {\n\t\t\treturn false\n\t\t}\n\n\t\t// Fetch next page.\n\t\tswitch it.cmd.args[0] {\n\t\tcase \"scan\", \"qscan\":\n\t\t\tit.cmd.args[1] = it.cmd.cursor\n\t\tdefault:\n\t\t\tit.cmd.args[2] = it.cmd.cursor\n\t\t}\n\n\t\terr := it.cmd.process(ctx, it.cmd)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tit.pos = 1\n\n\t\t// Redis can occasionally return empty page.\n\t\tif len(it.cmd.page) > 0 {\n\t\t\treturn true\n\t\t}\n\t}\n}\n\n// Val returns the key/field at the current cursor position.\nfunc (it *ScanIterator) Val() string {\n\tvar v string\n\tif it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) {\n\t\tv = it.cmd.page[it.pos-1]\n\t}\n\treturn v\n}\n"
  },
  {
    "path": "iterator_test.go",
    "content": "package redis_test\n\nimport (\n\t\"fmt\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"ScanIterator\", func() {\n\tvar client *redis.Client\n\n\tseed := func(n int) error {\n\t\tpipe := client.Pipeline()\n\t\tfor i := 1; i <= n; i++ {\n\t\t\tpipe.Set(ctx, fmt.Sprintf(\"K%02d\", i), \"x\", 0).Err()\n\t\t}\n\t\t_, err := pipe.Exec(ctx)\n\t\treturn err\n\t}\n\n\textraSeed := func(n int, m int) error {\n\t\tpipe := client.Pipeline()\n\t\tfor i := 1; i <= m; i++ {\n\t\t\tpipe.Set(ctx, fmt.Sprintf(\"A%02d\", i), \"x\", 0).Err()\n\t\t}\n\t\tfor i := 1; i <= n; i++ {\n\t\t\tpipe.Set(ctx, fmt.Sprintf(\"K%02d\", i), \"x\", 0).Err()\n\t\t}\n\t\t_, err := pipe.Exec(ctx)\n\t\treturn err\n\t}\n\n\thashKey := \"K_HASHTEST\"\n\thashSeed := func(n int) error {\n\t\tpipe := client.Pipeline()\n\t\tfor i := 1; i <= n; i++ {\n\t\t\tpipe.HSet(ctx, hashKey, fmt.Sprintf(\"K%02d\", i), \"x\").Err()\n\t\t}\n\t\t_, err := pipe.Exec(ctx)\n\t\treturn err\n\t}\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should scan across empty DBs\", func() {\n\t\titer := client.Scan(ctx, 0, \"\", 10).Iterator()\n\t\tExpect(iter.Next(ctx)).To(BeFalse())\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should scan across one page\", func() {\n\t\tExpect(seed(7)).NotTo(HaveOccurred())\n\n\t\tvar vals []string\n\t\titer := client.Scan(ctx, 0, \"\", 0).Iterator()\n\t\tfor iter.Next(ctx) {\n\t\t\tvals = append(vals, iter.Val())\n\t\t}\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t\tExpect(vals).To(ConsistOf([]string{\"K01\", \"K02\", \"K03\", \"K04\", \"K05\", \"K06\", \"K07\"}))\n\t})\n\n\tIt(\"should scan across multiple pages\", func() {\n\t\tExpect(seed(71)).NotTo(HaveOccurred())\n\n\t\tvar vals []string\n\t\titer := client.Scan(ctx, 0, \"\", 10).Iterator()\n\t\tfor iter.Next(ctx) {\n\t\t\tvals = append(vals, iter.Val())\n\t\t}\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t\tExpect(vals).To(HaveLen(71))\n\t\tExpect(vals).To(ContainElement(\"K01\"))\n\t\tExpect(vals).To(ContainElement(\"K71\"))\n\t})\n\n\tIt(\"should hscan across multiple pages\", func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\tExpect(hashSeed(71)).NotTo(HaveOccurred())\n\n\t\tvar vals []string\n\t\titer := client.HScan(ctx, hashKey, 0, \"\", 10).Iterator()\n\t\tfor iter.Next(ctx) {\n\t\t\tvals = append(vals, iter.Val())\n\t\t}\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t\tExpect(vals).To(HaveLen(71 * 2))\n\t\tExpect(vals).To(ContainElement(\"K01\"))\n\t\tExpect(vals).To(ContainElement(\"K71\"))\n\t\tExpect(vals).To(ContainElement(\"x\"))\n\t})\n\n\tIt(\"should hscan without values across multiple pages\", Label(\"NonRedisEnterprise\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\tExpect(hashSeed(71)).NotTo(HaveOccurred())\n\n\t\tvar vals []string\n\t\titer := client.HScanNoValues(ctx, hashKey, 0, \"\", 10).Iterator()\n\t\tfor iter.Next(ctx) {\n\t\t\tvals = append(vals, iter.Val())\n\t\t}\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t\tExpect(vals).To(HaveLen(71))\n\t\tExpect(vals).To(ContainElement(\"K01\"))\n\t\tExpect(vals).To(ContainElement(\"K71\"))\n\t\tExpect(vals).NotTo(ContainElement(\"x\"))\n\t})\n\n\tIt(\"should scan to page borders\", func() {\n\t\tExpect(seed(20)).NotTo(HaveOccurred())\n\n\t\tvar vals []string\n\t\titer := client.Scan(ctx, 0, \"\", 10).Iterator()\n\t\tfor iter.Next(ctx) {\n\t\t\tvals = append(vals, iter.Val())\n\t\t}\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t\tExpect(vals).To(HaveLen(20))\n\t})\n\n\tIt(\"should scan with match\", func() {\n\t\tExpect(seed(33)).NotTo(HaveOccurred())\n\n\t\tvar vals []string\n\t\titer := client.Scan(ctx, 0, \"K*2*\", 10).Iterator()\n\t\tfor iter.Next(ctx) {\n\t\t\tvals = append(vals, iter.Val())\n\t\t}\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t\tExpect(vals).To(HaveLen(13))\n\t})\n\n\tIt(\"should scan with match across empty pages\", func() {\n\t\tExpect(extraSeed(2, 10)).NotTo(HaveOccurred())\n\n\t\tvar vals []string\n\t\titer := client.Scan(ctx, 0, \"K*\", 1).Iterator()\n\t\tfor iter.Next(ctx) {\n\t\t\tvals = append(vals, iter.Val())\n\t\t}\n\t\tExpect(iter.Err()).NotTo(HaveOccurred())\n\t\tExpect(vals).To(HaveLen(2))\n\t})\n})\n"
  },
  {
    "path": "json.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\n// -------------------------------------------\n\ntype JSONCmdable interface {\n\tJSONArrAppend(ctx context.Context, key, path string, values ...interface{}) *IntSliceCmd\n\tJSONArrIndex(ctx context.Context, key, path string, value ...interface{}) *IntSliceCmd\n\tJSONArrIndexWithArgs(ctx context.Context, key, path string, options *JSONArrIndexArgs, value ...interface{}) *IntSliceCmd\n\tJSONArrInsert(ctx context.Context, key, path string, index int64, values ...interface{}) *IntSliceCmd\n\tJSONArrLen(ctx context.Context, key, path string) *IntSliceCmd\n\tJSONArrPop(ctx context.Context, key, path string, index int) *StringSliceCmd\n\tJSONArrTrim(ctx context.Context, key, path string) *IntSliceCmd\n\tJSONArrTrimWithArgs(ctx context.Context, key, path string, options *JSONArrTrimArgs) *IntSliceCmd\n\tJSONClear(ctx context.Context, key, path string) *IntCmd\n\tJSONDebugMemory(ctx context.Context, key, path string) *IntCmd\n\tJSONDel(ctx context.Context, key, path string) *IntCmd\n\tJSONForget(ctx context.Context, key, path string) *IntCmd\n\tJSONGet(ctx context.Context, key string, paths ...string) *JSONCmd\n\tJSONGetWithArgs(ctx context.Context, key string, options *JSONGetArgs, paths ...string) *JSONCmd\n\tJSONMerge(ctx context.Context, key, path string, value string) *StatusCmd\n\tJSONMSetArgs(ctx context.Context, docs []JSONSetArgs) *StatusCmd\n\tJSONMSet(ctx context.Context, params ...interface{}) *StatusCmd\n\tJSONMGet(ctx context.Context, path string, keys ...string) *JSONSliceCmd\n\tJSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd\n\tJSONObjKeys(ctx context.Context, key, path string) *SliceCmd\n\tJSONObjLen(ctx context.Context, key, path string) *IntPointerSliceCmd\n\tJSONSet(ctx context.Context, key, path string, value interface{}) *StatusCmd\n\tJSONSetMode(ctx context.Context, key, path string, value interface{}, mode string) *StatusCmd\n\tJSONStrAppend(ctx context.Context, key, path, value string) *IntPointerSliceCmd\n\tJSONStrLen(ctx context.Context, key, path string) *IntPointerSliceCmd\n\tJSONToggle(ctx context.Context, key, path string) *IntPointerSliceCmd\n\tJSONType(ctx context.Context, key, path string) *JSONSliceCmd\n}\n\ntype JSONSetArgs struct {\n\tKey   string\n\tPath  string\n\tValue interface{}\n}\n\ntype JSONArrIndexArgs struct {\n\tStart int\n\tStop  *int\n}\n\ntype JSONArrTrimArgs struct {\n\tStart int\n\tStop  *int\n}\n\ntype JSONCmd struct {\n\tbaseCmd\n\tval      string\n\texpanded interface{}\n}\n\nvar _ Cmder = (*JSONCmd)(nil)\n\nfunc newJSONCmd(ctx context.Context, args ...interface{}) *JSONCmd {\n\treturn &JSONCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeJSON,\n\t\t},\n\t}\n}\n\nfunc (cmd *JSONCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *JSONCmd) SetVal(val string) {\n\tcmd.val = val\n}\n\n// Val returns the result of the JSON.GET command as a string.\nfunc (cmd *JSONCmd) Val() string {\n\tif len(cmd.val) == 0 && cmd.expanded != nil {\n\t\tval, err := json.Marshal(cmd.expanded)\n\t\tif err != nil {\n\t\t\tcmd.SetErr(err)\n\t\t\treturn \"\"\n\t\t}\n\t\treturn string(val)\n\n\t} else {\n\t\treturn cmd.val\n\t}\n}\n\nfunc (cmd *JSONCmd) Result() (string, error) {\n\treturn cmd.Val(), cmd.Err()\n}\n\n// Expanded returns the result of the JSON.GET command as unmarshalled JSON.\nfunc (cmd *JSONCmd) Expanded() (interface{}, error) {\n\tif len(cmd.val) != 0 && cmd.expanded == nil {\n\t\terr := json.Unmarshal([]byte(cmd.val), &cmd.expanded)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn cmd.expanded, nil\n}\n\nfunc (cmd *JSONCmd) readReply(rd *proto.Reader) error {\n\t// nil response from JSON.(M)GET (cmd.baseCmd.err will be \"redis: nil\")\n\t// This happens when the key doesn't exist\n\tif cmd.baseCmd.Err() == Nil {\n\t\tcmd.val = \"\"\n\t\treturn Nil\n\t}\n\n\t// Handle other base command errors\n\tif cmd.baseCmd.Err() != nil {\n\t\treturn cmd.baseCmd.Err()\n\t}\n\n\tif readType, err := rd.PeekReplyType(); err != nil {\n\t\treturn err\n\t} else if readType == proto.RespArray {\n\n\t\tsize, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Empty array means no results found for JSON path, but key exists\n\t\t// This should return \"[]\", not an error\n\t\tif size == 0 {\n\t\t\tcmd.val = \"[]\"\n\t\t\treturn nil\n\t\t}\n\n\t\texpanded := make([]interface{}, size)\n\n\t\tfor i := 0; i < size; i++ {\n\t\t\tif expanded[i], err = rd.ReadReply(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tcmd.expanded = expanded\n\n\t} else {\n\t\tif str, err := rd.ReadString(); err != nil && err != Nil {\n\t\t\treturn err\n\t\t} else if str == \"\" || err == Nil {\n\t\t\tcmd.val = \"\"\n\t\t\treturn Nil\n\t\t} else {\n\t\t\tcmd.val = str\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *JSONCmd) Clone() Cmder {\n\treturn &JSONCmd{\n\t\tbaseCmd:  cmd.cloneBaseCmd(),\n\t\tval:      cmd.val,\n\t\texpanded: cmd.expanded, // interface{} can be shared as it should be immutable after parsing\n\t}\n}\n\n// -------------------------------------------\n\ntype JSONSliceCmd struct {\n\tbaseCmd\n\tval []interface{}\n}\n\nfunc NewJSONSliceCmd(ctx context.Context, args ...interface{}) *JSONSliceCmd {\n\treturn &JSONSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeJSONSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *JSONSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *JSONSliceCmd) SetVal(val []interface{}) {\n\tcmd.val = val\n}\n\nfunc (cmd *JSONSliceCmd) Val() []interface{} {\n\treturn cmd.val\n}\n\nfunc (cmd *JSONSliceCmd) Result() ([]interface{}, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *JSONSliceCmd) readReply(rd *proto.Reader) error {\n\tif cmd.baseCmd.Err() == Nil {\n\t\tcmd.val = nil\n\t\treturn Nil\n\t}\n\n\tif readType, err := rd.PeekReplyType(); err != nil {\n\t\treturn err\n\t} else if readType == proto.RespArray {\n\t\tresponse, err := rd.ReadReply()\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t} else {\n\t\t\tcmd.val = response.([]interface{})\n\t\t}\n\n\t} else {\n\t\tn, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val = make([]interface{}, n)\n\t\tfor i := 0; i < len(cmd.val); i++ {\n\t\t\tswitch s, err := rd.ReadString(); {\n\t\t\tcase err == Nil:\n\t\t\t\tcmd.val[i] = \"\"\n\t\t\tcase err != nil:\n\t\t\t\treturn err\n\t\t\tdefault:\n\t\t\t\tcmd.val[i] = s\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cmd *JSONSliceCmd) Clone() Cmder {\n\tvar val []interface{}\n\tif cmd.val != nil {\n\t\tval = make([]interface{}, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &JSONSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n/*******************************************************************************\n*\n* IntPointerSliceCmd\n* used to represent a RedisJSON response where the result is either an integer or nil\n*\n*******************************************************************************/\n\ntype IntPointerSliceCmd struct {\n\tbaseCmd\n\tval []*int64\n}\n\n// NewIntPointerSliceCmd initialises an IntPointerSliceCmd\nfunc NewIntPointerSliceCmd(ctx context.Context, args ...interface{}) *IntPointerSliceCmd {\n\treturn &IntPointerSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeIntPointerSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *IntPointerSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *IntPointerSliceCmd) SetVal(val []*int64) {\n\tcmd.val = val\n}\n\nfunc (cmd *IntPointerSliceCmd) Val() []*int64 {\n\treturn cmd.val\n}\n\nfunc (cmd *IntPointerSliceCmd) Result() ([]*int64, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *IntPointerSliceCmd) readReply(rd *proto.Reader) error {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]*int64, n)\n\n\tfor i := 0; i < len(cmd.val); i++ {\n\t\tval, err := rd.ReadInt()\n\t\tif err != nil && err != Nil {\n\t\t\treturn err\n\t\t} else if err != Nil {\n\t\t\tcmd.val[i] = &val\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *IntPointerSliceCmd) Clone() Cmder {\n\tvar val []*int64\n\tif cmd.val != nil {\n\t\tval = make([]*int64, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &IntPointerSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\n// JSONArrAppend adds the provided JSON values to the end of the array at the given path.\n// For more information, see https://redis.io/commands/json.arrappend\nfunc (c cmdable) JSONArrAppend(ctx context.Context, key, path string, values ...interface{}) *IntSliceCmd {\n\targs := []interface{}{\"JSON.ARRAPPEND\", key, path}\n\targs = append(args, values...)\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONArrIndex searches for the first occurrence of the provided JSON value in the array at the given path.\n// For more information, see https://redis.io/commands/json.arrindex\nfunc (c cmdable) JSONArrIndex(ctx context.Context, key, path string, value ...interface{}) *IntSliceCmd {\n\targs := []interface{}{\"JSON.ARRINDEX\", key, path}\n\targs = append(args, value...)\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONArrIndexWithArgs searches for the first occurrence of a JSON value in an array while allowing the start and\n// stop options to be provided.\n// For more information, see https://redis.io/commands/json.arrindex\nfunc (c cmdable) JSONArrIndexWithArgs(ctx context.Context, key, path string, options *JSONArrIndexArgs, value ...interface{}) *IntSliceCmd {\n\targs := []interface{}{\"JSON.ARRINDEX\", key, path}\n\targs = append(args, value...)\n\n\tif options != nil {\n\t\targs = append(args, options.Start)\n\t\tif options.Stop != nil {\n\t\t\targs = append(args, *options.Stop)\n\t\t}\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONArrInsert inserts the JSON values into the array at the specified path before the index (shifts to the right).\n// For more information, see https://redis.io/commands/json.arrinsert\nfunc (c cmdable) JSONArrInsert(ctx context.Context, key, path string, index int64, values ...interface{}) *IntSliceCmd {\n\targs := []interface{}{\"JSON.ARRINSERT\", key, path, index}\n\targs = append(args, values...)\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONArrLen reports the length of the JSON array at the specified path in the given key.\n// For more information, see https://redis.io/commands/json.arrlen\nfunc (c cmdable) JSONArrLen(ctx context.Context, key, path string) *IntSliceCmd {\n\targs := []interface{}{\"JSON.ARRLEN\", key, path}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONArrPop removes and returns an element from the specified index in the array.\n// For more information, see https://redis.io/commands/json.arrpop\nfunc (c cmdable) JSONArrPop(ctx context.Context, key, path string, index int) *StringSliceCmd {\n\targs := []interface{}{\"JSON.ARRPOP\", key, path, index}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONArrTrim trims an array to contain only the specified inclusive range of elements.\n// For more information, see https://redis.io/commands/json.arrtrim\nfunc (c cmdable) JSONArrTrim(ctx context.Context, key, path string) *IntSliceCmd {\n\targs := []interface{}{\"JSON.ARRTRIM\", key, path}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONArrTrimWithArgs trims an array to contain only the specified inclusive range of elements.\n// For more information, see https://redis.io/commands/json.arrtrim\nfunc (c cmdable) JSONArrTrimWithArgs(ctx context.Context, key, path string, options *JSONArrTrimArgs) *IntSliceCmd {\n\targs := []interface{}{\"JSON.ARRTRIM\", key, path}\n\n\tif options != nil {\n\t\targs = append(args, options.Start)\n\n\t\tif options.Stop != nil {\n\t\t\targs = append(args, *options.Stop)\n\t\t}\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONClear clears container values (arrays/objects) and sets numeric values to 0.\n// For more information, see https://redis.io/commands/json.clear\nfunc (c cmdable) JSONClear(ctx context.Context, key, path string) *IntCmd {\n\targs := []interface{}{\"JSON.CLEAR\", key, path}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONDebugMemory reports a value's memory usage in bytes (unimplemented)\n// For more information, see https://redis.io/commands/json.debug-memory\nfunc (c cmdable) JSONDebugMemory(ctx context.Context, key, path string) *IntCmd {\n\tpanic(\"not implemented\")\n}\n\n// JSONDel deletes a value.\n// For more information, see https://redis.io/commands/json.del\nfunc (c cmdable) JSONDel(ctx context.Context, key, path string) *IntCmd {\n\targs := []interface{}{\"JSON.DEL\", key, path}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONForget deletes a value.\n// For more information, see https://redis.io/commands/json.forget\nfunc (c cmdable) JSONForget(ctx context.Context, key, path string) *IntCmd {\n\targs := []interface{}{\"JSON.FORGET\", key, path}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONGet returns the value at path in JSON serialized form. JSON.GET returns an\n// array of strings. This function parses out the wrapping array but leaves the\n// internal strings unprocessed by default (see Val())\n// For more information - https://redis.io/commands/json.get/\nfunc (c cmdable) JSONGet(ctx context.Context, key string, paths ...string) *JSONCmd {\n\targs := make([]interface{}, len(paths)+2)\n\targs[0] = \"JSON.GET\"\n\targs[1] = key\n\tfor n, path := range paths {\n\t\targs[n+2] = path\n\t}\n\tcmd := newJSONCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype JSONGetArgs struct {\n\tIndent  string\n\tNewline string\n\tSpace   string\n}\n\n// JSONGetWithArgs - Retrieves the value of a key from a JSON document.\n// This function also allows for specifying additional options such as:\n// Indention, NewLine and Space\n// For more information - https://redis.io/commands/json.get/\nfunc (c cmdable) JSONGetWithArgs(ctx context.Context, key string, options *JSONGetArgs, paths ...string) *JSONCmd {\n\targs := []interface{}{\"JSON.GET\", key}\n\tif options != nil {\n\t\tif options.Indent != \"\" {\n\t\t\targs = append(args, \"INDENT\", options.Indent)\n\t\t}\n\t\tif options.Newline != \"\" {\n\t\t\targs = append(args, \"NEWLINE\", options.Newline)\n\t\t}\n\t\tif options.Space != \"\" {\n\t\t\targs = append(args, \"SPACE\", options.Space)\n\t\t}\n\t\tfor _, path := range paths {\n\t\t\targs = append(args, path)\n\t\t}\n\t}\n\tcmd := newJSONCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONMerge merges a given JSON value into matching paths.\n// For more information, see https://redis.io/commands/json.merge\nfunc (c cmdable) JSONMerge(ctx context.Context, key, path string, value string) *StatusCmd {\n\targs := []interface{}{\"JSON.MERGE\", key, path, value}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONMGet returns the values at the specified path from multiple key arguments.\n// Note - the arguments are reversed when compared with `JSON.MGET` as we want\n// to follow the pattern of having the last argument be variable.\n// For more information, see https://redis.io/commands/json.mget\nfunc (c cmdable) JSONMGet(ctx context.Context, path string, keys ...string) *JSONSliceCmd {\n\targs := make([]interface{}, len(keys)+1)\n\targs[0] = \"JSON.MGET\"\n\tfor n, key := range keys {\n\t\targs[n+1] = key\n\t}\n\targs = append(args, path)\n\tcmd := NewJSONSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONMSetArgs sets or updates one or more JSON values according to the specified key-path-value triplets.\n// For more information, see https://redis.io/commands/json.mset\nfunc (c cmdable) JSONMSetArgs(ctx context.Context, docs []JSONSetArgs) *StatusCmd {\n\targs := []interface{}{\"JSON.MSET\"}\n\tfor _, doc := range docs {\n\t\targs = append(args, doc.Key, doc.Path, doc.Value)\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) JSONMSet(ctx context.Context, params ...interface{}) *StatusCmd {\n\targs := []interface{}{\"JSON.MSET\"}\n\targs = append(args, params...)\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONNumIncrBy increments the number value stored at the specified path by the provided number.\n// For more information, see https://redis.io/docs/latest/commands/json.numincrby/\nfunc (c cmdable) JSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd {\n\targs := []interface{}{\"JSON.NUMINCRBY\", key, path, value}\n\tcmd := newJSONCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONObjKeys returns the keys in the object that's referenced by the specified path.\n// For more information, see https://redis.io/commands/json.objkeys\nfunc (c cmdable) JSONObjKeys(ctx context.Context, key, path string) *SliceCmd {\n\targs := []interface{}{\"JSON.OBJKEYS\", key, path}\n\tcmd := NewSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONObjLen reports the number of keys in the JSON object at the specified path in the given key.\n// For more information, see https://redis.io/commands/json.objlen\nfunc (c cmdable) JSONObjLen(ctx context.Context, key, path string) *IntPointerSliceCmd {\n\targs := []interface{}{\"JSON.OBJLEN\", key, path}\n\tcmd := NewIntPointerSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONSet sets the JSON value at the given path in the given key. The value must be something that\n// can be marshaled to JSON (using encoding/JSON) unless the argument is a string or a []byte when we assume that\n// it can be passed directly as JSON.\n// For more information, see https://redis.io/commands/json.set\nfunc (c cmdable) JSONSet(ctx context.Context, key, path string, value interface{}) *StatusCmd {\n\treturn c.JSONSetMode(ctx, key, path, value, \"\")\n}\n\n// JSONSetMode sets the JSON value at the given path in the given key and allows the mode to be set\n// (the mode value must be \"XX\" or \"NX\"). The value must be something that can be marshaled to JSON (using encoding/JSON) unless\n// the argument is a string or []byte when we assume that it can be passed directly as JSON.\n// For more information, see https://redis.io/commands/json.set\nfunc (c cmdable) JSONSetMode(ctx context.Context, key, path string, value interface{}, mode string) *StatusCmd {\n\tvar bytes []byte\n\tvar err error\n\tswitch v := value.(type) {\n\tcase string:\n\t\tbytes = []byte(v)\n\tcase []byte:\n\t\tbytes = v\n\tdefault:\n\t\tbytes, err = json.Marshal(v)\n\t}\n\targs := []interface{}{\"JSON.SET\", key, path, util.BytesToString(bytes)}\n\tif mode != \"\" {\n\t\tswitch strings.ToUpper(mode) {\n\t\tcase \"XX\", \"NX\":\n\t\t\targs = append(args, strings.ToUpper(mode))\n\n\t\tdefault:\n\t\t\tpanic(\"redis: JSON.SET mode must be NX or XX\")\n\t\t}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\tif err != nil {\n\t\tcmd.SetErr(err)\n\t} else {\n\t\t_ = c(ctx, cmd)\n\t}\n\treturn cmd\n}\n\n// JSONStrAppend appends the JSON-string values to the string at the specified path.\n// For more information, see https://redis.io/commands/json.strappend\nfunc (c cmdable) JSONStrAppend(ctx context.Context, key, path, value string) *IntPointerSliceCmd {\n\targs := []interface{}{\"JSON.STRAPPEND\", key, path, value}\n\tcmd := NewIntPointerSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONStrLen reports the length of the JSON String at the specified path in the given key.\n// For more information, see https://redis.io/commands/json.strlen\nfunc (c cmdable) JSONStrLen(ctx context.Context, key, path string) *IntPointerSliceCmd {\n\targs := []interface{}{\"JSON.STRLEN\", key, path}\n\tcmd := NewIntPointerSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONToggle toggles a Boolean value stored at the specified path.\n// For more information, see https://redis.io/commands/json.toggle\nfunc (c cmdable) JSONToggle(ctx context.Context, key, path string) *IntPointerSliceCmd {\n\targs := []interface{}{\"JSON.TOGGLE\", key, path}\n\tcmd := NewIntPointerSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// JSONType reports the type of JSON value at the specified path.\n// For more information, see https://redis.io/commands/json.type\nfunc (c cmdable) JSONType(ctx context.Context, key, path string) *JSONSliceCmd {\n\targs := []interface{}{\"JSON.TYPE\", key, path}\n\tcmd := NewJSONSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "json_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype JSONGetTestStruct struct {\n\tHello string `json:\"hello\"`\n}\n\nvar _ = Describe(\"JSON Commands\", Label(\"json\"), func() {\n\tctx := context.TODO()\n\tvar client *redis.Client\n\n\tsetupRedisClient := func(protocolVersion int) *redis.Client {\n\t\treturn redis.NewClient(&redis.Options{\n\t\t\tAddr:          \"localhost:6379\",\n\t\t\tDB:            0,\n\t\t\tProtocol:      protocolVersion,\n\t\t\tUnstableResp3: true,\n\t\t})\n\t}\n\n\tAfterEach(func() {\n\t\tif client != nil {\n\t\t\tclient.FlushDB(ctx)\n\t\t\tclient.Close()\n\t\t}\n\t})\n\n\tprotocols := []int{2, 3}\n\tfor _, protocol := range protocols {\n\t\tBeforeEach(func() {\n\t\t\tclient = setupRedisClient(protocol)\n\t\t\tExpect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tDescribe(\"arrays\", Label(\"arrays\"), func() {\n\t\t\tIt(\"should JSONArrAppend\", Label(\"json.arrappend\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"append2\", \"$\", `{\"a\": [10], \"b\": {\"a\": [12, 13]}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONArrAppend(ctx, \"append2\", \"$..a\", 10)\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal([]int64{2, 3}))\n\t\t\t})\n\n\t\t\tIt(\"should JSONArrIndex and JSONArrIndexWithArgs\", Label(\"json.arrindex\", \"json\"), func() {\n\t\t\t\tcmd1, err := client.JSONSet(ctx, \"index1\", \"$\", `{\"a\": [10], \"b\": {\"a\": [12, 10]}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2, err := client.JSONArrIndex(ctx, \"index1\", \"$.b.a\", 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2).To(Equal([]int64{1}))\n\n\t\t\t\tcmd3, err := client.JSONSet(ctx, \"index2\", \"$\", `[0,1,2,3,4]`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3).To(Equal(\"OK\"))\n\n\t\t\t\tres, err := client.JSONArrIndex(ctx, \"index2\", \"$\", 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res[0]).To(Equal(int64(1)))\n\n\t\t\t\tres, err = client.JSONArrIndex(ctx, \"index2\", \"$\", 1, 2).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res[0]).To(Equal(int64(-1)))\n\n\t\t\t\tres, err = client.JSONArrIndex(ctx, \"index2\", \"$\", 4).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res[0]).To(Equal(int64(4)))\n\n\t\t\t\tres, err = client.JSONArrIndexWithArgs(ctx, \"index2\", \"$\", &redis.JSONArrIndexArgs{}, 4).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res[0]).To(Equal(int64(4)))\n\n\t\t\t\tstop := 5000\n\t\t\t\tres, err = client.JSONArrIndexWithArgs(ctx, \"index2\", \"$\", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res[0]).To(Equal(int64(4)))\n\n\t\t\t\tstop = -1\n\t\t\t\tres, err = client.JSONArrIndexWithArgs(ctx, \"index2\", \"$\", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res[0]).To(Equal(int64(-1)))\n\t\t\t})\n\n\t\t\tIt(\"should JSONArrIndex and JSONArrIndexWithArgs with $\", Label(\"json.arrindex\", \"json\"), func() {\n\t\t\t\tdoc := `{\n\t\t\t\t\t\"store\": {\n\t\t\t\t\t\t\"book\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"category\": \"reference\",\n\t\t\t\t\t\t\t\t\"author\": \"Nigel Rees\",\n\t\t\t\t\t\t\t\t\"title\": \"Sayings of the Century\",\n\t\t\t\t\t\t\t\t\"price\": 8.95,\n\t\t\t\t\t\t\t\t\"size\": [10, 20, 30, 40]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"category\": \"fiction\",\n\t\t\t\t\t\t\t\t\"author\": \"Evelyn Waugh\",\n\t\t\t\t\t\t\t\t\"title\": \"Sword of Honour\",\n\t\t\t\t\t\t\t\t\"price\": 12.99,\n\t\t\t\t\t\t\t\t\"size\": [50, 60, 70, 80]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"category\": \"fiction\",\n\t\t\t\t\t\t\t\t\"author\": \"Herman Melville\",\n\t\t\t\t\t\t\t\t\"title\": \"Moby Dick\",\n\t\t\t\t\t\t\t\t\"isbn\": \"0-553-21311-3\",\n\t\t\t\t\t\t\t\t\"price\": 8.99,\n\t\t\t\t\t\t\t\t\"size\": [5, 10, 20, 30]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"category\": \"fiction\",\n\t\t\t\t\t\t\t\t\"author\": \"J. R. R. Tolkien\",\n\t\t\t\t\t\t\t\t\"title\": \"The Lord of the Rings\",\n\t\t\t\t\t\t\t\t\"isbn\": \"0-395-19395-8\",\n\t\t\t\t\t\t\t\t\"price\": 22.99,\n\t\t\t\t\t\t\t\t\"size\": [5, 6, 7, 8]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"bicycle\": {\"color\": \"red\", \"price\": 19.95}\n\t\t\t\t\t}\n\t\t\t\t}`\n\t\t\t\tres, err := client.JSONSet(ctx, \"doc1\", \"$\", doc).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tresGet, err := client.JSONGet(ctx, \"doc1\", \"$.store.book[?(@.price<10)].size\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(\"[[10,20,30,40],[5,10,20,30]]\"))\n\n\t\t\t\tresArr, err := client.JSONArrIndex(ctx, \"doc1\", \"$.store.book[?(@.price<10)].size\", 20).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resArr).To(Equal([]int64{1, 2}))\n\n\t\t\t\t_, err = client.JSONGet(ctx, \"this-key-does-not-exist\", \"$\").Result()\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\tExpect(err).To(BeIdenticalTo(redis.Nil))\n\t\t\t})\n\n\t\t\tIt(\"should JSONArrInsert\", Label(\"json.arrinsert\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"insert2\", \"$\", `[100, 200, 300, 200]`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONArrInsert(ctx, \"insert2\", \"$\", -1, 1, 2)\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal([]int64{6}))\n\n\t\t\t\tcmd3 := client.JSONGet(ctx, \"insert2\")\n\t\t\t\tExpect(cmd3.Err()).NotTo(HaveOccurred())\n\t\t\t\t// RESP2 vs RESP3\n\t\t\t\tExpect(cmd3.Val()).To(Or(\n\t\t\t\t\tEqual(`[100,200,300,1,2,200]`),\n\t\t\t\t\tEqual(`[[100,200,300,1,2,200]]`)))\n\t\t\t})\n\n\t\t\tIt(\"should JSONArrLen\", Label(\"json.arrlen\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"length2\", \"$\", `{\"a\": [10], \"b\": {\"a\": [12, 10, 20, 12, 90, 10]}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONArrLen(ctx, \"length2\", \"$..a\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal([]int64{1, 6}))\n\t\t\t})\n\n\t\t\tIt(\"should JSONArrPop\", Label(\"json.arrpop\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"pop4\", \"$\", `[100, 200, 300, 200]`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONArrPop(ctx, \"pop4\", \"$\", 2)\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal([]string{\"300\"}))\n\n\t\t\t\tcmd3 := client.JSONGet(ctx, \"pop4\", \"$\")\n\t\t\t\tExpect(cmd3.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3.Val()).To(Equal(\"[[100,200,200]]\"))\n\t\t\t})\n\n\t\t\tIt(\"should JSONArrTrim\", Label(\"json.arrtrim\", \"json\"), func() {\n\t\t\t\tcmd1, err := client.JSONSet(ctx, \"trim1\", \"$\", `[0,1,2,3,4]`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1).To(Equal(\"OK\"))\n\n\t\t\t\tstop := 3\n\t\t\t\tcmd2, err := client.JSONArrTrimWithArgs(ctx, \"trim1\", \"$\", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2).To(Equal([]int64{3}))\n\n\t\t\t\tres, err := client.JSONGet(ctx, \"trim1\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(`[[1,2,3]]`))\n\n\t\t\t\tcmd3, err := client.JSONSet(ctx, \"trim2\", \"$\", `[0,1,2,3,4]`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3).To(Equal(\"OK\"))\n\n\t\t\t\tstop = 3\n\t\t\t\tcmd4, err := client.JSONArrTrimWithArgs(ctx, \"trim2\", \"$\", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd4).To(Equal([]int64{0}))\n\n\t\t\t\tcmd5, err := client.JSONSet(ctx, \"trim3\", \"$\", `[0,1,2,3,4]`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd5).To(Equal(\"OK\"))\n\n\t\t\t\tstop = 99\n\t\t\t\tcmd6, err := client.JSONArrTrimWithArgs(ctx, \"trim3\", \"$\", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd6).To(Equal([]int64{2}))\n\n\t\t\t\tcmd7, err := client.JSONSet(ctx, \"trim4\", \"$\", `[0,1,2,3,4]`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd7).To(Equal(\"OK\"))\n\n\t\t\t\tstop = 1\n\t\t\t\tcmd8, err := client.JSONArrTrimWithArgs(ctx, \"trim4\", \"$\", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd8).To(Equal([]int64{0}))\n\n\t\t\t\tcmd9, err := client.JSONSet(ctx, \"trim5\", \"$\", `[0,1,2,3,4]`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd9).To(Equal(\"OK\"))\n\n\t\t\t\tstop = 11\n\t\t\t\tcmd10, err := client.JSONArrTrimWithArgs(ctx, \"trim5\", \"$\", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd10).To(Equal([]int64{0}))\n\t\t\t})\n\n\t\t\tIt(\"should JSONArrPop\", Label(\"json.arrpop\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"pop4\", \"$\", `[100, 200, 300, 200]`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONArrPop(ctx, \"pop4\", \"$\", 2)\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal([]string{\"300\"}))\n\n\t\t\t\tcmd3 := client.JSONGet(ctx, \"pop4\", \"$\")\n\t\t\t\tExpect(cmd3.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3.Val()).To(Equal(\"[[100,200,200]]\"))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"get/set\", Label(\"getset\"), func() {\n\t\t\tIt(\"should JSONSet\", Label(\"json.set\", \"json\"), func() {\n\t\t\t\tcmd := client.JSONSet(ctx, \"set1\", \"$\", `{\"a\": 1, \"b\": 2, \"hello\": \"world\"}`)\n\t\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd.Val()).To(Equal(\"OK\"))\n\t\t\t})\n\n\t\t\tIt(\"should JSONGet\", Label(\"json.get\", \"json\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tres, err := client.JSONSet(ctx, \"get3\", \"$\", `{\"a\": 1, \"b\": 2}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tres, err = client.JSONGetWithArgs(ctx, \"get3\", &redis.JSONGetArgs{Indent: \"-\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(`{-\"a\":1,-\"b\":2}`))\n\n\t\t\t\tres, err = client.JSONGetWithArgs(ctx, \"get3\", &redis.JSONGetArgs{Indent: \"-\", Newline: `~`, Space: `!`}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(`{~-\"a\":!1,~-\"b\":!2~}`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONMerge\", Label(\"json.merge\", \"json\"), func() {\n\t\t\t\tres, err := client.JSONSet(ctx, \"merge1\", \"$\", `{\"a\": 1, \"b\": 2}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tres, err = client.JSONMerge(ctx, \"merge1\", \"$\", `{\"b\": 3, \"c\": 4}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tres, err = client.JSONGet(ctx, \"merge1\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(`[{\"a\":1,\"b\":3,\"c\":4}]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONMSet\", Label(\"json.mset\", \"json\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tdoc1 := redis.JSONSetArgs{Key: \"mset1\", Path: \"$\", Value: `{\"a\": 1}`}\n\t\t\t\tdoc2 := redis.JSONSetArgs{Key: \"mset2\", Path: \"$\", Value: 2}\n\t\t\t\tdocs := []redis.JSONSetArgs{doc1, doc2}\n\n\t\t\t\tmSetResult, err := client.JSONMSetArgs(ctx, docs).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(mSetResult).To(Equal(\"OK\"))\n\n\t\t\t\tres, err := client.JSONMGet(ctx, \"$\", \"mset1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]interface{}{`[{\"a\":1}]`}))\n\n\t\t\t\tres, err = client.JSONMGet(ctx, \"$\", \"mset1\", \"mset2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal([]interface{}{`[{\"a\":1}]`, \"[2]\"}))\n\n\t\t\t\t_, err = client.JSONMSet(ctx, \"mset1\", \"$.a\", 2, \"mset3\", \"$\", `[1]`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t})\n\n\t\t\tIt(\"should JSONMGet\", Label(\"json.mget\", \"json\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"mget2a\", \"$\", `{\"a\": [\"aa\", \"ab\", \"ac\", \"ad\"], \"b\": {\"a\": [\"ba\", \"bb\", \"bc\", \"bd\"]}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\t\t\t\tcmd2 := client.JSONSet(ctx, \"mget2b\", \"$\", `{\"a\": [100, 200, 300, 200], \"b\": {\"a\": [100, 200, 300, 200]}}`)\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd3 := client.JSONMGet(ctx, \"$..a\", \"mget2a\", \"mget2b\")\n\t\t\t\tExpect(cmd3.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3.Val()).To(HaveLen(2))\n\t\t\t\tExpect(cmd3.Val()[0]).To(Equal(`[[\"aa\",\"ab\",\"ac\",\"ad\"],[\"ba\",\"bb\",\"bc\",\"bd\"]]`))\n\t\t\t\tExpect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONMget with $\", Label(\"json.mget\", \"json\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tres, err := client.JSONSet(ctx, \"doc1\", \"$\", `{\"a\": 1, \"b\": 2, \"nested\": {\"a\": 3}, \"c\": \"\", \"nested2\": {\"a\": \"\"}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tres, err = client.JSONSet(ctx, \"doc2\", \"$\", `{\"a\": 4, \"b\": 5, \"nested\": {\"a\": 6}, \"c\": \"\", \"nested2\": {\"a\": [\"\"]}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err := client.JSONMGet(ctx, \"$..a\", \"doc1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal([]interface{}{`[1,3,\"\"]`}))\n\n\t\t\t\tiRes, err = client.JSONMGet(ctx, \"$..a\", \"doc1\", \"doc2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal([]interface{}{`[1,3,\"\"]`, `[4,6,[\"\"]]`}))\n\n\t\t\t\tiRes, err = client.JSONMGet(ctx, \"$..a\", \"non_existing_doc\", \"non_existing_doc1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal([]interface{}{nil, nil}))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Misc\", Label(\"misc\"), func() {\n\t\t\tIt(\"should JSONClear\", Label(\"json.clear\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"clear1\", \"$\", `[1]`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONClear(ctx, \"clear1\", \"$\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal(int64(1)))\n\n\t\t\t\tcmd3 := client.JSONGet(ctx, \"clear1\", \"$\")\n\t\t\t\tExpect(cmd3.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3.Val()).To(Equal(`[[]]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONClear with $\", Label(\"json.clear\", \"json\"), func() {\n\t\t\t\tdoc := `{\n\t\t\t\t\t\"nested1\": {\"a\": {\"foo\": 10, \"bar\": 20}},\n\t\t\t\t\t\"a\": [\"foo\"],\n\t\t\t\t\t\"nested2\": {\"a\": \"claro\"},\n\t\t\t\t\t\"nested3\": {\"a\": {\"baz\": 50}}\n\t\t\t\t}`\n\t\t\t\tres, err := client.JSONSet(ctx, \"doc1\", \"$\", doc).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err := client.JSONClear(ctx, \"doc1\", \"$..a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(3)))\n\n\t\t\t\tresGet, err := client.JSONGet(ctx, \"doc1\", `$`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(`[{\"nested1\":{\"a\":{}},\"a\":[],\"nested2\":{\"a\":\"claro\"},\"nested3\":{\"a\":{}}}]`))\n\n\t\t\t\tres, err = client.JSONSet(ctx, \"doc1\", \"$\", doc).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err = client.JSONClear(ctx, \"doc1\", \"$.nested1.a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(1)))\n\n\t\t\t\tresGet, err = client.JSONGet(ctx, \"doc1\", `$`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(`[{\"nested1\":{\"a\":{}},\"a\":[\"foo\"],\"nested2\":{\"a\":\"claro\"},\"nested3\":{\"a\":{\"baz\":50}}}]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONDel\", Label(\"json.del\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"del1\", \"$\", `[1]`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONDel(ctx, \"del1\", \"$\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal(int64(1)))\n\n\t\t\t\tcmd3 := client.JSONGet(ctx, \"del1\", \"$\")\n\t\t\t\tExpect(cmd3.Err()).To(Equal(redis.Nil))\n\t\t\t\tExpect(cmd3.Val()).To(Equal(\"\"))\n\t\t\t})\n\n\t\t\tIt(\"should JSONDel with $\", Label(\"json.del\", \"json\"), func() {\n\t\t\t\tres, err := client.JSONSet(ctx, \"del1\", \"$\", `{\"a\": 1, \"nested\": {\"a\": 2, \"b\": 3}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err := client.JSONDel(ctx, \"del1\", \"$..a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(2)))\n\n\t\t\t\tresGet, err := client.JSONGet(ctx, \"del1\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(`[{\"nested\":{\"b\":3}}]`))\n\n\t\t\t\tres, err = client.JSONSet(ctx, \"del2\", \"$\", `{\"a\": {\"a\": 2, \"b\": 3}, \"b\": [\"a\", \"b\"], \"nested\": {\"b\": [true, \"a\", \"b\"]}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err = client.JSONDel(ctx, \"del2\", \"$..a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(1)))\n\n\t\t\t\tresGet, err = client.JSONGet(ctx, \"del2\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(`[{\"nested\":{\"b\":[true,\"a\",\"b\"]},\"b\":[\"a\",\"b\"]}]`))\n\n\t\t\t\tdoc := `[\n\t\t\t\t\t{\n\t\t\t\t\t\t\"ciao\": [\"non ancora\"],\n\t\t\t\t\t\t\"nested\": [\n\t\t\t\t\t\t\t{\"ciao\": [1, \"a\"]},\n\t\t\t\t\t\t\t{\"ciao\": [2, \"a\"]},\n\t\t\t\t\t\t\t{\"ciaoc\": [3, \"non\", \"ciao\"]},\n\t\t\t\t\t\t\t{\"ciao\": [4, \"a\"]},\n\t\t\t\t\t\t\t{\"e\": [5, \"non\", \"ciao\"]}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]`\n\t\t\t\tres, err = client.JSONSet(ctx, \"del3\", \"$\", doc).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err = client.JSONDel(ctx, \"del3\", `$.[0][\"nested\"]..ciao`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(3)))\n\n\t\t\t\tresVal := `[[{\"ciao\":[\"non ancora\"],\"nested\":[{},{},{\"ciaoc\":[3,\"non\",\"ciao\"]},{},{\"e\":[5,\"non\",\"ciao\"]}]}]]`\n\t\t\t\tresGet, err = client.JSONGet(ctx, \"del3\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(resVal))\n\t\t\t})\n\n\t\t\tIt(\"should JSONForget\", Label(\"json.forget\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"forget3\", \"$\", `{\"a\": [1,2,3], \"b\": {\"a\": [1,2,3], \"b\": \"annie\"}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONForget(ctx, \"forget3\", \"$..a\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal(int64(2)))\n\n\t\t\t\tcmd3 := client.JSONGet(ctx, \"forget3\", \"$\")\n\t\t\t\tExpect(cmd3.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3.Val()).To(Equal(`[{\"b\":{\"b\":\"annie\"}}]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONForget with $\", Label(\"json.forget\", \"json\"), func() {\n\t\t\t\tres, err := client.JSONSet(ctx, \"doc1\", \"$\", `{\"a\": 1, \"nested\": {\"a\": 2, \"b\": 3}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err := client.JSONForget(ctx, \"doc1\", \"$..a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(2)))\n\n\t\t\t\tresGet, err := client.JSONGet(ctx, \"doc1\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(`[{\"nested\":{\"b\":3}}]`))\n\n\t\t\t\tres, err = client.JSONSet(ctx, \"doc2\", \"$\", `{\"a\": {\"a\": 2, \"b\": 3}, \"b\": [\"a\", \"b\"], \"nested\": {\"b\": [true, \"a\", \"b\"]}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err = client.JSONForget(ctx, \"doc2\", \"$..a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(1)))\n\n\t\t\t\tresGet, err = client.JSONGet(ctx, \"doc2\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(`[{\"nested\":{\"b\":[true,\"a\",\"b\"]},\"b\":[\"a\",\"b\"]}]`))\n\n\t\t\t\tdoc := `[\n\t\t\t\t\t{\n\t\t\t\t\t\t\"ciao\": [\"non ancora\"],\n\t\t\t\t\t\t\"nested\": [\n\t\t\t\t\t\t\t{\"ciao\": [1, \"a\"]},\n\t\t\t\t\t\t\t{\"ciao\": [2, \"a\"]},\n\t\t\t\t\t\t\t{\"ciaoc\": [3, \"non\", \"ciao\"]},\n\t\t\t\t\t\t\t{\"ciao\": [4, \"a\"]},\n\t\t\t\t\t\t\t{\"e\": [5, \"non\", \"ciao\"]}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]`\n\t\t\t\tres, err = client.JSONSet(ctx, \"doc3\", \"$\", doc).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tiRes, err = client.JSONForget(ctx, \"doc3\", `$.[0][\"nested\"]..ciao`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(iRes).To(Equal(int64(3)))\n\n\t\t\t\tresVal := `[[{\"ciao\":[\"non ancora\"],\"nested\":[{},{},{\"ciaoc\":[3,\"non\",\"ciao\"]},{},{\"e\":[5,\"non\",\"ciao\"]}]}]]`\n\t\t\t\tresGet, err = client.JSONGet(ctx, \"doc3\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resGet).To(Equal(resVal))\n\t\t\t})\n\n\t\t\tIt(\"should JSONNumIncrBy\", Label(\"json.numincrby\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"incr3\", \"$\", `{\"a\": [1, 2], \"b\": {\"a\": [0, -1]}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONNumIncrBy(ctx, \"incr3\", \"$..a[1]\", float64(1))\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(Equal(`[3,0]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONNumIncrBy with $\", Label(\"json.numincrby\", \"json\"), func() {\n\t\t\t\tres, err := client.JSONSet(ctx, \"doc1\", \"$\", `{\"a\": \"b\", \"b\": [{\"a\": 2}, {\"a\": 5.0}, {\"a\": \"c\"}]}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tres, err = client.JSONNumIncrBy(ctx, \"doc1\", \"$.b[1].a\", 2).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(`[7]`))\n\n\t\t\t\tres, err = client.JSONNumIncrBy(ctx, \"doc1\", \"$.b[1].a\", 3.5).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(`[10.5]`))\n\n\t\t\t\tres, err = client.JSONSet(ctx, \"doc2\", \"$\", `{\"a\": \"b\", \"b\": [{\"a\": 2}, {\"a\": 5.0}, {\"a\": \"c\"}]}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tres, err = client.JSONNumIncrBy(ctx, \"doc2\", \"$.b[0].a\", 3).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(`[5]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONObjKeys\", Label(\"json.objkeys\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"objkeys1\", \"$\", `{\"a\": [1, 2], \"b\": {\"a\": [0, -1]}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONObjKeys(ctx, \"objkeys1\", \"$..*\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(HaveLen(7))\n\t\t\t\tExpect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{\"a\"}, nil, nil, nil, nil, nil}))\n\t\t\t})\n\n\t\t\tIt(\"should JSONObjKeys with $\", Label(\"json.objkeys\", \"json\"), func() {\n\t\t\t\tdoc := `{\n\t\t\t\t\t\"nested1\": {\"a\": {\"foo\": 10, \"bar\": 20}},\n\t\t\t\t\t\"a\": [\"foo\"],\n\t\t\t\t\t\"nested2\": {\"a\": {\"baz\": 50}}\n\t\t\t\t}`\n\t\t\t\tcmd1, err := client.JSONSet(ctx, \"objkeys1\", \"$\", doc).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2, err := client.JSONObjKeys(ctx, \"objkeys1\", \"$.nested1.a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2).To(Equal([]interface{}{[]interface{}{\"foo\", \"bar\"}}))\n\n\t\t\t\tcmd2, err = client.JSONObjKeys(ctx, \"objkeys1\", \".*.a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2).To(Equal([]interface{}{\"foo\", \"bar\"}))\n\n\t\t\t\tcmd2, err = client.JSONObjKeys(ctx, \"objkeys1\", \".nested2.a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2).To(Equal([]interface{}{\"baz\"}))\n\n\t\t\t\t_, err = client.JSONObjKeys(ctx, \"non_existing_doc\", \"..a\").Result()\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t})\n\n\t\t\tIt(\"should JSONObjLen\", Label(\"json.objlen\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"objlen2\", \"$\", `{\"a\": [1, 2], \"b\": {\"a\": [0, -1]}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONObjLen(ctx, \"objlen2\", \"$..*\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(HaveLen(7))\n\t\t\t\tExpect(cmd2.Val()[0]).To(BeNil())\n\t\t\t\tExpect(*cmd2.Val()[1]).To(Equal(int64(1)))\n\t\t\t})\n\n\t\t\tIt(\"should JSONStrLen\", Label(\"json.strlen\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"strlen2\", \"$\", `{\"a\": \"alice\", \"b\": \"bob\", \"c\": {\"a\": \"alice\", \"b\": \"bob\"}}`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONStrLen(ctx, \"strlen2\", \"$..*\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(HaveLen(5))\n\t\t\t\tvar tmp int64 = 20\n\t\t\t\tExpect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp))\n\t\t\t\tExpect(*cmd2.Val()[0]).To(Equal(int64(5)))\n\t\t\t\tExpect(*cmd2.Val()[1]).To(Equal(int64(3)))\n\t\t\t\tExpect(cmd2.Val()[2]).To(BeNil())\n\t\t\t\tExpect(*cmd2.Val()[3]).To(Equal(int64(5)))\n\t\t\t\tExpect(*cmd2.Val()[4]).To(Equal(int64(3)))\n\t\t\t})\n\n\t\t\tIt(\"should JSONStrAppend\", Label(\"json.strappend\", \"json\"), func() {\n\t\t\t\tcmd1, err := client.JSONSet(ctx, \"strapp1\", \"$\", `\"foo\"`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1).To(Equal(\"OK\"))\n\t\t\t\tcmd2, err := client.JSONStrAppend(ctx, \"strapp1\", \"$\", `\"bar\"`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(*cmd2[0]).To(Equal(int64(6)))\n\t\t\t\tcmd3, err := client.JSONGet(ctx, \"strapp1\", \"$\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd3).To(Equal(`[\"foobar\"]`))\n\t\t\t})\n\n\t\t\tIt(\"should JSONStrAppend and JSONStrLen with $\", Label(\"json.strappend\", \"json.strlen\", \"json\"), func() {\n\t\t\t\tres, err := client.JSONSet(ctx, \"doc1\", \"$\", `{\"a\": \"foo\", \"nested1\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tintArrayResult, err := client.JSONStrAppend(ctx, \"doc1\", \"$.nested1.a\", `\"baz\"`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(*intArrayResult[0]).To(Equal(int64(8)))\n\n\t\t\t\tres, err = client.JSONSet(ctx, \"doc2\", \"$\", `{\"a\": \"foo\", \"nested1\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}`).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(Equal(\"OK\"))\n\n\t\t\t\tintResult, err := client.JSONStrLen(ctx, \"doc2\", \"$.nested1.a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(*intResult[0]).To(Equal(int64(5)))\n\t\t\t})\n\n\t\t\tIt(\"should JSONToggle\", Label(\"json.toggle\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"toggle1\", \"$\", `[true]`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONToggle(ctx, \"toggle1\", \"$[0]\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(HaveLen(1))\n\t\t\t\tExpect(*cmd2.Val()[0]).To(Equal(int64(0)))\n\t\t\t})\n\n\t\t\tIt(\"should JSONType\", Label(\"json.type\", \"json\"), func() {\n\t\t\t\tcmd1 := client.JSONSet(ctx, \"type1\", \"$\", `[true]`)\n\t\t\t\tExpect(cmd1.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd1.Val()).To(Equal(\"OK\"))\n\n\t\t\t\tcmd2 := client.JSONType(ctx, \"type1\", \"$[0]\")\n\t\t\t\tExpect(cmd2.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd2.Val()).To(HaveLen(1))\n\t\t\t\t// RESP2 v RESP3\n\t\t\t\tExpect(cmd2.Val()[0]).To(Or(Equal([]interface{}{\"boolean\"}), Equal(\"boolean\")))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"JSON Nil Handling\", func() {\n\t\t\tIt(\"should return redis.Nil for non-existent key\", func() {\n\t\t\t\t_, err := client.JSONGet(ctx, \"non-existent-key\", \"$\").Result()\n\t\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\t})\n\n\t\t\tIt(\"should return empty array for non-existent path in existing key\", func() {\n\t\t\t\terr := client.JSONSet(ctx, \"test-key\", \"$\", `{\"a\": 1, \"b\": \"hello\"}`).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Non-existent path should return empty array, not error\n\t\t\t\tval, err := client.JSONGet(ctx, \"test-key\", \"$.nonexistent\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(Equal(\"[]\"))\n\t\t\t})\n\n\t\t\tIt(\"should distinguish empty array from non-existent path\", func() {\n\t\t\t\terr := client.JSONSet(ctx, \"test-key\", \"$\", `{\"arr\": [], \"obj\": {}}`).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Empty array should return the array\n\t\t\t\tval, err := client.JSONGet(ctx, \"test-key\", \"$.arr\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(Equal(\"[[]]\"))\n\n\t\t\t\t// Non-existent field should return empty array\n\t\t\t\tval, err = client.JSONGet(ctx, \"test-key\", \"$.missing\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(Equal(\"[]\"))\n\t\t\t})\n\n\t\t\tIt(\"should handle multiple paths with mixed results\", func() {\n\t\t\t\terr := client.JSONSet(ctx, \"test-key\", \"$\", `{\"a\": 1, \"b\": 2}`).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Path that exists\n\t\t\t\tval, err := client.JSONGet(ctx, \"test-key\", \"$.a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(Equal(\"[1]\"))\n\n\t\t\t\t// Path that doesn't exist should return empty array\n\t\t\t\tval, err = client.JSONGet(ctx, \"test-key\", \"$.c\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(Equal(\"[]\"))\n\t\t\t})\n\n\t\t\tAfterEach(func() {\n\t\t\t\t// Clean up test keys\n\t\t\t\tclient.Del(ctx, \"test-key\", \"non-existent-key\")\n\t\t\t})\n\t\t})\n\t}\n})\n\nvar _ = Describe(\"Go-Redis Advanced JSON and RediSearch Tests\", func() {\n\tvar client *redis.Client\n\tvar ctx = context.Background()\n\n\tsetupRedisClient := func(protocolVersion int) *redis.Client {\n\t\treturn redis.NewClient(&redis.Options{\n\t\t\tAddr:          \"localhost:6379\",\n\t\t\tDB:            0,\n\t\t\tProtocol:      protocolVersion, // Setting RESP2 or RESP3 protocol\n\t\t\tUnstableResp3: true,            // Enable RESP3 features\n\t\t})\n\t}\n\n\tAfterEach(func() {\n\t\tif client != nil {\n\t\t\tclient.FlushDB(ctx)\n\t\t\tclient.Close()\n\t\t}\n\t})\n\n\tContext(\"when testing with RESP2 and RESP3\", func() {\n\t\tprotocols := []int{2, 3}\n\n\t\tfor _, protocol := range protocols {\n\t\t\tWhen(\"using protocol version\", func() {\n\t\t\t\tBeforeEach(func() {\n\t\t\t\t\tclient = setupRedisClient(protocol)\n\t\t\t\t})\n\n\t\t\t\tIt(\"should perform complex JSON and RediSearch operations\", func() {\n\t\t\t\t\tjsonDoc := map[string]interface{}{\n\t\t\t\t\t\t\"person\": map[string]interface{}{\n\t\t\t\t\t\t\t\"name\":   \"Alice\",\n\t\t\t\t\t\t\t\"age\":    30,\n\t\t\t\t\t\t\t\"status\": true,\n\t\t\t\t\t\t\t\"address\": map[string]interface{}{\n\t\t\t\t\t\t\t\t\"city\":     \"Wonderland\",\n\t\t\t\t\t\t\t\t\"postcode\": \"12345\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"contacts\": []map[string]interface{}{\n\t\t\t\t\t\t\t\t{\"type\": \"email\", \"value\": \"alice@example.com\"},\n\t\t\t\t\t\t\t\t{\"type\": \"phone\", \"value\": \"+123456789\"},\n\t\t\t\t\t\t\t\t{\"type\": \"fax\", \"value\": \"+987654321\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"friends\": []map[string]interface{}{\n\t\t\t\t\t\t\t\t{\"name\": \"Bob\", \"age\": 35, \"status\": true},\n\t\t\t\t\t\t\t\t{\"name\": \"Charlie\", \"age\": 28, \"status\": false},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"settings\": map[string]interface{}{\n\t\t\t\t\t\t\t\"notifications\": map[string]interface{}{\n\t\t\t\t\t\t\t\t\"email\":  true,\n\t\t\t\t\t\t\t\t\"sms\":    false,\n\t\t\t\t\t\t\t\t\"alerts\": []string{\"low battery\", \"door open\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"theme\": \"dark\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\n\t\t\t\t\tsetCmd := client.JSONSet(ctx, \"person:1\", \".\", jsonDoc)\n\t\t\t\t\tExpect(setCmd.Err()).NotTo(HaveOccurred(), \"JSON.SET failed\")\n\n\t\t\t\t\tgetCmdRaw := client.JSONGet(ctx, \"person:1\", \".\")\n\t\t\t\t\trawJSON, err := getCmdRaw.Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred(), \"JSON.GET (raw) failed\")\n\t\t\t\t\tGinkgoWriter.Printf(\"Raw JSON: %s\\n\", rawJSON)\n\n\t\t\t\t\tgetCmdExpanded := client.JSONGet(ctx, \"person:1\", \".\")\n\t\t\t\t\texpandedJSON, err := getCmdExpanded.Expanded()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred(), \"JSON.GET (expanded) failed\")\n\t\t\t\t\tGinkgoWriter.Printf(\"Expanded JSON: %+v\\n\", expandedJSON)\n\n\t\t\t\t\tExpect(rawJSON).To(MatchJSON(jsonMustMarshal(expandedJSON)))\n\n\t\t\t\t\tarrAppendCmd := client.JSONArrAppend(ctx, \"person:1\", \"$.person.contacts\", `{\"type\": \"social\", \"value\": \"@alice_wonder\"}`)\n\t\t\t\t\tExpect(arrAppendCmd.Err()).NotTo(HaveOccurred(), \"JSON.ARRAPPEND failed\")\n\t\t\t\t\tarrLenCmd := client.JSONArrLen(ctx, \"person:1\", \"$.person.contacts\")\n\t\t\t\t\tarrLen, err := arrLenCmd.Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred(), \"JSON.ARRLEN failed\")\n\t\t\t\t\tExpect(arrLen).To(Equal([]int64{4}), \"Array length mismatch after append\")\n\n\t\t\t\t\tarrInsertCmd := client.JSONArrInsert(ctx, \"person:1\", \"$.person.friends\", 1, `{\"name\": \"Diana\", \"age\": 25, \"status\": true}`)\n\t\t\t\t\tExpect(arrInsertCmd.Err()).NotTo(HaveOccurred(), \"JSON.ARRINSERT failed\")\n\n\t\t\t\t\tstart := 0\n\t\t\t\t\tstop := 1\n\t\t\t\t\tarrTrimCmd := client.JSONArrTrimWithArgs(ctx, \"person:1\", \"$.person.friends\", &redis.JSONArrTrimArgs{Start: start, Stop: &stop})\n\t\t\t\t\tExpect(arrTrimCmd.Err()).NotTo(HaveOccurred(), \"JSON.ARRTRIM failed\")\n\n\t\t\t\t\tmergeData := map[string]interface{}{\n\t\t\t\t\t\t\"status\":    false,\n\t\t\t\t\t\t\"nickname\":  \"WonderAlice\",\n\t\t\t\t\t\t\"lastLogin\": time.Now().Format(time.RFC3339),\n\t\t\t\t\t}\n\t\t\t\t\tmergeCmd := client.JSONMerge(ctx, \"person:1\", \"$.person\", jsonMustMarshal(mergeData))\n\t\t\t\t\tExpect(mergeCmd.Err()).NotTo(HaveOccurred(), \"JSON.MERGE failed\")\n\n\t\t\t\t\ttypeCmd := client.JSONType(ctx, \"person:1\", \"$.person.nickname\")\n\t\t\t\t\tnicknameType, err := typeCmd.Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred(), \"JSON.TYPE failed\")\n\t\t\t\t\tExpect(nicknameType).To(HaveLen(1), \"JSON.TYPE should return one element\")\n\t\t\t\t\t// RESP2 v RESP3\n\t\t\t\t\tExpect(nicknameType[0]).To(Or(Equal([]interface{}{\"string\"}), Equal(\"string\")), \"JSON.TYPE mismatch for nickname\")\n\n\t\t\t\t\tcreateIndexCmd := client.Do(ctx, \"FT.CREATE\", \"person_idx\", \"ON\", \"JSON\",\n\t\t\t\t\t\t\"PREFIX\", \"1\", \"person:\", \"SCHEMA\",\n\t\t\t\t\t\t\"$.person.name\", \"AS\", \"name\", \"TEXT\",\n\t\t\t\t\t\t\"$.person.age\", \"AS\", \"age\", \"NUMERIC\",\n\t\t\t\t\t\t\"$.person.address.city\", \"AS\", \"city\", \"TEXT\",\n\t\t\t\t\t\t\"$.person.contacts[*].value\", \"AS\", \"contact_value\", \"TEXT\",\n\t\t\t\t\t)\n\t\t\t\t\tExpect(createIndexCmd.Err()).NotTo(HaveOccurred(), \"FT.CREATE failed\")\n\n\t\t\t\t\tsearchCmd := client.FTSearchWithArgs(ctx, \"person_idx\", \"@contact_value:(alice\\\\@example\\\\.com alice_wonder)\", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: \"$.person.name\"}, {FieldName: \"$.person.age\"}, {FieldName: \"$.person.address.city\"}}})\n\t\t\t\t\tsearchResult, err := searchCmd.Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred(), \"FT.SEARCH failed\")\n\t\t\t\t\tGinkgoWriter.Printf(\"Advanced Search result: %+v\\n\", searchResult)\n\n\t\t\t\t\tincrCmd := client.JSONNumIncrBy(ctx, \"person:1\", \"$.person.age\", 5)\n\t\t\t\t\tincrResult, err := incrCmd.Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred(), \"JSON.NUMINCRBY failed\")\n\t\t\t\t\tExpect(incrResult).To(Equal(\"[35]\"), \"Age increment mismatch\")\n\n\t\t\t\t\tdelCmd := client.JSONDel(ctx, \"person:1\", \"$.settings.notifications.email\")\n\t\t\t\t\tExpect(delCmd.Err()).NotTo(HaveOccurred(), \"JSON.DEL failed\")\n\n\t\t\t\t\ttypeCmd = client.JSONType(ctx, \"person:1\", \"$.settings.notifications.email\")\n\t\t\t\t\ttypeResult, err := typeCmd.Result()\n\t\t\t\t\tExpect(err).ToNot(HaveOccurred())\n\t\t\t\t\t// After deletion, JSON.TYPE returns empty slice or slice with empty/nil value\n\t\t\t\t\tif len(typeResult) > 0 {\n\t\t\t\t\t\tExpect(typeResult[0]).To(Or(BeNil(), BeEmpty()), \"Expected JSON.TYPE to be empty for deleted field\")\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t})\n})\n\n// Helper function to marshal data into JSON for comparisons\nfunc jsonMustMarshal(v interface{}) string {\n\tbytes, err := json.Marshal(v)\n\tExpect(err).NotTo(HaveOccurred())\n\treturn string(bytes)\n}\n"
  },
  {
    "path": "list_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype ListCmdable interface {\n\tBLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd\n\tBLMPop(ctx context.Context, timeout time.Duration, direction string, count int64, keys ...string) *KeyValuesCmd\n\tBRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd\n\tBRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd\n\tLIndex(ctx context.Context, key string, index int64) *StringCmd\n\tLInsert(ctx context.Context, key, op string, pivot, value interface{}) *IntCmd\n\tLInsertBefore(ctx context.Context, key string, pivot, value interface{}) *IntCmd\n\tLInsertAfter(ctx context.Context, key string, pivot, value interface{}) *IntCmd\n\tLLen(ctx context.Context, key string) *IntCmd\n\tLMPop(ctx context.Context, direction string, count int64, keys ...string) *KeyValuesCmd\n\tLPop(ctx context.Context, key string) *StringCmd\n\tLPopCount(ctx context.Context, key string, count int) *StringSliceCmd\n\tLPos(ctx context.Context, key string, value string, args LPosArgs) *IntCmd\n\tLPosCount(ctx context.Context, key string, value string, count int64, args LPosArgs) *IntSliceCmd\n\tLPush(ctx context.Context, key string, values ...interface{}) *IntCmd\n\tLPushX(ctx context.Context, key string, values ...interface{}) *IntCmd\n\tLRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd\n\tLRem(ctx context.Context, key string, count int64, value interface{}) *IntCmd\n\tLSet(ctx context.Context, key string, index int64, value interface{}) *StatusCmd\n\tLTrim(ctx context.Context, key string, start, stop int64) *StatusCmd\n\tRPop(ctx context.Context, key string) *StringCmd\n\tRPopCount(ctx context.Context, key string, count int) *StringSliceCmd\n\tRPopLPush(ctx context.Context, source, destination string) *StringCmd\n\tRPush(ctx context.Context, key string, values ...interface{}) *IntCmd\n\tRPushX(ctx context.Context, key string, values ...interface{}) *IntCmd\n\tLMove(ctx context.Context, source, destination, srcpos, destpos string) *StringCmd\n\tBLMove(ctx context.Context, source, destination, srcpos, destpos string, timeout time.Duration) *StringCmd\n}\n\nfunc (c cmdable) BLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd {\n\targs := make([]interface{}, 1+len(keys)+1)\n\targs[0] = \"blpop\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\targs[len(args)-1] = formatSec(ctx, timeout)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) BLMPop(ctx context.Context, timeout time.Duration, direction string, count int64, keys ...string) *KeyValuesCmd {\n\targs := make([]interface{}, 3+len(keys), 6+len(keys))\n\targs[0] = \"blmpop\"\n\targs[1] = formatSec(ctx, timeout)\n\targs[2] = len(keys)\n\tfor i, key := range keys {\n\t\targs[3+i] = key\n\t}\n\targs = append(args, strings.ToLower(direction), \"count\", count)\n\tcmd := NewKeyValuesCmd(ctx, args...)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) BRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd {\n\targs := make([]interface{}, 1+len(keys)+1)\n\targs[0] = \"brpop\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\targs[len(keys)+1] = formatSec(ctx, timeout)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BRPopLPush pops an element from a list, pushes it to another list and returns it.\n// Blocks until an element is available or timeout is reached.\n//\n// Deprecated: Use BLMove with RIGHT and LEFT arguments instead as of Redis 6.2.0.\nfunc (c cmdable) BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd {\n\tcmd := NewStringCmd(\n\t\tctx,\n\t\t\"brpoplpush\",\n\t\tsource,\n\t\tdestination,\n\t\tformatSec(ctx, timeout),\n\t)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LIndex(ctx context.Context, key string, index int64) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"lindex\", key, index)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// LMPop Pops one or more elements from the first non-empty list key from the list of provided key names.\n// direction: left or right, count: > 0\n// example: client.LMPop(ctx, \"left\", 3, \"key1\", \"key2\")\nfunc (c cmdable) LMPop(ctx context.Context, direction string, count int64, keys ...string) *KeyValuesCmd {\n\targs := make([]interface{}, 2+len(keys), 5+len(keys))\n\targs[0] = \"lmpop\"\n\targs[1] = len(keys)\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\targs = append(args, strings.ToLower(direction), \"count\", count)\n\tcmd := NewKeyValuesCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LInsert(ctx context.Context, key, op string, pivot, value interface{}) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"linsert\", key, op, pivot, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LInsertBefore(ctx context.Context, key string, pivot, value interface{}) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"linsert\", key, \"before\", pivot, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LInsertAfter(ctx context.Context, key string, pivot, value interface{}) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"linsert\", key, \"after\", pivot, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LLen(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"llen\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LPop(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"lpop\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LPopCount(ctx context.Context, key string, count int) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"lpop\", key, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype LPosArgs struct {\n\tRank, MaxLen int64\n}\n\nfunc (c cmdable) LPos(ctx context.Context, key string, value string, a LPosArgs) *IntCmd {\n\targs := []interface{}{\"lpos\", key, value}\n\tif a.Rank != 0 {\n\t\targs = append(args, \"rank\", a.Rank)\n\t}\n\tif a.MaxLen != 0 {\n\t\targs = append(args, \"maxlen\", a.MaxLen)\n\t}\n\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LPosCount(ctx context.Context, key string, value string, count int64, a LPosArgs) *IntSliceCmd {\n\targs := []interface{}{\"lpos\", key, value, \"count\", count}\n\tif a.Rank != 0 {\n\t\targs = append(args, \"rank\", a.Rank)\n\t}\n\tif a.MaxLen != 0 {\n\t\targs = append(args, \"maxlen\", a.MaxLen)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LPush(ctx context.Context, key string, values ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"lpush\"\n\targs[1] = key\n\targs = appendArgs(args, values)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LPushX(ctx context.Context, key string, values ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"lpushx\"\n\targs[1] = key\n\targs = appendArgs(args, values)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(\n\t\tctx,\n\t\t\"lrange\",\n\t\tkey,\n\t\tstart,\n\t\tstop,\n\t)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LRem(ctx context.Context, key string, count int64, value interface{}) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"lrem\", key, count, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LSet(ctx context.Context, key string, index int64, value interface{}) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"lset\", key, index, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LTrim(ctx context.Context, key string, start, stop int64) *StatusCmd {\n\tcmd := NewStatusCmd(\n\t\tctx,\n\t\t\"ltrim\",\n\t\tkey,\n\t\tstart,\n\t\tstop,\n\t)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) RPop(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"rpop\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) RPopCount(ctx context.Context, key string, count int) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"rpop\", key, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// RPopLPush atomically returns and removes the last element of the source list,\n// and pushes the element as the first element of the destination list.\n//\n// Deprecated: Use LMove with RIGHT and LEFT arguments instead as of Redis 6.2.0.\nfunc (c cmdable) RPopLPush(ctx context.Context, source, destination string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"rpoplpush\", source, destination)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) RPush(ctx context.Context, key string, values ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"rpush\"\n\targs[1] = key\n\targs = appendArgs(args, values)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) RPushX(ctx context.Context, key string, values ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(values))\n\targs[0] = \"rpushx\"\n\targs[1] = key\n\targs = appendArgs(args, values)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) LMove(ctx context.Context, source, destination, srcpos, destpos string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"lmove\", source, destination, srcpos, destpos)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) BLMove(\n\tctx context.Context, source, destination, srcpos, destpos string, timeout time.Duration,\n) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"blmove\", source, destination, srcpos, destpos, formatSec(ctx, timeout))\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "logging/logging.go",
    "content": "// Package logging provides logging level constants and utilities for the go-redis library.\n// This package centralizes logging configuration to ensure consistency across all components.\npackage logging\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n)\n\ntype LogLevelT = internal.LogLevelT\n\nconst (\n\tLogLevelError = internal.LogLevelError\n\tLogLevelWarn  = internal.LogLevelWarn\n\tLogLevelInfo  = internal.LogLevelInfo\n\tLogLevelDebug = internal.LogLevelDebug\n)\n\n// VoidLogger is a logger that does nothing.\n// Used to disable logging and thus speed up the library.\ntype VoidLogger struct{}\n\nfunc (v *VoidLogger) Printf(_ context.Context, _ string, _ ...interface{}) {\n\t// do nothing\n}\n\n// Disable disables logging by setting the internal logger to a void logger.\n// This can be used to speed up the library if logging is not needed.\n// It will override any custom logger that was set before and set the VoidLogger.\nfunc Disable() {\n\tinternal.Logger = &VoidLogger{}\n}\n\n// Enable enables logging by setting the internal logger to the default logger.\n// This is the default behavior.\n// You can use redis.SetLogger to set a custom logger.\n//\n// NOTE: This function is not thread-safe.\n// It will override any custom logger that was set before and set the DefaultLogger.\nfunc Enable() {\n\tinternal.Logger = internal.NewDefaultLogger()\n}\n\n// SetLogLevel sets the log level for the library.\nfunc SetLogLevel(logLevel LogLevelT) {\n\tinternal.LogLevel = logLevel\n}\n\n// NewBlacklistLogger returns a new logger that filters out messages containing any of the substrings.\n// This can be used to filter out messages containing sensitive information.\nfunc NewBlacklistLogger(substr []string) internal.Logging {\n\tl := internal.NewDefaultLogger()\n\treturn &filterLogger{logger: l, substr: substr, blacklist: true}\n}\n\n// NewWhitelistLogger returns a new logger that only logs messages containing any of the substrings.\n// This can be used to only log messages related to specific commands or patterns.\nfunc NewWhitelistLogger(substr []string) internal.Logging {\n\tl := internal.NewDefaultLogger()\n\treturn &filterLogger{logger: l, substr: substr, blacklist: false}\n}\n\ntype filterLogger struct {\n\tlogger    internal.Logging\n\tblacklist bool\n\tsubstr    []string\n}\n\nfunc (l *filterLogger) Printf(ctx context.Context, format string, v ...interface{}) {\n\tmsg := fmt.Sprintf(format, v...)\n\tfound := false\n\tfor _, substr := range l.substr {\n\t\tif strings.Contains(msg, substr) {\n\t\t\tfound = true\n\t\t\tif l.blacklist {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\t// whitelist, only log if one of the substrings is present\n\tif !l.blacklist && !found {\n\t\treturn\n\t}\n\tif l.logger != nil {\n\t\tl.logger.Printf(ctx, format, v...)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "logging/logging_test.go",
    "content": "package logging\n\nimport \"testing\"\n\nfunc TestLogLevel_String(t *testing.T) {\n\ttests := []struct {\n\t\tlevel    LogLevelT\n\t\texpected string\n\t}{\n\t\t{LogLevelError, \"ERROR\"},\n\t\t{LogLevelWarn, \"WARN\"},\n\t\t{LogLevelInfo, \"INFO\"},\n\t\t{LogLevelDebug, \"DEBUG\"},\n\t\t{LogLevelT(99), \"UNKNOWN\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tif got := test.level.String(); got != test.expected {\n\t\t\tt.Errorf(\"LogLevel(%d).String() = %q, want %q\", test.level, got, test.expected)\n\t\t}\n\t}\n}\n\nfunc TestLogLevel_IsValid(t *testing.T) {\n\ttests := []struct {\n\t\tlevel    LogLevelT\n\t\texpected bool\n\t}{\n\t\t{LogLevelError, true},\n\t\t{LogLevelWarn, true},\n\t\t{LogLevelInfo, true},\n\t\t{LogLevelDebug, true},\n\t\t{LogLevelT(-1), false},\n\t\t{LogLevelT(4), false},\n\t\t{LogLevelT(99), false},\n\t}\n\n\tfor _, test := range tests {\n\t\tif got := test.level.IsValid(); got != test.expected {\n\t\t\tt.Errorf(\"LogLevel(%d).IsValid() = %v, want %v\", test.level, got, test.expected)\n\t\t}\n\t}\n}\n\nfunc TestLogLevelConstants(t *testing.T) {\n\t// Test that constants have expected values\n\tif LogLevelError != 0 {\n\t\tt.Errorf(\"LogLevelError = %d, want 0\", LogLevelError)\n\t}\n\tif LogLevelWarn != 1 {\n\t\tt.Errorf(\"LogLevelWarn = %d, want 1\", LogLevelWarn)\n\t}\n\tif LogLevelInfo != 2 {\n\t\tt.Errorf(\"LogLevelInfo = %d, want 2\", LogLevelInfo)\n\t}\n\tif LogLevelDebug != 3 {\n\t\tt.Errorf(\"LogLevelDebug = %d, want 3\", LogLevelDebug)\n\t}\n}\n"
  },
  {
    "path": "main_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/logging\"\n)\n\nconst (\n\tredisSecondaryPort = \"6381\"\n)\n\nconst (\n\tringShard1Port = \"6390\"\n\tringShard2Port = \"6391\"\n\tringShard3Port = \"6392\"\n)\n\nconst (\n\tsentinelName       = \"go-redis-test\"\n\tsentinelMasterPort = \"9121\"\n\tsentinelSlave1Port = \"9122\"\n\tsentinelSlave2Port = \"9123\"\n\tsentinelPort1      = \"26379\"\n\tsentinelPort2      = \"26380\"\n\tsentinelPort3      = \"26381\"\n)\n\nvar (\n\tredisPort = \"6380\"\n\tredisAddr = \":\" + redisPort\n)\n\nvar (\n\tredisStackPort = \"6379\"\n\tredisStackAddr = \":\" + redisStackPort\n)\n\nvar (\n\tsentinelAddrs = []string{\"127.0.0.1:\" + sentinelPort1, \"127.0.0.1:\" + sentinelPort2, \"127.0.0.1:\" + sentinelPort3}\n\n\tringShard1, ringShard2, ringShard3             *redis.Client\n\tsentinelMaster, sentinelSlave1, sentinelSlave2 *redis.Client\n\tsentinel1, sentinel2, sentinel3                *redis.Client\n)\n\nvar cluster = &clusterScenario{\n\tports:   []string{\"16600\", \"16601\", \"16602\", \"16603\", \"16604\", \"16605\"},\n\tnodeIDs: make([]string, 6),\n\tclients: make(map[string]*redis.Client, 6),\n}\n\nvar tlsCluster = &clusterScenario{\n\tports:   []string{\"5430\", \"5431\", \"5432\", \"5433\", \"5434\", \"5435\"},\n\tnodeIDs: make([]string, 6),\n\tclients: make(map[string]*redis.Client, 6),\n}\n\n// Redis Software Cluster\nvar RECluster = false\n\n// Redis Community Edition Docker\nvar RCEDocker = false\n\n// Notes version of redis we are executing tests against.\n// This can be used before we change the bsm fork of ginkgo for one,\n// which have support for label sets, so we can filter tests per redis version.\nvar RedisVersion float64 = 8.4\n\nfunc SkipBeforeRedisVersion(version float64, msg string) {\n\tif RedisVersion < version {\n\t\tSkip(fmt.Sprintf(\"(redis version < %f) %s\", version, msg))\n\t}\n}\n\nfunc SkipAfterRedisVersion(version float64, msg string) {\n\tif RedisVersion > version {\n\t\tSkip(fmt.Sprintf(\"(redis version > %f) %s\", version, msg))\n\t}\n}\n\nvar _ = BeforeSuite(func() {\n\taddr := os.Getenv(\"REDIS_PORT\")\n\tif addr != \"\" {\n\t\tredisPort = addr\n\t\tredisAddr = \":\" + redisPort\n\t}\n\tvar err error\n\tRECluster, _ = strconv.ParseBool(os.Getenv(\"RE_CLUSTER\"))\n\tRCEDocker, _ = strconv.ParseBool(os.Getenv(\"RCE_DOCKER\"))\n\n\tRedisVersion, _ = strconv.ParseFloat(strings.Trim(os.Getenv(\"REDIS_VERSION\"), \"\\\"\"), 64)\n\n\tif RedisVersion == 0 {\n\t\tRedisVersion = 8.4\n\t}\n\n\tfmt.Printf(\"RECluster: %v\\n\", RECluster)\n\tfmt.Printf(\"RCEDocker: %v\\n\", RCEDocker)\n\tfmt.Printf(\"REDIS_VERSION: %.1f\\n\", RedisVersion)\n\tfmt.Printf(\"CLIENT_LIBS_TEST_IMAGE: %v\\n\", os.Getenv(\"CLIENT_LIBS_TEST_IMAGE\"))\n\tlogging.Disable()\n\n\tif RedisVersion < 7.0 || RedisVersion > 9 {\n\t\tpanic(\"incorrect or not supported redis version\")\n\t}\n\n\tredisPort = redisStackPort\n\tredisAddr = redisStackAddr\n\tif !RECluster {\n\t\tringShard1, err = connectTo(ringShard1Port)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tringShard2, err = connectTo(ringShard2Port)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tringShard3, err = connectTo(ringShard3Port)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tsentinelMaster, err = connectTo(sentinelMasterPort)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tsentinel1, err = startSentinel(sentinelPort1, sentinelName, sentinelMasterPort)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tsentinel2, err = startSentinel(sentinelPort2, sentinelName, sentinelMasterPort)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tsentinel3, err = startSentinel(sentinelPort3, sentinelName, sentinelMasterPort)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tsentinelSlave1, err = connectTo(sentinelSlave1Port)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\terr = sentinelSlave1.ReplicaOf(ctx, \"127.0.0.1\", sentinelMasterPort).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tsentinelSlave2, err = connectTo(sentinelSlave2Port)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\terr = sentinelSlave2.ReplicaOf(ctx, \"127.0.0.1\", sentinelMasterPort).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// populate cluster node information\n\t\tExpect(configureClusterTopology(ctx, cluster)).NotTo(HaveOccurred())\n\n\t\t// Initialize TLS cluster if available\n\t\t_ = initializeTLSCluster(ctx)\n\t}\n})\n\nvar _ = AfterSuite(func() {\n\tif !RECluster {\n\t\tExpect(cluster.Close()).NotTo(HaveOccurred())\n\t\tcleanupTLSCluster()\n\t}\n})\n\nfunc TestGinkgoSuite(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"go-redis\")\n}\n\n//------------------------------------------------------------------------------\n\nfunc redisOptions() *redis.Options {\n\tif RECluster {\n\t\treturn &redis.Options{\n\t\t\tAddr: redisAddr,\n\t\t\tDB:   0,\n\n\t\t\tDialTimeout:           10 * time.Second,\n\t\t\tReadTimeout:           30 * time.Second,\n\t\t\tWriteTimeout:          30 * time.Second,\n\t\t\tContextTimeoutEnabled: true,\n\n\t\t\tMaxRetries: -1,\n\n\t\t\tPoolSize:        10,\n\t\t\tPoolTimeout:     30 * time.Second,\n\t\t\tConnMaxIdleTime: time.Minute,\n\t\t}\n\t}\n\treturn &redis.Options{\n\t\tAddr: redisAddr,\n\t\tDB:   0,\n\n\t\tDialTimeout:           10 * time.Second,\n\t\tReadTimeout:           30 * time.Second,\n\t\tWriteTimeout:          30 * time.Second,\n\t\tContextTimeoutEnabled: true,\n\n\t\tMaxRetries: -1,\n\n\t\tPoolSize:        10,\n\t\tPoolTimeout:     30 * time.Second,\n\t\tConnMaxIdleTime: time.Minute,\n\t}\n}\n\nfunc redisClusterOptions() *redis.ClusterOptions {\n\treturn &redis.ClusterOptions{\n\t\tDialTimeout:  10 * time.Second,\n\t\tReadTimeout:  30 * time.Second,\n\t\tWriteTimeout: 30 * time.Second,\n\n\t\tMaxRedirects: 8,\n\n\t\tPoolSize:        10,\n\t\tPoolTimeout:     30 * time.Second,\n\t\tConnMaxIdleTime: time.Minute,\n\t}\n}\n\nfunc redisRingOptions() *redis.RingOptions {\n\treturn &redis.RingOptions{\n\t\tAddrs: map[string]string{\n\t\t\t\"ringShardOne\": \":\" + ringShard1Port,\n\t\t\t\"ringShardTwo\": \":\" + ringShard2Port,\n\t\t},\n\n\t\tDialTimeout:  10 * time.Second,\n\t\tReadTimeout:  30 * time.Second,\n\t\tWriteTimeout: 30 * time.Second,\n\n\t\tMaxRetries: -1,\n\n\t\tPoolSize:        10,\n\t\tPoolTimeout:     30 * time.Second,\n\t\tConnMaxIdleTime: time.Minute,\n\t}\n}\n\nfunc performAsync(n int, cbs ...func(int)) *sync.WaitGroup {\n\tvar wg sync.WaitGroup\n\tfor _, cb := range cbs {\n\t\twg.Add(n)\n\t\t// start from 1, so we can skip db 0 where such test is executed with\n\t\t// select db command\n\t\tfor i := 1; i <= n; i++ {\n\t\t\tgo func(cb func(int), i int) {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\tcb(i)\n\t\t\t}(cb, i)\n\t\t}\n\t}\n\treturn &wg\n}\n\nfunc perform(n int, cbs ...func(int)) {\n\twg := performAsync(n, cbs...)\n\twg.Wait()\n}\n\nfunc eventually(fn func() error, timeout time.Duration) error {\n\terrCh := make(chan error, 1)\n\tdone := make(chan struct{})\n\texit := make(chan struct{})\n\n\tgo func() {\n\t\tfor {\n\t\t\terr := fn()\n\t\t\tif err == nil {\n\t\t\t\tclose(done)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tselect {\n\t\t\tcase errCh <- err:\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tselect {\n\t\t\tcase <-exit:\n\t\t\t\treturn\n\t\t\tcase <-time.After(timeout / 100):\n\t\t\t}\n\t\t}\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\treturn nil\n\tcase <-time.After(timeout):\n\t\tclose(exit)\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\treturn err\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"timeout after %s without an error\", timeout)\n\t\t}\n\t}\n}\n\nfunc connectTo(port string) (*redis.Client, error) {\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr:       \"127.0.0.1:\" + port,\n\t\tMaxRetries: -1,\n\t})\n\n\terr := eventually(func() error {\n\t\treturn client.Ping(ctx).Err()\n\t}, 30*time.Second)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n\nfunc startSentinel(port, masterName, masterPort string) (*redis.Client, error) {\n\tclient, err := connectTo(port)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, cmd := range []*redis.StatusCmd{\n\t\tredis.NewStatusCmd(ctx, \"SENTINEL\", \"MONITOR\", masterName, \"127.0.0.1\", masterPort, \"2\"),\n\t} {\n\t\tclient.Process(ctx, cmd)\n\t\tif err := cmd.Err(); err != nil && !strings.Contains(err.Error(), \"ERR Duplicate master name.\") {\n\t\t\treturn nil, fmt.Errorf(\"%s failed: %w\", cmd, err)\n\t\t}\n\t}\n\n\treturn client, nil\n}\n\n//------------------------------------------------------------------------------\n\ntype badConnError string\n\nfunc (e badConnError) Error() string   { return string(e) }\nfunc (e badConnError) Timeout() bool   { return true }\nfunc (e badConnError) Temporary() bool { return false }\n\ntype badConn struct {\n\tnet.TCPConn\n\n\treadDelay, writeDelay time.Duration\n\treadErr, writeErr     error\n}\n\nvar _ net.Conn = &badConn{}\n\nfunc (cn *badConn) SetReadDeadline(t time.Time) error {\n\treturn nil\n}\n\nfunc (cn *badConn) SetWriteDeadline(t time.Time) error {\n\treturn nil\n}\n\nfunc (cn *badConn) Read([]byte) (int, error) {\n\tif cn.readDelay != 0 {\n\t\ttime.Sleep(cn.readDelay)\n\t}\n\tif cn.readErr != nil {\n\t\treturn 0, cn.readErr\n\t}\n\treturn 0, badConnError(\"bad connection\")\n}\n\nfunc (cn *badConn) Write([]byte) (int, error) {\n\tif cn.writeDelay != 0 {\n\t\ttime.Sleep(cn.writeDelay)\n\t}\n\tif cn.writeErr != nil {\n\t\treturn 0, cn.writeErr\n\t}\n\treturn 0, badConnError(\"bad connection\")\n}\n\n//------------------------------------------------------------------------------\n\ntype hook struct {\n\tdialHook            func(hook redis.DialHook) redis.DialHook\n\tprocessHook         func(hook redis.ProcessHook) redis.ProcessHook\n\tprocessPipelineHook func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook\n}\n\nfunc (h *hook) DialHook(hook redis.DialHook) redis.DialHook {\n\tif h.dialHook != nil {\n\t\treturn h.dialHook(hook)\n\t}\n\treturn hook\n}\n\nfunc (h *hook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {\n\tif h.processHook != nil {\n\t\treturn h.processHook(hook)\n\t}\n\treturn hook\n}\n\nfunc (h *hook) ProcessPipelineHook(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\tif h.processPipelineHook != nil {\n\t\treturn h.processPipelineHook(hook)\n\t}\n\treturn hook\n}\n\n// loadClusterTLSConfig loads TLS certificates for cluster tests\nfunc loadClusterTLSConfig(certDir string) (*tls.Config, error) {\n\t// Check if cert directory exists\n\tif _, err := os.Stat(certDir); os.IsNotExist(err) {\n\t\treturn nil, fmt.Errorf(\"certificate directory does not exist: %s\", certDir)\n\t}\n\n\t// Load CA cert\n\tcaCert, err := os.ReadFile(filepath.Join(certDir, \"ca.crt\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcaCertPool := x509.NewCertPool()\n\tcaCertPool.AppendCertsFromPEM(caCert)\n\n\t// Load client cert and key\n\tcert, err := tls.LoadX509KeyPair(\n\t\tfilepath.Join(certDir, \"client.crt\"),\n\t\tfilepath.Join(certDir, \"client.key\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &tls.Config{\n\t\tRootCAs:            caCertPool,\n\t\tCertificates:       []tls.Certificate{cert},\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true,\n\t}, nil\n}\n\n// initializeTLSCluster initializes the TLS cluster for testing\nfunc initializeTLSCluster(ctx context.Context) error {\n\t// Load TLS config\n\tcertDir := \"dockers/osscluster-tls/tls\"\n\t_, err := loadClusterTLSConfig(certDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load TLS config: %w\", err)\n\t}\n\n\t// The TLS cluster is auto-created by the container (REDIS_CLUSTER=yes)\n\t// Just verify it's ready by checking cluster info\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr: net.JoinHostPort(\"127.0.0.1\", tlsCluster.ports[0]),\n\t\tTLSConfig: &tls.Config{\n\t\t\tInsecureSkipVerify: true,\n\t\t},\n\t})\n\tdefer client.Close()\n\n\t// Wait for cluster to be ready\n\terr = eventually(func() error {\n\t\tinfo, err := client.ClusterInfo(ctx).Result()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get cluster info: %w\", err)\n\t\t}\n\t\tif !strings.Contains(info, \"cluster_state:ok\") {\n\t\t\treturn fmt.Errorf(\"cluster not ready: %s\", info)\n\t\t}\n\t\treturn nil\n\t}, 30*time.Second)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"TLS cluster not ready: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// cleanupTLSCluster cleans up TLS cluster resources\nfunc cleanupTLSCluster() {\n\t// TLS cluster is auto-managed by the container, no cleanup needed\n}\n"
  },
  {
    "path": "maintnotifications/FEATURES.md",
    "content": "# Maintenance Notifications - FEATURES\n\n## Overview\n\nThe Maintenance Notifications feature enables seamless Redis connection handoffs during cluster maintenance operations without dropping active connections. This feature leverages Redis RESP3 push notifications to provide zero-downtime maintenance for Redis Enterprise and compatible Redis deployments.\n\n## Important\n\nUsing Maintenance Notifications may affect the read and write timeouts by relaxing them during maintenance operations.\nThis is necessary to prevent false failures due to increased latency during handoffs. The relaxed timeouts are automatically applied and removed as needed.\n\n## Key Features\n\n### Seamless Connection Handoffs\n- **Zero-Downtime Maintenance**: Automatically handles connection transitions during cluster operations\n- **Active Operation Preservation**: Transfers in-flight operations to new connections without interruption\n- **Graceful Degradation**: Falls back to standard reconnection if handoff fails\n\n### Push Notification Support\nSupports all Redis Enterprise maintenance notification types:\n- **MOVING** - Slot moving to a new node\n- **MIGRATING** - Slot in migration state\n- **MIGRATED** - Migration completed\n- **FAILING_OVER** - Node failing over\n- **FAILED_OVER** - Failover completed\n\n### Circuit Breaker Pattern\n- **Endpoint-Specific Failure Tracking**: Prevents repeated connection attempts to failing endpoints\n- **Automatic Recovery Testing**: Half-open state allows gradual recovery validation\n- **Configurable Thresholds**: Customize failure thresholds and reset timeouts\n\n### Flexible Configuration\n- **Auto-Detection Mode**: Automatically detects server support for maintenance notifications\n- **Multiple Endpoint Types**: Support for internal/external IP/FQDN endpoint resolution\n- **Auto-Scaling Workers**: Automatically sizes worker pool based on connection pool size\n- **Timeout Management**: Separate timeouts for relaxed (during maintenance) and normal operations\n\n### Extensible Hook System\n- **Pre/Post Processing Hooks**: Monitor and customize notification handling\n- **Built-in Hooks**: Logging and metrics collection hooks included\n- **Custom Hook Support**: Implement custom business logic around maintenance events\n\n### Comprehensive Monitoring\n- **Metrics Collection**: Track notification counts, processing times, and error rates\n- **Circuit Breaker Stats**: Monitor endpoint health and circuit breaker states\n- **Operation Tracking**: Track active handoff operations and their lifecycle\n\n## Architecture Highlights\n\n### Event-Driven Handoff System\n- **Asynchronous Processing**: Non-blocking handoff operations using worker pool pattern\n- **Queue-Based Architecture**: Configurable queue size with auto-scaling support\n- **Retry Mechanism**: Configurable retry attempts with exponential backoff\n\n### Connection Pool Integration\n- **Pool Hook Interface**: Seamless integration with go-redis connection pool\n- **Connection State Management**: Atomic flags for connection usability tracking\n- **Graceful Shutdown**: Ensures all in-flight handoffs complete before shutdown\n\n### Thread-Safe Design\n- **Lock-Free Operations**: Atomic operations for high-performance state tracking\n- **Concurrent-Safe Maps**: sync.Map for tracking active operations\n- **Minimal Lock Contention**: Read-write locks only where necessary\n\n## Configuration Options\n\n### Operation Modes\n- **`ModeDisabled`**: Maintenance notifications completely disabled\n- **`ModeEnabled`**: Forcefully enabled (fails if server doesn't support)\n- **`ModeAuto`**: Auto-detect server support (recommended default)\n\n### Endpoint Types\n- **`EndpointTypeAuto`**: Auto-detect based on current connection\n- **`EndpointTypeInternalIP`**: Use internal IP addresses\n- **`EndpointTypeInternalFQDN`**: Use internal fully qualified domain names\n- **`EndpointTypeExternalIP`**: Use external IP addresses\n- **`EndpointTypeExternalFQDN`**: Use external fully qualified domain names\n- **`EndpointTypeNone`**: No endpoint (reconnect with current configuration)\n\n### Timeout Configuration\n- **`RelaxedTimeout`**: Extended timeout during maintenance operations (default: 10s)\n- **`HandoffTimeout`**: Maximum time for handoff completion (default: 15s)\n- **`PostHandoffRelaxedDuration`**: Relaxed period after handoff (default: 2×RelaxedTimeout)\n\n### Worker Pool Configuration\n- **`MaxWorkers`**: Maximum concurrent handoff workers (auto-calculated if 0)\n- **`HandoffQueueSize`**: Handoff queue capacity (auto-calculated if 0)\n- **`MaxHandoffRetries`**: Maximum retry attempts for failed handoffs (default: 3)\n\n### Circuit Breaker Configuration\n- **`CircuitBreakerFailureThreshold`**: Failures before opening circuit (default: 5)\n- **`CircuitBreakerResetTimeout`**: Time before testing recovery (default: 60s)\n- **`CircuitBreakerMaxRequests`**: Max requests in half-open state (default: 3)\n\n## Auto-Scaling Formulas\n\n### Worker Pool Sizing\nWhen `MaxWorkers = 0` (auto-calculate):\n```\nMaxWorkers = min(PoolSize/2, max(10, PoolSize/3))\n```\n\n### Queue Sizing\nWhen `HandoffQueueSize = 0` (auto-calculate):\n```\nQueueSize = max(20 × MaxWorkers, PoolSize)\nCapped by: min(MaxActiveConns + 1, 5 × PoolSize)\n```\n\n### Examples\n- **Pool Size 100**: 33 workers, 660 queue (capped at 500)\n- **Pool Size 100 + MaxActiveConns 150**: 33 workers, 151 queue\n- **Pool Size 50**: 16 workers, 320 queue (capped at 250)\n\n## Performance Characteristics\n\n### Throughput\n- **Non-Blocking Handoffs**: Client operations continue during handoffs\n- **Concurrent Processing**: Multiple handoffs processed in parallel\n- **Minimal Overhead**: Lock-free atomic operations for state tracking\n\n### Latency\n- **Relaxed Timeouts**: Extended timeouts during maintenance prevent false failures\n- **Fast Path**: Connections not undergoing handoff have zero overhead\n- **Graceful Degradation**: Failed handoffs fall back to standard reconnection\n\n### Resource Usage\n- **Memory Efficient**: Bounded queue sizes prevent memory exhaustion\n- **Worker Pool**: Fixed worker count prevents goroutine explosion\n- **Connection Reuse**: Handoff reuses existing connection objects\n\n## Testing\n\n### Unit Tests\n- Comprehensive unit test coverage for all components\n- Mock-based testing for isolation\n- Concurrent operation testing\n\n### Integration Tests\n- Pool integration tests with real connection handoffs\n- Circuit breaker behavior validation\n- Hook system integration testing\n\n### E2E Tests\n- Real Redis Enterprise cluster testing\n- Multiple scenario coverage (timeouts, endpoint types, stress tests)\n- Fault injection testing\n- TLS configuration testing\n\n## Compatibility\n\n### Requirements\n- **Redis Protocol**: RESP3 required for push notifications\n- **Redis Version**: Redis Enterprise or compatible Redis with maintenance notifications\n- **Go Version**: Go 1.18+ (uses generics and atomic types)\n\n### Client Support\n#### Currently Supported\n- **Standalone Client** (`redis.NewClient`) - Full support for MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER notifications\n- **Cluster Client** (`redis.NewClusterClient`) - Support for SMIGRATING and SMIGRATED notifications for hitless slot migrations\n\n#### Will Not Support\n- **Failover Client** (no planned support)\n- **Ring Client** (no planned support)\n\n## Migration Guide\n\n### Enabling Maintenance Notifications (Standalone Client)\n\n**Before:**\n```go\nclient := redis.NewClient(&redis.Options{\n    Addr:     \"localhost:6379\",\n    Protocol: 2, // RESP2\n})\n```\n\n**After:**\n```go\nclient := redis.NewClient(&redis.Options{\n    Addr:     \"localhost:6379\",\n    Protocol: 3, // RESP3 required\n    MaintNotificationsConfig: &maintnotifications.Config{\n        Mode: maintnotifications.ModeAuto,\n    },\n})\n```\n\n### Enabling Hitless Upgrades (Cluster Client)\n\nFor Redis Cluster with hitless slot migration support:\n\n```go\nclient := redis.NewClusterClient(&redis.ClusterOptions{\n    Addrs:    []string{\"localhost:7000\", \"localhost:7001\", \"localhost:7002\"},\n    Protocol: 3, // RESP3 required for push notifications\n    MaintNotificationsConfig: &maintnotifications.Config{\n        Mode:           maintnotifications.ModeAuto,\n        RelaxedTimeout: 10 * time.Second, // Extended timeout during slot migrations\n    },\n})\n```\n\nThe cluster client automatically handles:\n- **SMIGRATING**: Relaxes timeouts when slots are being migrated\n- **SMIGRATED**: Triggers lazy cluster state reload when migration completes\n- **SeqID Deduplication**: Same notification from multiple nodes triggers only one reload\n\n### Adding Monitoring\n\n```go\n// Get the manager from the client\nmanager := client.GetMaintNotificationsManager()\nif manager != nil {\n    // Add logging hook\n    loggingHook := maintnotifications.NewLoggingHook(2) // Info level\n    manager.AddNotificationHook(loggingHook)\n    \n    // Add metrics hook\n    metricsHook := maintnotifications.NewMetricsHook()\n    manager.AddNotificationHook(metricsHook)\n}\n```\n\n## Known Limitations\n\n1. **RESP3 Required**: Push notifications require RESP3 protocol\n2. **Server Support**: Requires Redis Enterprise or compatible Redis with maintenance notifications\n3. **Single Connection Commands**: Some commands (MULTI/EXEC, WATCH) may need special handling\n4. **No Failover/Ring Client Support**: Failover and Ring clients are not supported and there are no plans to add support\n\n## Future Enhancements\n\n- Enhanced metrics and observability\n- TTL-based cleanup for SeqID deduplication map"
  },
  {
    "path": "maintnotifications/README.md",
    "content": "# Maintenance Notifications\n\nSeamless Redis connection handoffs during cluster maintenance operations without dropping connections.\n\n## Cluster Support\n\n**Cluster notifications are now supported for ClusterClient!**\n\n- **SMIGRATING**: `[\"SMIGRATING\", SeqID, slot/range, ...]` - Relaxes timeouts when slots are being migrated\n- **SMIGRATED**: `[\"SMIGRATED\", SeqID, src host:port, dst host:port, slot/range, ...]` - Reloads cluster state when slot migration completes\n\n**Note:** Other maintenance notifications (MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER) are supported only in standalone Redis clients. Cluster clients support SMIGRATING and SMIGRATED for cluster-specific slot migration handling.\n\n## Quick Start\n\n```go\nclient := redis.NewClient(&redis.Options{\n    Addr:     \"localhost:6379\",\n    Protocol: 3, // RESP3 required\n\tMaintNotificationsConfig: &maintnotifications.Config{\n        Mode: maintnotifications.ModeEnabled,\n    },\n})\n```\n\n## Modes\n\n- **`ModeDisabled`** - Maintenance notifications disabled\n- **`ModeEnabled`** - Forcefully enabled (fails if server doesn't support)\n- **`ModeAuto`** - Auto-detect server support (default)\n\n## Configuration\n\n```go\n&maintnotifications.Config{\n    Mode:                       maintnotifications.ModeAuto,\n    EndpointType:               maintnotifications.EndpointTypeAuto,\n    RelaxedTimeout:             10 * time.Second,\n    HandoffTimeout:             15 * time.Second,\n    MaxHandoffRetries:          3,\n    MaxWorkers:                 0,    // Auto-calculated\n    HandoffQueueSize:           0,    // Auto-calculated\n    PostHandoffRelaxedDuration: 0,    // 2 * RelaxedTimeout\n}\n```\n\n### Endpoint Types\n\n- **`EndpointTypeAuto`** - Auto-detect based on connection (default)\n- **`EndpointTypeInternalIP`** - Internal IP address\n- **`EndpointTypeInternalFQDN`** - Internal FQDN\n- **`EndpointTypeExternalIP`** - External IP address\n- **`EndpointTypeExternalFQDN`** - External FQDN\n- **`EndpointTypeNone`** - No endpoint (reconnect with current config)\n\n### Auto-Scaling\n\n**Workers**: `min(PoolSize/2, max(10, PoolSize/3))` when auto-calculated\n**Queue**: `max(20×Workers, PoolSize)` capped by `MaxActiveConns+1` or `5×PoolSize`\n\n**Examples:**\n- Pool 100: 33 workers, 660 queue (capped at 500)\n- Pool 100 + MaxActiveConns 150: 33 workers, 151 queue\n\n## How It Works\n\n1. Redis sends push notifications about cluster maintenance operations\n2. Client creates new connections to updated endpoints\n3. Active operations transfer to new connections\n4. Old connections close gracefully\n\n\n## For more information, see [FEATURES](FEATURES.md)\n"
  },
  {
    "path": "maintnotifications/circuit_breaker.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n)\n\n// CircuitBreakerState represents the state of a circuit breaker\ntype CircuitBreakerState int32\n\nconst (\n\t// CircuitBreakerClosed - normal operation, requests allowed\n\tCircuitBreakerClosed CircuitBreakerState = iota\n\t// CircuitBreakerOpen - failing fast, requests rejected\n\tCircuitBreakerOpen\n\t// CircuitBreakerHalfOpen - testing if service recovered\n\tCircuitBreakerHalfOpen\n)\n\nfunc (s CircuitBreakerState) String() string {\n\tswitch s {\n\tcase CircuitBreakerClosed:\n\t\treturn \"closed\"\n\tcase CircuitBreakerOpen:\n\t\treturn \"open\"\n\tcase CircuitBreakerHalfOpen:\n\t\treturn \"half-open\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// CircuitBreaker implements the circuit breaker pattern for endpoint-specific failure handling\ntype CircuitBreaker struct {\n\t// Configuration\n\tfailureThreshold int           // Number of failures before opening\n\tresetTimeout     time.Duration // How long to stay open before testing\n\tmaxRequests      int           // Max requests allowed in half-open state\n\n\t// State tracking (atomic for lock-free access)\n\tstate           atomic.Int32 // CircuitBreakerState\n\tfailures        atomic.Int64 // Current failure count\n\tsuccesses       atomic.Int64 // Success count in half-open state\n\trequests        atomic.Int64 // Request count in half-open state\n\tlastFailureTime atomic.Int64 // Unix timestamp of last failure\n\tlastSuccessTime atomic.Int64 // Unix timestamp of last success\n\n\t// Endpoint identification\n\tendpoint string\n\tconfig   *Config\n}\n\n// newCircuitBreaker creates a new circuit breaker for an endpoint\nfunc newCircuitBreaker(endpoint string, config *Config) *CircuitBreaker {\n\t// Use configuration values with sensible defaults\n\tfailureThreshold := 5\n\tresetTimeout := 60 * time.Second\n\tmaxRequests := 3\n\n\tif config != nil {\n\t\tfailureThreshold = config.CircuitBreakerFailureThreshold\n\t\tresetTimeout = config.CircuitBreakerResetTimeout\n\t\tmaxRequests = config.CircuitBreakerMaxRequests\n\t}\n\n\treturn &CircuitBreaker{\n\t\tfailureThreshold: failureThreshold,\n\t\tresetTimeout:     resetTimeout,\n\t\tmaxRequests:      maxRequests,\n\t\tendpoint:         endpoint,\n\t\tconfig:           config,\n\t\tstate:            atomic.Int32{}, // Defaults to CircuitBreakerClosed (0)\n\t}\n}\n\n// IsOpen returns true if the circuit breaker is open (rejecting requests)\nfunc (cb *CircuitBreaker) IsOpen() bool {\n\tstate := CircuitBreakerState(cb.state.Load())\n\treturn state == CircuitBreakerOpen\n}\n\n// shouldAttemptReset checks if enough time has passed to attempt reset\nfunc (cb *CircuitBreaker) shouldAttemptReset() bool {\n\tlastFailure := time.Unix(cb.lastFailureTime.Load(), 0)\n\treturn time.Since(lastFailure) >= cb.resetTimeout\n}\n\n// Execute runs the given function with circuit breaker protection\nfunc (cb *CircuitBreaker) Execute(fn func() error) error {\n\t// Single atomic state load for consistency\n\tstate := CircuitBreakerState(cb.state.Load())\n\n\tswitch state {\n\tcase CircuitBreakerOpen:\n\t\tif cb.shouldAttemptReset() {\n\t\t\t// Attempt transition to half-open\n\t\t\tif cb.state.CompareAndSwap(int32(CircuitBreakerOpen), int32(CircuitBreakerHalfOpen)) {\n\t\t\t\tcb.requests.Store(0)\n\t\t\t\tcb.successes.Store(0)\n\t\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.CircuitBreakerTransitioningToHalfOpen(cb.endpoint))\n\t\t\t\t}\n\t\t\t\t// Fall through to half-open logic\n\t\t\t} else {\n\t\t\t\treturn ErrCircuitBreakerOpen\n\t\t\t}\n\t\t} else {\n\t\t\treturn ErrCircuitBreakerOpen\n\t\t}\n\t\tfallthrough\n\tcase CircuitBreakerHalfOpen:\n\t\trequests := cb.requests.Add(1)\n\t\tif requests > int64(cb.maxRequests) {\n\t\t\tcb.requests.Add(-1) // Revert the increment\n\t\t\treturn ErrCircuitBreakerOpen\n\t\t}\n\t}\n\n\t// Execute the function with consistent state\n\terr := fn()\n\n\tif err != nil {\n\t\tcb.recordFailure()\n\t\treturn err\n\t}\n\n\tcb.recordSuccess()\n\treturn nil\n}\n\n// recordFailure records a failure and potentially opens the circuit\nfunc (cb *CircuitBreaker) recordFailure() {\n\tcb.lastFailureTime.Store(time.Now().Unix())\n\tfailures := cb.failures.Add(1)\n\n\tstate := CircuitBreakerState(cb.state.Load())\n\n\tswitch state {\n\tcase CircuitBreakerClosed:\n\t\tif failures >= int64(cb.failureThreshold) {\n\t\t\tif cb.state.CompareAndSwap(int32(CircuitBreakerClosed), int32(CircuitBreakerOpen)) {\n\t\t\t\tif internal.LogLevel.WarnOrAbove() {\n\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.CircuitBreakerOpened(cb.endpoint, failures))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase CircuitBreakerHalfOpen:\n\t\t// Any failure in half-open state immediately opens the circuit\n\t\tif cb.state.CompareAndSwap(int32(CircuitBreakerHalfOpen), int32(CircuitBreakerOpen)) {\n\t\t\tif internal.LogLevel.WarnOrAbove() {\n\t\t\t\tinternal.Logger.Printf(context.Background(), logs.CircuitBreakerReopened(cb.endpoint))\n\t\t\t}\n\t\t}\n\t}\n}\n\n// recordSuccess records a success and potentially closes the circuit\nfunc (cb *CircuitBreaker) recordSuccess() {\n\tcb.lastSuccessTime.Store(time.Now().Unix())\n\n\tstate := CircuitBreakerState(cb.state.Load())\n\n\tswitch state {\n\tcase CircuitBreakerClosed:\n\t\t// Reset failure count on success in closed state\n\t\tcb.failures.Store(0)\n\tcase CircuitBreakerHalfOpen:\n\t\tsuccesses := cb.successes.Add(1)\n\n\t\t// If we've had enough successful requests, close the circuit\n\t\tif successes >= int64(cb.maxRequests) {\n\t\t\tif cb.state.CompareAndSwap(int32(CircuitBreakerHalfOpen), int32(CircuitBreakerClosed)) {\n\t\t\t\tcb.failures.Store(0)\n\t\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.CircuitBreakerClosed(cb.endpoint, successes))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// GetState returns the current state of the circuit breaker\nfunc (cb *CircuitBreaker) GetState() CircuitBreakerState {\n\treturn CircuitBreakerState(cb.state.Load())\n}\n\n// GetStats returns current statistics for monitoring\nfunc (cb *CircuitBreaker) GetStats() CircuitBreakerStats {\n\treturn CircuitBreakerStats{\n\t\tEndpoint:        cb.endpoint,\n\t\tState:           cb.GetState(),\n\t\tFailures:        cb.failures.Load(),\n\t\tSuccesses:       cb.successes.Load(),\n\t\tRequests:        cb.requests.Load(),\n\t\tLastFailureTime: time.Unix(cb.lastFailureTime.Load(), 0),\n\t\tLastSuccessTime: time.Unix(cb.lastSuccessTime.Load(), 0),\n\t}\n}\n\n// CircuitBreakerStats provides statistics about a circuit breaker\ntype CircuitBreakerStats struct {\n\tEndpoint        string\n\tState           CircuitBreakerState\n\tFailures        int64\n\tSuccesses       int64\n\tRequests        int64\n\tLastFailureTime time.Time\n\tLastSuccessTime time.Time\n}\n\n// CircuitBreakerEntry wraps a circuit breaker with access tracking\ntype CircuitBreakerEntry struct {\n\tbreaker    *CircuitBreaker\n\tlastAccess atomic.Int64 // Unix timestamp\n\tcreated    time.Time\n}\n\n// CircuitBreakerManager manages circuit breakers for multiple endpoints\ntype CircuitBreakerManager struct {\n\tbreakers    sync.Map // map[string]*CircuitBreakerEntry\n\tconfig      *Config\n\tcleanupStop chan struct{}\n\tcleanupMu   sync.Mutex\n\tlastCleanup atomic.Int64 // Unix timestamp\n}\n\n// newCircuitBreakerManager creates a new circuit breaker manager\nfunc newCircuitBreakerManager(config *Config) *CircuitBreakerManager {\n\tcbm := &CircuitBreakerManager{\n\t\tconfig:      config,\n\t\tcleanupStop: make(chan struct{}),\n\t}\n\tcbm.lastCleanup.Store(time.Now().Unix())\n\n\t// Start background cleanup goroutine\n\tgo cbm.cleanupLoop()\n\n\treturn cbm\n}\n\n// GetCircuitBreaker returns the circuit breaker for an endpoint, creating it if necessary\nfunc (cbm *CircuitBreakerManager) GetCircuitBreaker(endpoint string) *CircuitBreaker {\n\tnow := time.Now().Unix()\n\n\tif entry, ok := cbm.breakers.Load(endpoint); ok {\n\t\tcbEntry := entry.(*CircuitBreakerEntry)\n\t\tcbEntry.lastAccess.Store(now)\n\t\treturn cbEntry.breaker\n\t}\n\n\t// Create new circuit breaker with metadata\n\tnewBreaker := newCircuitBreaker(endpoint, cbm.config)\n\tnewEntry := &CircuitBreakerEntry{\n\t\tbreaker: newBreaker,\n\t\tcreated: time.Now(),\n\t}\n\tnewEntry.lastAccess.Store(now)\n\n\tactual, _ := cbm.breakers.LoadOrStore(endpoint, newEntry)\n\treturn actual.(*CircuitBreakerEntry).breaker\n}\n\n// GetAllStats returns statistics for all circuit breakers\nfunc (cbm *CircuitBreakerManager) GetAllStats() []CircuitBreakerStats {\n\tvar stats []CircuitBreakerStats\n\tcbm.breakers.Range(func(key, value interface{}) bool {\n\t\tentry := value.(*CircuitBreakerEntry)\n\t\tstats = append(stats, entry.breaker.GetStats())\n\t\treturn true\n\t})\n\treturn stats\n}\n\n// cleanupLoop runs background cleanup of unused circuit breakers\nfunc (cbm *CircuitBreakerManager) cleanupLoop() {\n\tticker := time.NewTicker(5 * time.Minute) // Cleanup every 5 minutes\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tcbm.cleanup()\n\t\tcase <-cbm.cleanupStop:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// cleanup removes circuit breakers that haven't been accessed recently\nfunc (cbm *CircuitBreakerManager) cleanup() {\n\t// Prevent concurrent cleanups\n\tif !cbm.cleanupMu.TryLock() {\n\t\treturn\n\t}\n\tdefer cbm.cleanupMu.Unlock()\n\n\tnow := time.Now()\n\tcutoff := now.Add(-30 * time.Minute).Unix() // 30 minute TTL\n\n\tvar toDelete []string\n\tcount := 0\n\n\tcbm.breakers.Range(func(key, value interface{}) bool {\n\t\tendpoint := key.(string)\n\t\tentry := value.(*CircuitBreakerEntry)\n\n\t\tcount++\n\n\t\t// Remove if not accessed recently\n\t\tif entry.lastAccess.Load() < cutoff {\n\t\t\ttoDelete = append(toDelete, endpoint)\n\t\t}\n\n\t\treturn true\n\t})\n\n\t// Delete expired entries\n\tfor _, endpoint := range toDelete {\n\t\tcbm.breakers.Delete(endpoint)\n\t}\n\n\t// Log cleanup results\n\tif len(toDelete) > 0 && internal.LogLevel.InfoOrAbove() {\n\t\tinternal.Logger.Printf(context.Background(), logs.CircuitBreakerCleanup(len(toDelete), count))\n\t}\n\n\tcbm.lastCleanup.Store(now.Unix())\n}\n\n// Shutdown stops the cleanup goroutine\nfunc (cbm *CircuitBreakerManager) Shutdown() {\n\tclose(cbm.cleanupStop)\n}\n\n// Reset resets all circuit breakers (useful for testing)\nfunc (cbm *CircuitBreakerManager) Reset() {\n\tcbm.breakers.Range(func(key, value interface{}) bool {\n\t\tentry := value.(*CircuitBreakerEntry)\n\t\tbreaker := entry.breaker\n\t\tbreaker.state.Store(int32(CircuitBreakerClosed))\n\t\tbreaker.failures.Store(0)\n\t\tbreaker.successes.Store(0)\n\t\tbreaker.requests.Store(0)\n\t\tbreaker.lastFailureTime.Store(0)\n\t\tbreaker.lastSuccessTime.Store(0)\n\t\treturn true\n\t})\n}\n"
  },
  {
    "path": "maintnotifications/circuit_breaker_test.go",
    "content": "package maintnotifications\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestCircuitBreaker(t *testing.T) {\n\tconfig := &Config{\n\t\tCircuitBreakerFailureThreshold: 5,\n\t\tCircuitBreakerResetTimeout:     60 * time.Second,\n\t\tCircuitBreakerMaxRequests:      3,\n\t}\n\n\tt.Run(\"InitialState\", func(t *testing.T) {\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", config)\n\n\t\tif cb.IsOpen() {\n\t\t\tt.Error(\"Circuit breaker should start in closed state\")\n\t\t}\n\n\t\tif cb.GetState() != CircuitBreakerClosed {\n\t\t\tt.Errorf(\"Expected state %v, got %v\", CircuitBreakerClosed, cb.GetState())\n\t\t}\n\t})\n\n\tt.Run(\"SuccessfulExecution\", func(t *testing.T) {\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", config)\n\n\t\terr := cb.Execute(func() error {\n\t\t\treturn nil // Success\n\t\t})\n\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected no error, got %v\", err)\n\t\t}\n\n\t\tif cb.GetState() != CircuitBreakerClosed {\n\t\t\tt.Errorf(\"Expected state %v, got %v\", CircuitBreakerClosed, cb.GetState())\n\t\t}\n\t})\n\n\tt.Run(\"FailureThreshold\", func(t *testing.T) {\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", config)\n\t\ttestError := errors.New(\"test error\")\n\n\t\t// Fail 4 times (below threshold of 5)\n\t\tfor i := 0; i < 4; i++ {\n\t\t\terr := cb.Execute(func() error {\n\t\t\t\treturn testError\n\t\t\t})\n\t\t\tif err != testError {\n\t\t\t\tt.Errorf(\"Expected test error, got %v\", err)\n\t\t\t}\n\t\t\tif cb.GetState() != CircuitBreakerClosed {\n\t\t\t\tt.Errorf(\"Circuit should still be closed after %d failures\", i+1)\n\t\t\t}\n\t\t}\n\n\t\t// 5th failure should open the circuit\n\t\terr := cb.Execute(func() error {\n\t\t\treturn testError\n\t\t})\n\t\tif err != testError {\n\t\t\tt.Errorf(\"Expected test error, got %v\", err)\n\t\t}\n\n\t\tif cb.GetState() != CircuitBreakerOpen {\n\t\t\tt.Errorf(\"Expected state %v, got %v\", CircuitBreakerOpen, cb.GetState())\n\t\t}\n\t})\n\n\tt.Run(\"OpenCircuitFailsFast\", func(t *testing.T) {\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", config)\n\t\ttestError := errors.New(\"test error\")\n\n\t\t// Force circuit to open\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tcb.Execute(func() error { return testError })\n\t\t}\n\n\t\t// Now it should fail fast\n\t\terr := cb.Execute(func() error {\n\t\t\tt.Error(\"Function should not be called when circuit is open\")\n\t\t\treturn nil\n\t\t})\n\n\t\tif err != ErrCircuitBreakerOpen {\n\t\t\tt.Errorf(\"Expected ErrCircuitBreakerOpen, got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"HalfOpenTransition\", func(t *testing.T) {\n\t\ttestConfig := &Config{\n\t\t\tCircuitBreakerFailureThreshold: 5,\n\t\t\tCircuitBreakerResetTimeout:     100 * time.Millisecond, // Short timeout for testing\n\t\t\tCircuitBreakerMaxRequests:      3,\n\t\t}\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", testConfig)\n\t\ttestError := errors.New(\"test error\")\n\n\t\t// Force circuit to open\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tcb.Execute(func() error { return testError })\n\t\t}\n\n\t\tif cb.GetState() != CircuitBreakerOpen {\n\t\t\tt.Error(\"Circuit should be open\")\n\t\t}\n\n\t\t// Wait for reset timeout\n\t\ttime.Sleep(150 * time.Millisecond)\n\n\t\t// Next call should transition to half-open\n\t\texecuted := false\n\t\terr := cb.Execute(func() error {\n\t\t\texecuted = true\n\t\t\treturn nil // Success\n\t\t})\n\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected no error, got %v\", err)\n\t\t}\n\n\t\tif !executed {\n\t\t\tt.Error(\"Function should have been executed in half-open state\")\n\t\t}\n\t})\n\n\tt.Run(\"HalfOpenToClosedTransition\", func(t *testing.T) {\n\t\ttestConfig := &Config{\n\t\t\tCircuitBreakerFailureThreshold: 5,\n\t\t\tCircuitBreakerResetTimeout:     50 * time.Millisecond,\n\t\t\tCircuitBreakerMaxRequests:      3,\n\t\t}\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", testConfig)\n\t\ttestError := errors.New(\"test error\")\n\n\t\t// Force circuit to open\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tcb.Execute(func() error { return testError })\n\t\t}\n\n\t\t// Wait for reset timeout\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Execute successful requests in half-open state\n\t\tfor i := 0; i < 3; i++ {\n\t\t\terr := cb.Execute(func() error {\n\t\t\t\treturn nil // Success\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Expected no error on attempt %d, got %v\", i+1, err)\n\t\t\t}\n\t\t}\n\n\t\t// Circuit should now be closed\n\t\tif cb.GetState() != CircuitBreakerClosed {\n\t\t\tt.Errorf(\"Expected state %v, got %v\", CircuitBreakerClosed, cb.GetState())\n\t\t}\n\t})\n\n\tt.Run(\"HalfOpenToOpenOnFailure\", func(t *testing.T) {\n\t\ttestConfig := &Config{\n\t\t\tCircuitBreakerFailureThreshold: 5,\n\t\t\tCircuitBreakerResetTimeout:     50 * time.Millisecond,\n\t\t\tCircuitBreakerMaxRequests:      3,\n\t\t}\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", testConfig)\n\t\ttestError := errors.New(\"test error\")\n\n\t\t// Force circuit to open\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tcb.Execute(func() error { return testError })\n\t\t}\n\n\t\t// Wait for reset timeout\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// First request in half-open state fails\n\t\terr := cb.Execute(func() error {\n\t\t\treturn testError\n\t\t})\n\n\t\tif err != testError {\n\t\t\tt.Errorf(\"Expected test error, got %v\", err)\n\t\t}\n\n\t\t// Circuit should be open again\n\t\tif cb.GetState() != CircuitBreakerOpen {\n\t\t\tt.Errorf(\"Expected state %v, got %v\", CircuitBreakerOpen, cb.GetState())\n\t\t}\n\t})\n\n\tt.Run(\"Stats\", func(t *testing.T) {\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", config)\n\t\ttestError := errors.New(\"test error\")\n\n\t\t// Execute some operations\n\t\tcb.Execute(func() error { return testError }) // Failure\n\t\tcb.Execute(func() error { return testError }) // Failure\n\n\t\tstats := cb.GetStats()\n\n\t\tif stats.Endpoint != \"test-endpoint:6379\" {\n\t\t\tt.Errorf(\"Expected endpoint 'test-endpoint:6379', got %s\", stats.Endpoint)\n\t\t}\n\n\t\tif stats.Failures != 2 {\n\t\t\tt.Errorf(\"Expected 2 failures, got %d\", stats.Failures)\n\t\t}\n\n\t\tif stats.State != CircuitBreakerClosed {\n\t\t\tt.Errorf(\"Expected state %v, got %v\", CircuitBreakerClosed, stats.State)\n\t\t}\n\n\t\t// Test that success resets failure count\n\t\tcb.Execute(func() error { return nil }) // Success\n\t\tstats = cb.GetStats()\n\n\t\tif stats.Failures != 0 {\n\t\t\tt.Errorf(\"Expected 0 failures after success, got %d\", stats.Failures)\n\t\t}\n\t})\n}\n\nfunc TestCircuitBreakerManager(t *testing.T) {\n\tconfig := &Config{\n\t\tCircuitBreakerFailureThreshold: 5,\n\t\tCircuitBreakerResetTimeout:     60 * time.Second,\n\t\tCircuitBreakerMaxRequests:      3,\n\t}\n\n\tt.Run(\"GetCircuitBreaker\", func(t *testing.T) {\n\t\tmanager := newCircuitBreakerManager(config)\n\n\t\tcb1 := manager.GetCircuitBreaker(\"endpoint1:6379\")\n\t\tcb2 := manager.GetCircuitBreaker(\"endpoint2:6379\")\n\t\tcb3 := manager.GetCircuitBreaker(\"endpoint1:6379\") // Same as cb1\n\n\t\tif cb1 == cb2 {\n\t\t\tt.Error(\"Different endpoints should have different circuit breakers\")\n\t\t}\n\n\t\tif cb1 != cb3 {\n\t\t\tt.Error(\"Same endpoint should return the same circuit breaker\")\n\t\t}\n\t})\n\n\tt.Run(\"GetAllStats\", func(t *testing.T) {\n\t\tmanager := newCircuitBreakerManager(config)\n\n\t\t// Create circuit breakers for different endpoints\n\t\tcb1 := manager.GetCircuitBreaker(\"endpoint1:6379\")\n\t\tcb2 := manager.GetCircuitBreaker(\"endpoint2:6379\")\n\n\t\t// Execute some operations\n\t\tcb1.Execute(func() error { return nil })\n\t\tcb2.Execute(func() error { return errors.New(\"test error\") })\n\n\t\tstats := manager.GetAllStats()\n\n\t\tif len(stats) != 2 {\n\t\t\tt.Errorf(\"Expected 2 circuit breaker stats, got %d\", len(stats))\n\t\t}\n\n\t\t// Check that we have stats for both endpoints\n\t\tendpoints := make(map[string]bool)\n\t\tfor _, stat := range stats {\n\t\t\tendpoints[stat.Endpoint] = true\n\t\t}\n\n\t\tif !endpoints[\"endpoint1:6379\"] || !endpoints[\"endpoint2:6379\"] {\n\t\t\tt.Error(\"Missing stats for expected endpoints\")\n\t\t}\n\t})\n\n\tt.Run(\"Reset\", func(t *testing.T) {\n\t\tmanager := newCircuitBreakerManager(config)\n\t\ttestError := errors.New(\"test error\")\n\n\t\tcb := manager.GetCircuitBreaker(\"test-endpoint:6379\")\n\n\t\t// Force circuit to open\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tcb.Execute(func() error { return testError })\n\t\t}\n\n\t\tif cb.GetState() != CircuitBreakerOpen {\n\t\t\tt.Error(\"Circuit should be open\")\n\t\t}\n\n\t\t// Reset all circuit breakers\n\t\tmanager.Reset()\n\n\t\tif cb.GetState() != CircuitBreakerClosed {\n\t\t\tt.Error(\"Circuit should be closed after reset\")\n\t\t}\n\n\t\tif cb.failures.Load() != 0 {\n\t\t\tt.Error(\"Failure count should be reset to 0\")\n\t\t}\n\t})\n\n\tt.Run(\"ConfigurableParameters\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tCircuitBreakerFailureThreshold: 10,\n\t\t\tCircuitBreakerResetTimeout:     30 * time.Second,\n\t\t\tCircuitBreakerMaxRequests:      5,\n\t\t}\n\n\t\tcb := newCircuitBreaker(\"test-endpoint:6379\", config)\n\n\t\t// Test that configuration values are used\n\t\tif cb.failureThreshold != 10 {\n\t\t\tt.Errorf(\"Expected failureThreshold=10, got %d\", cb.failureThreshold)\n\t\t}\n\t\tif cb.resetTimeout != 30*time.Second {\n\t\t\tt.Errorf(\"Expected resetTimeout=30s, got %v\", cb.resetTimeout)\n\t\t}\n\t\tif cb.maxRequests != 5 {\n\t\t\tt.Errorf(\"Expected maxRequests=5, got %d\", cb.maxRequests)\n\t\t}\n\n\t\t// Test that circuit opens after configured threshold\n\t\ttestError := errors.New(\"test error\")\n\t\tfor i := 0; i < 9; i++ {\n\t\t\terr := cb.Execute(func() error { return testError })\n\t\t\tif err != testError {\n\t\t\t\tt.Errorf(\"Expected test error, got %v\", err)\n\t\t\t}\n\t\t\tif cb.GetState() != CircuitBreakerClosed {\n\t\t\t\tt.Errorf(\"Circuit should still be closed after %d failures\", i+1)\n\t\t\t}\n\t\t}\n\n\t\t// 10th failure should open the circuit\n\t\terr := cb.Execute(func() error { return testError })\n\t\tif err != testError {\n\t\t\tt.Errorf(\"Expected test error, got %v\", err)\n\t\t}\n\n\t\tif cb.GetState() != CircuitBreakerOpen {\n\t\t\tt.Errorf(\"Expected state %v, got %v\", CircuitBreakerOpen, cb.GetState())\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "maintnotifications/config.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n)\n\n// Mode represents the maintenance notifications mode\ntype Mode string\n\n// Constants for maintenance push notifications modes\nconst (\n\tModeDisabled Mode = \"disabled\" // Client doesn't send CLIENT MAINT_NOTIFICATIONS ON command\n\tModeEnabled  Mode = \"enabled\"  // Client forcefully sends command, interrupts connection on error\n\tModeAuto     Mode = \"auto\"     // Client tries to send command, disables feature on error\n)\n\n// IsValid returns true if the maintenance notifications mode is valid\nfunc (m Mode) IsValid() bool {\n\tswitch m {\n\tcase ModeDisabled, ModeEnabled, ModeAuto:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// String returns the string representation of the mode\nfunc (m Mode) String() string {\n\treturn string(m)\n}\n\n// EndpointType represents the type of endpoint to request in MOVING notifications\ntype EndpointType string\n\n// Constants for endpoint types\nconst (\n\tEndpointTypeAuto         EndpointType = \"auto\"          // Auto-detect based on connection\n\tEndpointTypeInternalIP   EndpointType = \"internal-ip\"   // Internal IP address\n\tEndpointTypeInternalFQDN EndpointType = \"internal-fqdn\" // Internal FQDN\n\tEndpointTypeExternalIP   EndpointType = \"external-ip\"   // External IP address\n\tEndpointTypeExternalFQDN EndpointType = \"external-fqdn\" // External FQDN\n\tEndpointTypeNone         EndpointType = \"none\"          // No endpoint (reconnect with current config)\n)\n\n// IsValid returns true if the endpoint type is valid\nfunc (e EndpointType) IsValid() bool {\n\tswitch e {\n\tcase EndpointTypeAuto, EndpointTypeInternalIP, EndpointTypeInternalFQDN,\n\t\tEndpointTypeExternalIP, EndpointTypeExternalFQDN, EndpointTypeNone:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// String returns the string representation of the endpoint type\nfunc (e EndpointType) String() string {\n\treturn string(e)\n}\n\n// Config provides configuration options for maintenance notifications\ntype Config struct {\n\t// Mode controls how client maintenance notifications are handled.\n\t// Valid values: ModeDisabled, ModeEnabled, ModeAuto\n\t// Default: ModeAuto\n\tMode Mode\n\n\t// EndpointType specifies the type of endpoint to request in MOVING notifications.\n\t// Valid values: EndpointTypeAuto, EndpointTypeInternalIP, EndpointTypeInternalFQDN,\n\t//               EndpointTypeExternalIP, EndpointTypeExternalFQDN, EndpointTypeNone\n\t// Default: EndpointTypeAuto\n\tEndpointType EndpointType\n\n\t// RelaxedTimeout is the concrete timeout value to use during\n\t// MIGRATING/FAILING_OVER states to accommodate increased latency.\n\t// This applies to both read and write timeouts.\n\t// Default: 10 seconds\n\tRelaxedTimeout time.Duration\n\n\t// HandoffTimeout is the maximum time to wait for connection handoff to complete.\n\t// If handoff takes longer than this, the old connection will be forcibly closed.\n\t// Default: 15 seconds (matches server-side eviction timeout)\n\tHandoffTimeout time.Duration\n\n\t// MaxWorkers is the maximum number of worker goroutines for processing handoff requests.\n\t// Workers are created on-demand and automatically cleaned up when idle.\n\t// If zero, defaults to min(10, PoolSize/2) to handle bursts effectively.\n\t// If explicitly set, enforces minimum of PoolSize/2\n\t//\n\t// Default: min(PoolSize/2, max(10, PoolSize/3)), Minimum when set: PoolSize/2\n\tMaxWorkers int\n\n\t// HandoffQueueSize is the size of the buffered channel used to queue handoff requests.\n\t// If the queue is full, new handoff requests will be rejected.\n\t// Scales with both worker count and pool size for better burst handling.\n\t//\n\t// Default: max(20×MaxWorkers, PoolSize), capped by MaxActiveConns+1 (if set) or 5×PoolSize\n\t// When set: minimum 200, capped by MaxActiveConns+1 (if set) or 5×PoolSize\n\tHandoffQueueSize int\n\n\t// PostHandoffRelaxedDuration is how long to keep relaxed timeouts on the new connection\n\t// after a handoff completes. This provides additional resilience during cluster transitions.\n\t// Default: 2 * RelaxedTimeout\n\tPostHandoffRelaxedDuration time.Duration\n\n\t// Circuit breaker configuration for endpoint failure handling\n\t// CircuitBreakerFailureThreshold is the number of failures before opening the circuit.\n\t// Default: 5\n\tCircuitBreakerFailureThreshold int\n\n\t// CircuitBreakerResetTimeout is how long to wait before testing if the endpoint recovered.\n\t// Default: 60 seconds\n\tCircuitBreakerResetTimeout time.Duration\n\n\t// CircuitBreakerMaxRequests is the maximum number of requests allowed in half-open state.\n\t// Default: 3\n\tCircuitBreakerMaxRequests int\n\n\t// MaxHandoffRetries is the maximum number of times to retry a failed handoff.\n\t// After this many retries, the connection will be removed from the pool.\n\t// Default: 3\n\tMaxHandoffRetries int\n}\n\nfunc (c *Config) IsEnabled() bool {\n\treturn c != nil && c.Mode != ModeDisabled\n}\n\n// DefaultConfig returns a Config with sensible defaults.\nfunc DefaultConfig() *Config {\n\treturn &Config{\n\t\tMode:                       ModeAuto,         // Enable by default for Redis Cloud\n\t\tEndpointType:               EndpointTypeAuto, // Auto-detect based on connection\n\t\tRelaxedTimeout:             10 * time.Second,\n\t\tHandoffTimeout:             15 * time.Second,\n\t\tMaxWorkers:                 0, // Auto-calculated based on pool size\n\t\tHandoffQueueSize:           0, // Auto-calculated based on max workers\n\t\tPostHandoffRelaxedDuration: 0, // Auto-calculated based on relaxed timeout\n\n\t\t// Circuit breaker configuration\n\t\tCircuitBreakerFailureThreshold: 5,\n\t\tCircuitBreakerResetTimeout:     60 * time.Second,\n\t\tCircuitBreakerMaxRequests:      3,\n\n\t\t// Connection Handoff Configuration\n\t\tMaxHandoffRetries: 3,\n\t}\n}\n\n// Validate checks if the configuration is valid.\nfunc (c *Config) Validate() error {\n\tif c.RelaxedTimeout <= 0 {\n\t\treturn ErrInvalidRelaxedTimeout\n\t}\n\tif c.HandoffTimeout <= 0 {\n\t\treturn ErrInvalidHandoffTimeout\n\t}\n\t// Validate worker configuration\n\t// Allow 0 for auto-calculation, but negative values are invalid\n\tif c.MaxWorkers < 0 {\n\t\treturn ErrInvalidHandoffWorkers\n\t}\n\t// HandoffQueueSize validation - allow 0 for auto-calculation\n\tif c.HandoffQueueSize < 0 {\n\t\treturn ErrInvalidHandoffQueueSize\n\t}\n\tif c.PostHandoffRelaxedDuration < 0 {\n\t\treturn ErrInvalidPostHandoffRelaxedDuration\n\t}\n\n\t// Circuit breaker validation\n\tif c.CircuitBreakerFailureThreshold < 1 {\n\t\treturn ErrInvalidCircuitBreakerFailureThreshold\n\t}\n\tif c.CircuitBreakerResetTimeout < 0 {\n\t\treturn ErrInvalidCircuitBreakerResetTimeout\n\t}\n\tif c.CircuitBreakerMaxRequests < 1 {\n\t\treturn ErrInvalidCircuitBreakerMaxRequests\n\t}\n\n\t// Validate Mode (maintenance notifications mode)\n\tif !c.Mode.IsValid() {\n\t\treturn ErrInvalidMaintNotifications\n\t}\n\n\t// Validate EndpointType\n\tif !c.EndpointType.IsValid() {\n\t\treturn ErrInvalidEndpointType\n\t}\n\n\t// Validate configuration fields\n\tif c.MaxHandoffRetries < 1 || c.MaxHandoffRetries > 10 {\n\t\treturn ErrInvalidHandoffRetries\n\t}\n\n\treturn nil\n}\n\n// ApplyDefaults applies default values to any zero-value fields in the configuration.\n// This ensures that partially configured structs get sensible defaults for missing fields.\nfunc (c *Config) ApplyDefaults() *Config {\n\treturn c.ApplyDefaultsWithPoolSize(0)\n}\n\n// ApplyDefaultsWithPoolSize applies default values to any zero-value fields in the configuration,\n// using the provided pool size to calculate worker defaults.\n// This ensures that partially configured structs get sensible defaults for missing fields.\nfunc (c *Config) ApplyDefaultsWithPoolSize(poolSize int) *Config {\n\treturn c.ApplyDefaultsWithPoolConfig(poolSize, 0)\n}\n\n// ApplyDefaultsWithPoolConfig applies default values to any zero-value fields in the configuration,\n// using the provided pool size and max active connections to calculate worker and queue defaults.\n// This ensures that partially configured structs get sensible defaults for missing fields.\nfunc (c *Config) ApplyDefaultsWithPoolConfig(poolSize int, maxActiveConns int) *Config {\n\tif c == nil {\n\t\treturn DefaultConfig().ApplyDefaultsWithPoolSize(poolSize)\n\t}\n\n\tdefaults := DefaultConfig()\n\tresult := &Config{}\n\n\t// Apply defaults for enum fields (empty/zero means not set)\n\tresult.Mode = defaults.Mode\n\tif c.Mode != \"\" {\n\t\tresult.Mode = c.Mode\n\t}\n\n\tresult.EndpointType = defaults.EndpointType\n\tif c.EndpointType != \"\" {\n\t\tresult.EndpointType = c.EndpointType\n\t}\n\n\t// Apply defaults for duration fields (zero means not set)\n\tresult.RelaxedTimeout = defaults.RelaxedTimeout\n\tif c.RelaxedTimeout > 0 {\n\t\tresult.RelaxedTimeout = c.RelaxedTimeout\n\t}\n\n\tresult.HandoffTimeout = defaults.HandoffTimeout\n\tif c.HandoffTimeout > 0 {\n\t\tresult.HandoffTimeout = c.HandoffTimeout\n\t}\n\n\t// Copy worker configuration\n\tresult.MaxWorkers = c.MaxWorkers\n\n\t// Apply worker defaults based on pool size\n\tresult.applyWorkerDefaults(poolSize)\n\n\t// Apply queue size defaults with new scaling approach\n\t// Default: max(20x workers, PoolSize), capped by maxActiveConns or 5x pool size\n\tworkerBasedSize := result.MaxWorkers * 20\n\tpoolBasedSize := poolSize\n\tresult.HandoffQueueSize = max(workerBasedSize, poolBasedSize)\n\tif c.HandoffQueueSize > 0 {\n\t\t// When explicitly set: enforce minimum of 200\n\t\tresult.HandoffQueueSize = max(200, c.HandoffQueueSize)\n\t}\n\n\t// Cap queue size: use maxActiveConns+1 if set, otherwise 5x pool size\n\tvar queueCap int\n\tif maxActiveConns > 0 {\n\t\tqueueCap = maxActiveConns + 1\n\t\t// Ensure queue cap is at least 2 for very small maxActiveConns\n\t\tif queueCap < 2 {\n\t\t\tqueueCap = 2\n\t\t}\n\t} else {\n\t\tqueueCap = poolSize * 5\n\t}\n\tresult.HandoffQueueSize = min(result.HandoffQueueSize, queueCap)\n\n\t// Ensure minimum queue size of 2 (fallback for very small pools)\n\tif result.HandoffQueueSize < 2 {\n\t\tresult.HandoffQueueSize = 2\n\t}\n\n\tresult.PostHandoffRelaxedDuration = result.RelaxedTimeout * 2\n\tif c.PostHandoffRelaxedDuration > 0 {\n\t\tresult.PostHandoffRelaxedDuration = c.PostHandoffRelaxedDuration\n\t}\n\n\t// Apply defaults for configuration fields\n\tresult.MaxHandoffRetries = defaults.MaxHandoffRetries\n\tif c.MaxHandoffRetries > 0 {\n\t\tresult.MaxHandoffRetries = c.MaxHandoffRetries\n\t}\n\n\t// Circuit breaker configuration\n\tresult.CircuitBreakerFailureThreshold = defaults.CircuitBreakerFailureThreshold\n\tif c.CircuitBreakerFailureThreshold > 0 {\n\t\tresult.CircuitBreakerFailureThreshold = c.CircuitBreakerFailureThreshold\n\t}\n\n\tresult.CircuitBreakerResetTimeout = defaults.CircuitBreakerResetTimeout\n\tif c.CircuitBreakerResetTimeout > 0 {\n\t\tresult.CircuitBreakerResetTimeout = c.CircuitBreakerResetTimeout\n\t}\n\n\tresult.CircuitBreakerMaxRequests = defaults.CircuitBreakerMaxRequests\n\tif c.CircuitBreakerMaxRequests > 0 {\n\t\tresult.CircuitBreakerMaxRequests = c.CircuitBreakerMaxRequests\n\t}\n\n\tif internal.LogLevel.DebugOrAbove() {\n\t\tinternal.Logger.Printf(context.Background(), logs.DebugLoggingEnabled())\n\t\tinternal.Logger.Printf(context.Background(), logs.ConfigDebug(result))\n\t}\n\treturn result\n}\n\n// Clone creates a deep copy of the configuration.\nfunc (c *Config) Clone() *Config {\n\tif c == nil {\n\t\treturn DefaultConfig()\n\t}\n\n\treturn &Config{\n\t\tMode:                       c.Mode,\n\t\tEndpointType:               c.EndpointType,\n\t\tRelaxedTimeout:             c.RelaxedTimeout,\n\t\tHandoffTimeout:             c.HandoffTimeout,\n\t\tMaxWorkers:                 c.MaxWorkers,\n\t\tHandoffQueueSize:           c.HandoffQueueSize,\n\t\tPostHandoffRelaxedDuration: c.PostHandoffRelaxedDuration,\n\n\t\t// Circuit breaker configuration\n\t\tCircuitBreakerFailureThreshold: c.CircuitBreakerFailureThreshold,\n\t\tCircuitBreakerResetTimeout:     c.CircuitBreakerResetTimeout,\n\t\tCircuitBreakerMaxRequests:      c.CircuitBreakerMaxRequests,\n\n\t\t// Configuration fields\n\t\tMaxHandoffRetries: c.MaxHandoffRetries,\n\t}\n}\n\n// applyWorkerDefaults calculates and applies worker defaults based on pool size\nfunc (c *Config) applyWorkerDefaults(poolSize int) {\n\t// Calculate defaults based on pool size\n\tif poolSize <= 0 {\n\t\tpoolSize = 10 * runtime.GOMAXPROCS(0)\n\t}\n\n\t// When not set: min(poolSize/2, max(10, poolSize/3)) - balanced scaling approach\n\toriginalMaxWorkers := c.MaxWorkers\n\tc.MaxWorkers = min(poolSize/2, max(10, poolSize/3))\n\tif originalMaxWorkers != 0 {\n\t\t// When explicitly set: max(poolSize/2, set_value) - ensure at least poolSize/2 workers\n\t\tc.MaxWorkers = max(poolSize/2, originalMaxWorkers)\n\t}\n\n\t// Ensure minimum of 1 worker (fallback for very small pools)\n\tif c.MaxWorkers < 1 {\n\t\tc.MaxWorkers = 1\n\t}\n}\n\n// DetectEndpointType automatically detects the appropriate endpoint type\n// based on the connection address and TLS configuration.\n//\n// For IP addresses:\n//   - If TLS is enabled: requests FQDN for proper certificate validation\n//   - If TLS is disabled: requests IP for better performance\n//\n// For hostnames:\n//   - If TLS is enabled: always requests FQDN for proper certificate validation\n//   - If TLS is disabled: requests IP for better performance\n//\n// Internal vs External detection:\n//   - For IPs: uses private IP range detection\n//   - For hostnames: uses heuristics based on common internal naming patterns\nfunc DetectEndpointType(addr string, tlsEnabled bool) EndpointType {\n\t// Extract host from \"host:port\" format\n\thost, _, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\thost = addr // Assume no port\n\t}\n\n\t// Check if the host is an IP address or hostname\n\tip := net.ParseIP(host)\n\tisIPAddress := ip != nil\n\tvar endpointType EndpointType\n\n\tif isIPAddress {\n\t\t// Address is an IP - determine if it's private or public\n\t\tisPrivate := ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()\n\n\t\tif tlsEnabled {\n\t\t\t// TLS with IP addresses - still prefer FQDN for certificate validation\n\t\t\tif isPrivate {\n\t\t\t\tendpointType = EndpointTypeInternalFQDN\n\t\t\t} else {\n\t\t\t\tendpointType = EndpointTypeExternalFQDN\n\t\t\t}\n\t\t} else {\n\t\t\t// No TLS - can use IP addresses directly\n\t\t\tif isPrivate {\n\t\t\t\tendpointType = EndpointTypeInternalIP\n\t\t\t} else {\n\t\t\t\tendpointType = EndpointTypeExternalIP\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Address is a hostname\n\t\tisInternalHostname := isInternalHostname(host)\n\t\tif isInternalHostname {\n\t\t\tendpointType = EndpointTypeInternalFQDN\n\t\t} else {\n\t\t\tendpointType = EndpointTypeExternalFQDN\n\t\t}\n\t}\n\n\treturn endpointType\n}\n\n// isInternalHostname determines if a hostname appears to be internal/private.\n// This is a heuristic based on common naming patterns.\nfunc isInternalHostname(hostname string) bool {\n\t// Convert to lowercase for comparison\n\thostname = strings.ToLower(hostname)\n\n\t// Common internal hostname patterns\n\tinternalPatterns := []string{\n\t\t\"localhost\",\n\t\t\".local\",\n\t\t\".internal\",\n\t\t\".corp\",\n\t\t\".lan\",\n\t\t\".intranet\",\n\t\t\".private\",\n\t}\n\n\t// Check for exact match or suffix match\n\tfor _, pattern := range internalPatterns {\n\t\tif hostname == pattern || strings.HasSuffix(hostname, pattern) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Check for RFC 1918 style hostnames (e.g., redis-1, db-server, etc.)\n\t// If hostname doesn't contain dots, it's likely internal\n\tif !strings.Contains(hostname, \".\") {\n\t\treturn true\n\t}\n\n\t// Default to external for fully qualified domain names\n\treturn false\n}\n"
  },
  {
    "path": "maintnotifications/config_test.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConfig(t *testing.T) {\n\tt.Run(\"DefaultConfig\", func(t *testing.T) {\n\t\tconfig := DefaultConfig()\n\n\t\t// MaxWorkers should be 0 in default config (auto-calculated)\n\t\tif config.MaxWorkers != 0 {\n\t\t\tt.Errorf(\"Expected MaxWorkers to be 0 (auto-calculated), got %d\", config.MaxWorkers)\n\t\t}\n\n\t\t// HandoffQueueSize should be 0 in default config (auto-calculated)\n\t\tif config.HandoffQueueSize != 0 {\n\t\t\tt.Errorf(\"Expected HandoffQueueSize to be 0 (auto-calculated), got %d\", config.HandoffQueueSize)\n\t\t}\n\n\t\tif config.RelaxedTimeout != 10*time.Second {\n\t\t\tt.Errorf(\"Expected RelaxedTimeout to be 10s, got %v\", config.RelaxedTimeout)\n\t\t}\n\n\t\t// Test configuration fields have proper defaults\n\t\tif config.MaxHandoffRetries != 3 {\n\t\t\tt.Errorf(\"Expected MaxHandoffRetries to be 3, got %d\", config.MaxHandoffRetries)\n\t\t}\n\n\t\t// Circuit breaker defaults\n\t\tif config.CircuitBreakerFailureThreshold != 5 {\n\t\t\tt.Errorf(\"Expected CircuitBreakerFailureThreshold=5, got %d\", config.CircuitBreakerFailureThreshold)\n\t\t}\n\t\tif config.CircuitBreakerResetTimeout != 60*time.Second {\n\t\t\tt.Errorf(\"Expected CircuitBreakerResetTimeout=60s, got %v\", config.CircuitBreakerResetTimeout)\n\t\t}\n\t\tif config.CircuitBreakerMaxRequests != 3 {\n\t\t\tt.Errorf(\"Expected CircuitBreakerMaxRequests=3, got %d\", config.CircuitBreakerMaxRequests)\n\t\t}\n\n\t\tif config.HandoffTimeout != 15*time.Second {\n\t\t\tt.Errorf(\"Expected HandoffTimeout to be 15s, got %v\", config.HandoffTimeout)\n\t\t}\n\n\t\tif config.PostHandoffRelaxedDuration != 0 {\n\t\t\tt.Errorf(\"Expected PostHandoffRelaxedDuration to be 0 (auto-calculated), got %v\", config.PostHandoffRelaxedDuration)\n\t\t}\n\n\t\t// Test that defaults are applied correctly\n\t\tconfigWithDefaults := config.ApplyDefaultsWithPoolSize(100)\n\t\tif configWithDefaults.PostHandoffRelaxedDuration != 20*time.Second {\n\t\t\tt.Errorf(\"Expected PostHandoffRelaxedDuration to be 20s (2x RelaxedTimeout) after applying defaults, got %v\", configWithDefaults.PostHandoffRelaxedDuration)\n\t\t}\n\t})\n\n\tt.Run(\"ConfigValidation\", func(t *testing.T) {\n\t\t// Valid config with applied defaults\n\t\tconfig := DefaultConfig().ApplyDefaults()\n\t\tif err := config.Validate(); err != nil {\n\t\t\tt.Errorf(\"Default config with applied defaults should be valid: %v\", err)\n\t\t}\n\n\t\t// Invalid worker configuration (negative MaxWorkers)\n\t\tconfig = &Config{\n\t\t\tRelaxedTimeout:             30 * time.Second,\n\t\t\tHandoffTimeout:             15 * time.Second,\n\t\t\tMaxWorkers:                 -1, // This should be invalid\n\t\t\tHandoffQueueSize:           100,\n\t\t\tPostHandoffRelaxedDuration: 10 * time.Second,\n\t\t\tMaxHandoffRetries:          3, // Add required field\n\t\t}\n\t\tif err := config.Validate(); err != ErrInvalidHandoffWorkers {\n\t\t\tt.Errorf(\"Expected ErrInvalidHandoffWorkers, got %v\", err)\n\t\t}\n\n\t\t// Invalid HandoffQueueSize\n\t\tconfig = DefaultConfig().ApplyDefaults()\n\t\tconfig.HandoffQueueSize = -1\n\t\tif err := config.Validate(); err != ErrInvalidHandoffQueueSize {\n\t\t\tt.Errorf(\"Expected ErrInvalidHandoffQueueSize, got %v\", err)\n\t\t}\n\n\t\t// Invalid PostHandoffRelaxedDuration\n\t\tconfig = DefaultConfig().ApplyDefaults()\n\t\tconfig.PostHandoffRelaxedDuration = -1 * time.Second\n\t\tif err := config.Validate(); err != ErrInvalidPostHandoffRelaxedDuration {\n\t\t\tt.Errorf(\"Expected ErrInvalidPostHandoffRelaxedDuration, got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"ConfigClone\", func(t *testing.T) {\n\t\toriginal := DefaultConfig()\n\t\toriginal.MaxWorkers = 20\n\t\toriginal.HandoffQueueSize = 200\n\n\t\tcloned := original.Clone()\n\n\t\tif cloned.MaxWorkers != 20 {\n\t\t\tt.Errorf(\"Expected cloned MaxWorkers to be 20, got %d\", cloned.MaxWorkers)\n\t\t}\n\n\t\tif cloned.HandoffQueueSize != 200 {\n\t\t\tt.Errorf(\"Expected cloned HandoffQueueSize to be 200, got %d\", cloned.HandoffQueueSize)\n\t\t}\n\n\t\t// Modify original to ensure clone is independent\n\t\toriginal.MaxWorkers = 2\n\t\tif cloned.MaxWorkers != 20 {\n\t\t\tt.Error(\"Clone should be independent of original\")\n\t\t}\n\t})\n}\n\nfunc TestApplyDefaults(t *testing.T) {\n\tt.Run(\"NilConfig\", func(t *testing.T) {\n\t\tvar config *Config\n\t\tresult := config.ApplyDefaultsWithPoolSize(100) // Use explicit pool size for testing\n\n\t\t// With nil config, should get default config with auto-calculated workers\n\t\tif result.MaxWorkers <= 0 {\n\t\t\tt.Errorf(\"Expected MaxWorkers to be > 0 after applying defaults, got %d\", result.MaxWorkers)\n\t\t}\n\n\t\t// HandoffQueueSize should be auto-calculated with hybrid scaling\n\t\tworkerBasedSize := result.MaxWorkers * 20\n\t\tpoolSize := 100 // Default pool size used in ApplyDefaults\n\t\tpoolBasedSize := poolSize\n\t\texpectedQueueSize := max(workerBasedSize, poolBasedSize)\n\t\texpectedQueueSize = min(expectedQueueSize, poolSize*5) // Cap by 5x pool size\n\t\tif result.HandoffQueueSize != expectedQueueSize {\n\t\t\tt.Errorf(\"Expected HandoffQueueSize to be %d (max(20*MaxWorkers=%d, poolSize=%d) capped by 5*poolSize=%d), got %d\",\n\t\t\t\texpectedQueueSize, workerBasedSize, poolBasedSize, poolSize*5, result.HandoffQueueSize)\n\t\t}\n\t})\n\n\tt.Run(\"PartialConfig\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMaxWorkers: 60, // Set this field explicitly (> poolSize/2 = 50)\n\t\t\t// Leave other fields as zero values\n\t\t}\n\n\t\tresult := config.ApplyDefaultsWithPoolSize(100) // Use explicit pool size for testing\n\n\t\t// Should keep the explicitly set values when > poolSize/2\n\t\tif result.MaxWorkers != 60 {\n\t\t\tt.Errorf(\"Expected MaxWorkers to be 60 (explicitly set), got %d\", result.MaxWorkers)\n\t\t}\n\n\t\t// Should apply default for unset fields (auto-calculated queue size with hybrid scaling)\n\t\tworkerBasedSize := result.MaxWorkers * 20\n\t\tpoolSize := 100 // Default pool size used in ApplyDefaults\n\t\tpoolBasedSize := poolSize\n\t\texpectedQueueSize := max(workerBasedSize, poolBasedSize)\n\t\texpectedQueueSize = min(expectedQueueSize, poolSize*5) // Cap by 5x pool size\n\t\tif result.HandoffQueueSize != expectedQueueSize {\n\t\t\tt.Errorf(\"Expected HandoffQueueSize to be %d (max(20*MaxWorkers=%d, poolSize=%d) capped by 5*poolSize=%d), got %d\",\n\t\t\t\texpectedQueueSize, workerBasedSize, poolBasedSize, poolSize*5, result.HandoffQueueSize)\n\t\t}\n\n\t\t// Test explicit queue size capping by 5x pool size\n\t\tconfigWithLargeQueue := &Config{\n\t\t\tMaxWorkers:       5,\n\t\t\tHandoffQueueSize: 1000, // Much larger than 5x pool size\n\t\t}\n\n\t\tresultCapped := configWithLargeQueue.ApplyDefaultsWithPoolSize(20) // Small pool size\n\t\texpectedCap := 20 * 5                                              // 5x pool size = 100\n\t\tif resultCapped.HandoffQueueSize != expectedCap {\n\t\t\tt.Errorf(\"Expected HandoffQueueSize to be capped by 5x pool size (%d), got %d\", expectedCap, resultCapped.HandoffQueueSize)\n\t\t}\n\n\t\t// Test explicit queue size minimum enforcement\n\t\tconfigWithSmallQueue := &Config{\n\t\t\tMaxWorkers:       5,\n\t\t\tHandoffQueueSize: 10, // Below minimum of 200\n\t\t}\n\n\t\tresultMinimum := configWithSmallQueue.ApplyDefaultsWithPoolSize(100) // Large pool size\n\t\tif resultMinimum.HandoffQueueSize != 200 {\n\t\t\tt.Errorf(\"Expected HandoffQueueSize to be enforced minimum (200), got %d\", resultMinimum.HandoffQueueSize)\n\t\t}\n\n\t\t// Test that large explicit values are capped by 5x pool size\n\t\tconfigWithVeryLargeQueue := &Config{\n\t\t\tMaxWorkers:       5,\n\t\t\tHandoffQueueSize: 1000, // Much larger than 5x pool size\n\t\t}\n\n\t\tresultVeryLarge := configWithVeryLargeQueue.ApplyDefaultsWithPoolSize(100) // Pool size 100\n\t\texpectedVeryLargeCap := 100 * 5                                            // 5x pool size = 500\n\t\tif resultVeryLarge.HandoffQueueSize != expectedVeryLargeCap {\n\t\t\tt.Errorf(\"Expected very large HandoffQueueSize to be capped by 5x pool size (%d), got %d\", expectedVeryLargeCap, resultVeryLarge.HandoffQueueSize)\n\t\t}\n\n\t\tif result.RelaxedTimeout != 10*time.Second {\n\t\t\tt.Errorf(\"Expected RelaxedTimeout to be 10s (default), got %v\", result.RelaxedTimeout)\n\t\t}\n\n\t\tif result.HandoffTimeout != 15*time.Second {\n\t\t\tt.Errorf(\"Expected HandoffTimeout to be 15s (default), got %v\", result.HandoffTimeout)\n\t\t}\n\t})\n\n\tt.Run(\"ZeroValues\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:       0, // Zero value should get auto-calculated defaults\n\t\t\tHandoffQueueSize: 0, // Zero value should get default\n\t\t\tRelaxedTimeout:   0, // Zero value should get default\n\t\t}\n\n\t\tresult := config.ApplyDefaultsWithPoolSize(100) // Use explicit pool size for testing\n\n\t\t// Zero values should get auto-calculated defaults\n\t\tif result.MaxWorkers <= 0 {\n\t\t\tt.Errorf(\"Expected MaxWorkers to be > 0 (auto-calculated), got %d\", result.MaxWorkers)\n\t\t}\n\n\t\t// HandoffQueueSize should be auto-calculated with hybrid scaling\n\t\tworkerBasedSize := result.MaxWorkers * 20\n\t\tpoolSize := 100 // Default pool size used in ApplyDefaults\n\t\tpoolBasedSize := poolSize\n\t\texpectedQueueSize := max(workerBasedSize, poolBasedSize)\n\t\texpectedQueueSize = min(expectedQueueSize, poolSize*5) // Cap by 5x pool size\n\t\tif result.HandoffQueueSize != expectedQueueSize {\n\t\t\tt.Errorf(\"Expected HandoffQueueSize to be %d (max(20*MaxWorkers=%d, poolSize=%d) capped by 5*poolSize=%d), got %d\",\n\t\t\t\texpectedQueueSize, workerBasedSize, poolBasedSize, poolSize*5, result.HandoffQueueSize)\n\t\t}\n\n\t\tif result.RelaxedTimeout != 10*time.Second {\n\t\t\tt.Errorf(\"Expected RelaxedTimeout to be 10s (default), got %v\", result.RelaxedTimeout)\n\t\t}\n\n\t})\n}\n\nfunc TestProcessorWithConfig(t *testing.T) {\n\tt.Run(\"ProcessorUsesConfigValues\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:       5,\n\t\t\tHandoffQueueSize: 50,\n\t\t\tRelaxedTimeout:   10 * time.Second,\n\t\t\tHandoffTimeout:   5 * time.Second,\n\t\t}\n\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// The processor should be created successfully with custom config\n\t\tif processor == nil {\n\t\t\tt.Error(\"Processor should be created with custom config\")\n\t\t}\n\t})\n\n\tt.Run(\"ProcessorWithPartialConfig\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMaxWorkers: 7, // Only set worker field\n\t\t\t// Other fields will get defaults\n\t\t}\n\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Should work with partial config (defaults applied)\n\t\tif processor == nil {\n\t\t\tt.Error(\"Processor should be created with partial config\")\n\t\t}\n\t})\n\n\tt.Run(\"ProcessorWithNilConfig\", func(t *testing.T) {\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Should use default config when nil is passed\n\t\tif processor == nil {\n\t\t\tt.Error(\"Processor should be created with nil config (using defaults)\")\n\t\t}\n\t})\n}\n\nfunc TestIntegrationWithApplyDefaults(t *testing.T) {\n\tt.Run(\"ProcessorWithPartialConfigAppliesDefaults\", func(t *testing.T) {\n\t\t// Create a partial config with only some fields set\n\t\tpartialConfig := &Config{\n\t\t\tMaxWorkers: 15, // Custom value (>= 10 to test preservation)\n\t\t\t// Other fields left as zero values - should get defaults\n\t\t}\n\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\t// Create processor - should apply defaults to missing fields\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", partialConfig, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Processor should be created successfully\n\t\tif processor == nil {\n\t\t\tt.Error(\"Processor should be created with partial config\")\n\t\t}\n\n\t\t// Test that the ApplyDefaults method worked correctly by creating the same config\n\t\t// and applying defaults manually\n\t\texpectedConfig := partialConfig.ApplyDefaultsWithPoolSize(100) // Use explicit pool size for testing\n\n\t\t// Should preserve custom values (when >= poolSize/2)\n\t\tif expectedConfig.MaxWorkers != 50 { // max(poolSize/2, 15) = max(50, 15) = 50\n\t\t\tt.Errorf(\"Expected MaxWorkers to be 50, got %d\", expectedConfig.MaxWorkers)\n\t\t}\n\n\t\t// Should apply defaults for missing fields (auto-calculated queue size with hybrid scaling)\n\t\tworkerBasedSize := expectedConfig.MaxWorkers * 20\n\t\tpoolSize := 100 // Default pool size used in ApplyDefaults\n\t\tpoolBasedSize := poolSize\n\t\texpectedQueueSize := max(workerBasedSize, poolBasedSize)\n\t\texpectedQueueSize = min(expectedQueueSize, poolSize*5) // Cap by 5x pool size\n\t\tif expectedConfig.HandoffQueueSize != expectedQueueSize {\n\t\t\tt.Errorf(\"Expected HandoffQueueSize to be %d (max(20*MaxWorkers=%d, poolSize=%d) capped by 5*poolSize=%d), got %d\",\n\t\t\t\texpectedQueueSize, workerBasedSize, poolBasedSize, poolSize*5, expectedConfig.HandoffQueueSize)\n\t\t}\n\n\t\t// Test that queue size is always capped by 5x pool size\n\t\tif expectedConfig.HandoffQueueSize > poolSize*5 {\n\t\t\tt.Errorf(\"HandoffQueueSize (%d) should never exceed 5x pool size (%d)\",\n\t\t\t\texpectedConfig.HandoffQueueSize, poolSize*2)\n\t\t}\n\n\t\tif expectedConfig.RelaxedTimeout != 10*time.Second {\n\t\t\tt.Errorf(\"Expected RelaxedTimeout to be 10s (default), got %v\", expectedConfig.RelaxedTimeout)\n\t\t}\n\n\t\tif expectedConfig.HandoffTimeout != 15*time.Second {\n\t\t\tt.Errorf(\"Expected HandoffTimeout to be 15s (default), got %v\", expectedConfig.HandoffTimeout)\n\t\t}\n\n\t\tif expectedConfig.PostHandoffRelaxedDuration != 20*time.Second {\n\t\t\tt.Errorf(\"Expected PostHandoffRelaxedDuration to be 20s (2x RelaxedTimeout), got %v\", expectedConfig.PostHandoffRelaxedDuration)\n\t\t}\n\t})\n}\n\nfunc TestEnhancedConfigValidation(t *testing.T) {\n\tt.Run(\"ValidateFields\", func(t *testing.T) {\n\t\tconfig := DefaultConfig()\n\t\tconfig.ApplyDefaultsWithPoolSize(100) // Apply defaults with pool size 100\n\n\t\t// Should pass validation with default values\n\t\tif err := config.Validate(); err != nil {\n\t\t\tt.Errorf(\"Default config should be valid, got error: %v\", err)\n\t\t}\n\n\t\t// Test invalid MaxHandoffRetries\n\t\tconfig.MaxHandoffRetries = 0\n\t\tif err := config.Validate(); err == nil {\n\t\t\tt.Error(\"Expected validation error for MaxHandoffRetries = 0\")\n\t\t}\n\t\tconfig.MaxHandoffRetries = 11\n\t\tif err := config.Validate(); err == nil {\n\t\t\tt.Error(\"Expected validation error for MaxHandoffRetries = 11\")\n\t\t}\n\t\tconfig.MaxHandoffRetries = 3 // Reset to valid value\n\n\t\t// Test circuit breaker validation\n\t\tconfig.CircuitBreakerFailureThreshold = 0\n\t\tif err := config.Validate(); err != ErrInvalidCircuitBreakerFailureThreshold {\n\t\t\tt.Errorf(\"Expected ErrInvalidCircuitBreakerFailureThreshold, got %v\", err)\n\t\t}\n\t\tconfig.CircuitBreakerFailureThreshold = 5 // Reset to valid value\n\n\t\tconfig.CircuitBreakerResetTimeout = -1 * time.Second\n\t\tif err := config.Validate(); err != ErrInvalidCircuitBreakerResetTimeout {\n\t\t\tt.Errorf(\"Expected ErrInvalidCircuitBreakerResetTimeout, got %v\", err)\n\t\t}\n\t\tconfig.CircuitBreakerResetTimeout = 60 * time.Second // Reset to valid value\n\n\t\tconfig.CircuitBreakerMaxRequests = 0\n\t\tif err := config.Validate(); err != ErrInvalidCircuitBreakerMaxRequests {\n\t\t\tt.Errorf(\"Expected ErrInvalidCircuitBreakerMaxRequests, got %v\", err)\n\t\t}\n\t\tconfig.CircuitBreakerMaxRequests = 3 // Reset to valid value\n\n\t\t// Should pass validation again\n\t\tif err := config.Validate(); err != nil {\n\t\t\tt.Errorf(\"Config should be valid after reset, got error: %v\", err)\n\t\t}\n\t})\n}\n\nfunc TestConfigClone(t *testing.T) {\n\toriginal := DefaultConfig()\n\toriginal.MaxHandoffRetries = 7\n\toriginal.HandoffTimeout = 8 * time.Second\n\n\tcloned := original.Clone()\n\n\t// Test that values are copied\n\tif cloned.MaxHandoffRetries != 7 {\n\t\tt.Errorf(\"Expected cloned MaxHandoffRetries to be 7, got %d\", cloned.MaxHandoffRetries)\n\t}\n\tif cloned.HandoffTimeout != 8*time.Second {\n\t\tt.Errorf(\"Expected cloned HandoffTimeout to be 8s, got %v\", cloned.HandoffTimeout)\n\t}\n\n\t// Test that modifying clone doesn't affect original\n\tcloned.MaxHandoffRetries = 10\n\tif original.MaxHandoffRetries != 7 {\n\t\tt.Errorf(\"Modifying clone should not affect original, original MaxHandoffRetries changed to %d\", original.MaxHandoffRetries)\n\t}\n}\n\nfunc TestMaxWorkersLogic(t *testing.T) {\n\tt.Run(\"AutoCalculatedMaxWorkers\", func(t *testing.T) {\n\t\ttestCases := []struct {\n\t\t\tpoolSize        int\n\t\t\texpectedWorkers int\n\t\t\tdescription     string\n\t\t}{\n\t\t\t{6, 3, \"Small pool: min(6/2, max(10, 6/3)) = min(3, max(10, 2)) = min(3, 10) = 3\"},\n\t\t\t{15, 7, \"Medium pool: min(15/2, max(10, 15/3)) = min(7, max(10, 5)) = min(7, 10) = 7\"},\n\t\t\t{30, 10, \"Large pool: min(30/2, max(10, 30/3)) = min(15, max(10, 10)) = min(15, 10) = 10\"},\n\t\t\t{60, 20, \"Very large pool: min(60/2, max(10, 60/3)) = min(30, max(10, 20)) = min(30, 20) = 20\"},\n\t\t\t{120, 40, \"Huge pool: min(120/2, max(10, 120/3)) = min(60, max(10, 40)) = min(60, 40) = 40\"},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tconfig := &Config{} // MaxWorkers = 0 (not set)\n\t\t\tresult := config.ApplyDefaultsWithPoolSize(tc.poolSize)\n\n\t\t\tif result.MaxWorkers != tc.expectedWorkers {\n\t\t\t\tt.Errorf(\"PoolSize=%d: expected MaxWorkers=%d, got %d (%s)\",\n\t\t\t\t\ttc.poolSize, tc.expectedWorkers, result.MaxWorkers, tc.description)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"ExplicitlySetMaxWorkers\", func(t *testing.T) {\n\t\ttestCases := []struct {\n\t\t\tsetValue        int\n\t\t\texpectedWorkers int\n\t\t\tdescription     string\n\t\t}{\n\t\t\t{1, 50, \"Set 1: max(poolSize/2, 1) = max(50, 1) = 50 (enforced minimum)\"},\n\t\t\t{5, 50, \"Set 5: max(poolSize/2, 5) = max(50, 5) = 50 (enforced minimum)\"},\n\t\t\t{8, 50, \"Set 8: max(poolSize/2, 8) = max(50, 8) = 50 (enforced minimum)\"},\n\t\t\t{10, 50, \"Set 10: max(poolSize/2, 10) = max(50, 10) = 50 (enforced minimum)\"},\n\t\t\t{15, 50, \"Set 15: max(poolSize/2, 15) = max(50, 15) = 50 (enforced minimum)\"},\n\t\t\t{60, 60, \"Set 60: max(poolSize/2, 60) = max(50, 60) = 60 (respects user choice)\"},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tconfig := &Config{\n\t\t\t\tMaxWorkers: tc.setValue, // Explicitly set\n\t\t\t}\n\t\t\tresult := config.ApplyDefaultsWithPoolSize(100) // Pool size doesn't affect explicit values\n\n\t\t\tif result.MaxWorkers != tc.expectedWorkers {\n\t\t\t\tt.Errorf(\"Set MaxWorkers=%d: expected %d, got %d (%s)\",\n\t\t\t\t\ttc.setValue, tc.expectedWorkers, result.MaxWorkers, tc.description)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "maintnotifications/e2e/.gitignore",
    "content": "# E2E test artifacts\n*.log\n*.out\ntest-results/\ncoverage/\nprofiles/\n\n# Test data\ntest-data/\ntemp/\n*.tmp\n\n# CI artifacts\nartifacts/\nreports/\n\n# Redis data files (if running local Redis for testing)\ndump.rdb\nappendonly.aof\nredis.conf.local\n\n# Performance test results\n*.prof\n*.trace\nbenchmarks/\n\n# Docker compose files for local testing\ndocker-compose.override.yml\n.env.local\ninfra/\n"
  },
  {
    "path": "maintnotifications/e2e/DATABASE_MANAGEMENT.md",
    "content": "# Database Management with Fault Injector\n\nThis document describes how to use the fault injector's database management endpoints to create and delete Redis databases during E2E testing.\n\n## Overview\n\nThe fault injector now supports two new endpoints for database management:\n\n1. **CREATE_DATABASE** - Create a new Redis database with custom configuration\n2. **DELETE_DATABASE** - Delete an existing Redis database\n\nThese endpoints are useful for E2E tests that need to dynamically create and destroy databases as part of their test scenarios.\n\n## Action Types\n\n### CREATE_DATABASE\n\nCreates a new Redis database with the specified configuration.\n\n**Parameters:**\n- `cluster_index` (int): The index of the cluster where the database should be created\n- `database_config` (object): The database configuration (see structure below)\n\n**Raises:**\n- `CreateDatabaseException`: When database creation fails\n\n### DELETE_DATABASE\n\nDeletes an existing Redis database.\n\n**Parameters:**\n- `cluster_index` (int): The index of the cluster containing the database\n- `bdb_id` (int): The database ID to delete\n\n**Raises:**\n- `DeleteDatabaseException`: When database deletion fails\n\n## Database Configuration Structure\n\nThe `database_config` object supports the following fields:\n\n```go\ntype DatabaseConfig struct {\n    Name                            string                   `json:\"name\"`\n    Port                            int                      `json:\"port\"`\n    MemorySize                      int64                    `json:\"memory_size\"`\n    Replication                     bool                     `json:\"replication\"`\n    EvictionPolicy                  string                   `json:\"eviction_policy\"`\n    Sharding                        bool                     `json:\"sharding\"`\n    AutoUpgrade                     bool                     `json:\"auto_upgrade\"`\n    ShardsCount                     int                      `json:\"shards_count\"`\n    ModuleList                      []DatabaseModule         `json:\"module_list,omitempty\"`\n    OSSCluster                      bool                     `json:\"oss_cluster\"`\n    OSSClusterAPIPreferredIPType    string                   `json:\"oss_cluster_api_preferred_ip_type,omitempty\"`\n    ProxyPolicy                     string                   `json:\"proxy_policy,omitempty\"`\n    ShardsPlacement                 string                   `json:\"shards_placement,omitempty\"`\n    ShardKeyRegex                   []ShardKeyRegexPattern   `json:\"shard_key_regex,omitempty\"`\n}\n\ntype DatabaseModule struct {\n    ModuleArgs string `json:\"module_args\"`\n    ModuleName string `json:\"module_name\"`\n}\n\ntype ShardKeyRegexPattern struct {\n    Regex string `json:\"regex\"`\n}\n```\n\n### Example Configuration\n\n#### Simple Database\n\n```json\n{\n  \"name\": \"simple-db\",\n  \"port\": 12000,\n  \"memory_size\": 268435456,\n  \"replication\": false,\n  \"eviction_policy\": \"noeviction\",\n  \"sharding\": false,\n  \"auto_upgrade\": true,\n  \"shards_count\": 1,\n  \"oss_cluster\": false\n}\n```\n\n#### Clustered Database with Modules\n\n```json\n{\n  \"name\": \"ioredis-cluster\",\n  \"port\": 11112,\n  \"memory_size\": 1273741824,\n  \"replication\": true,\n  \"eviction_policy\": \"noeviction\",\n  \"sharding\": true,\n  \"auto_upgrade\": true,\n  \"shards_count\": 3,\n  \"module_list\": [\n    {\n      \"module_args\": \"\",\n      \"module_name\": \"ReJSON\"\n    },\n    {\n      \"module_args\": \"\",\n      \"module_name\": \"search\"\n    },\n    {\n      \"module_args\": \"\",\n      \"module_name\": \"timeseries\"\n    },\n    {\n      \"module_args\": \"\",\n      \"module_name\": \"bf\"\n    }\n  ],\n  \"oss_cluster\": true,\n  \"oss_cluster_api_preferred_ip_type\": \"external\",\n  \"proxy_policy\": \"all-master-shards\",\n  \"shards_placement\": \"sparse\",\n  \"shard_key_regex\": [\n    {\n      \"regex\": \".*\\\\{(?<tag>.*)\\\\}.*\"\n    },\n    {\n      \"regex\": \"(?<tag>.*)\"\n    }\n  ]\n}\n```\n\n## Usage Examples\n\n### Example 1: Create a Simple Database\n\n```go\nctx := context.Background()\nfaultInjector := NewFaultInjectorClient(\"http://127.0.0.1:20324\")\n\ndbConfig := DatabaseConfig{\n    Name:           \"test-db\",\n    Port:           12000,\n    MemorySize:     268435456, // 256MB\n    Replication:    false,\n    EvictionPolicy: \"noeviction\",\n    Sharding:       false,\n    AutoUpgrade:    true,\n    ShardsCount:    1,\n    OSSCluster:     false,\n}\n\nresp, err := faultInjector.CreateDatabase(ctx, 0, dbConfig)\nif err != nil {\n    log.Fatalf(\"Failed to create database: %v\", err)\n}\n\n// Wait for creation to complete\nstatus, err := faultInjector.WaitForAction(ctx, resp.ActionID,\n    WithMaxWaitTime(5*time.Minute))\nif err != nil {\n    log.Fatalf(\"Failed to wait for action: %v\", err)\n}\n\nif status.Status == StatusSuccess {\n    log.Println(\"Database created successfully!\")\n}\n```\n\n### Example 2: Create a Database with Modules\n\n```go\ndbConfig := DatabaseConfig{\n    Name:           \"modules-db\",\n    Port:           12001,\n    MemorySize:     536870912, // 512MB\n    Replication:    true,\n    EvictionPolicy: \"noeviction\",\n    Sharding:       true,\n    AutoUpgrade:    true,\n    ShardsCount:    3,\n    ModuleList: []DatabaseModule{\n        {ModuleArgs: \"\", ModuleName: \"ReJSON\"},\n        {ModuleArgs: \"\", ModuleName: \"search\"},\n    },\n    OSSCluster:                   true,\n    OSSClusterAPIPreferredIPType: \"external\",\n    ProxyPolicy:                  \"all-master-shards\",\n    ShardsPlacement:              \"sparse\",\n}\n\nresp, err := faultInjector.CreateDatabase(ctx, 0, dbConfig)\n// ... handle response\n```\n\n### Example 3: Create Database Using a Map\n\n```go\ndbConfigMap := map[string]interface{}{\n    \"name\":            \"map-db\",\n    \"port\":            12002,\n    \"memory_size\":     268435456,\n    \"replication\":     false,\n    \"eviction_policy\": \"volatile-lru\",\n    \"sharding\":        false,\n    \"auto_upgrade\":    true,\n    \"shards_count\":    1,\n    \"oss_cluster\":     false,\n}\n\nresp, err := faultInjector.CreateDatabaseFromMap(ctx, 0, dbConfigMap)\n// ... handle response\n```\n\n### Example 4: Delete a Database\n\n```go\nclusterIndex := 0\nbdbID := 1\n\nresp, err := faultInjector.DeleteDatabase(ctx, clusterIndex, bdbID)\nif err != nil {\n    log.Fatalf(\"Failed to delete database: %v\", err)\n}\n\nstatus, err := faultInjector.WaitForAction(ctx, resp.ActionID,\n    WithMaxWaitTime(2*time.Minute))\nif err != nil {\n    log.Fatalf(\"Failed to wait for action: %v\", err)\n}\n\nif status.Status == StatusSuccess {\n    log.Println(\"Database deleted successfully!\")\n}\n```\n\n### Example 5: Complete Lifecycle (Create and Delete)\n\n```go\n// Create database\ndbConfig := DatabaseConfig{\n    Name:           \"temp-db\",\n    Port:           13000,\n    MemorySize:     268435456,\n    Replication:    false,\n    EvictionPolicy: \"noeviction\",\n    Sharding:       false,\n    AutoUpgrade:    true,\n    ShardsCount:    1,\n    OSSCluster:     false,\n}\n\ncreateResp, err := faultInjector.CreateDatabase(ctx, 0, dbConfig)\nif err != nil {\n    log.Fatalf(\"Failed to create database: %v\", err)\n}\n\ncreateStatus, err := faultInjector.WaitForAction(ctx, createResp.ActionID,\n    WithMaxWaitTime(5*time.Minute))\nif err != nil || createStatus.Status != StatusSuccess {\n    log.Fatalf(\"Database creation failed\")\n}\n\n// Extract bdb_id from output\nvar bdbID int\nif id, ok := createStatus.Output[\"bdb_id\"].(float64); ok {\n    bdbID = int(id)\n}\n\n// Use the database for testing...\ntime.Sleep(10 * time.Second)\n\n// Delete the database\ndeleteResp, err := faultInjector.DeleteDatabase(ctx, 0, bdbID)\nif err != nil {\n    log.Fatalf(\"Failed to delete database: %v\", err)\n}\n\ndeleteStatus, err := faultInjector.WaitForAction(ctx, deleteResp.ActionID,\n    WithMaxWaitTime(2*time.Minute))\nif err != nil || deleteStatus.Status != StatusSuccess {\n    log.Fatalf(\"Database deletion failed\")\n}\n\nlog.Println(\"Database lifecycle completed successfully!\")\n```\n\n## Available Methods\n\nThe `FaultInjectorClient` provides the following methods for database management:\n\n### CreateDatabase\n\n```go\nfunc (c *FaultInjectorClient) CreateDatabase(\n    ctx context.Context,\n    clusterIndex int,\n    databaseConfig DatabaseConfig,\n) (*ActionResponse, error)\n```\n\nCreates a new database using a structured `DatabaseConfig` object.\n\n### CreateDatabaseFromMap\n\n```go\nfunc (c *FaultInjectorClient) CreateDatabaseFromMap(\n    ctx context.Context,\n    clusterIndex int,\n    databaseConfig map[string]interface{},\n) (*ActionResponse, error)\n```\n\nCreates a new database using a flexible map configuration. Useful when you need to pass custom or dynamic configurations.\n\n### DeleteDatabase\n\n```go\nfunc (c *FaultInjectorClient) DeleteDatabase(\n    ctx context.Context,\n    clusterIndex int,\n    bdbID int,\n) (*ActionResponse, error)\n```\n\nDeletes an existing database by its ID.\n\n## Testing\n\nTo run the database management E2E tests:\n\n```bash\n# Run all database management tests\ngo test -tags=e2e -v ./maintnotifications/e2e/ -run TestDatabase\n\n# Run specific test\ngo test -tags=e2e -v ./maintnotifications/e2e/ -run TestDatabaseLifecycle\n```\n\n## Notes\n\n- Database creation can take several minutes depending on the configuration\n- Always use `WaitForAction` to ensure the operation completes before proceeding\n- The `bdb_id` returned in the creation output should be used for deletion\n- Deleting a non-existent database will result in a failed action status\n- Memory sizes are specified in bytes (e.g., 268435456 = 256MB)\n- Port numbers should be unique and not conflict with existing databases\n\n## Common Eviction Policies\n\n- `noeviction` - Return errors when memory limit is reached\n- `allkeys-lru` - Evict any key using LRU algorithm\n- `volatile-lru` - Evict keys with TTL using LRU algorithm\n- `allkeys-random` - Evict random keys\n- `volatile-random` - Evict random keys with TTL\n- `volatile-ttl` - Evict keys with TTL, shortest TTL first\n\n## Common Proxy Policies\n\n- `all-master-shards` - Route to all master shards\n- `all-nodes` - Route to all nodes\n- `single-shard` - Route to a single shard\n\n"
  },
  {
    "path": "maintnotifications/e2e/README_SCENARIOS.md",
    "content": "# E2E Test Scenarios for Push Notifications\n\nThis directory contains comprehensive end-to-end test scenarios for Redis push notifications and maintenance notifications functionality. Each scenario tests different aspects of the system under various conditions.\n\n## ⚠️ **Important Note**\n**Maintenance notifications are currently supported only in standalone Redis clients.** Cluster clients (ClusterClient, FailoverClient, etc.) do not yet support maintenance notifications functionality.\n\n## Introduction\n\nThese tests support two modes:\n\n### 1. Mock Proxy Mode (Default)\nUses a local Docker-based proxy ([cae-resp-proxy](https://github.com/redis-developer/cae-resp-proxy)) to simulate Redis Enterprise behavior. This mode:\n- Runs entirely locally without external dependencies\n- Provides fast feedback for development\n- Simulates cluster topology changes\n- Supports SMIGRATING and SMIGRATED notifications\n\nTo run in mock proxy mode:\n```bash\nmake test.e2e\n```\n\n### 2. Real Fault Injector Mode\nUses a real Redis Enterprise fault injector service for comprehensive testing. This mode:\n- Tests against actual Redis Enterprise clusters\n- Validates real-world scenarios\n- Requires external fault injector setup\n\nTo run with a real fault injector, set these environment variables:\n- `REDIS_ENDPOINTS_CONFIG_PATH`: Path to Redis endpoints configuration\n- `FAULT_INJECTION_API_URL`: URL of the fault injector server\n- `E2E_SCENARIO_TESTS`: Set to `true` to enable scenario tests\n\nThen run:\n```bash\n./scripts/run-e2e-tests.sh\n```\n\n## Test Scenarios Overview\n\n### 1. Basic Push Notifications (`scenario_push_notifications_test.go`)\n**Original template scenario**\n- **Purpose**: Basic functionality test for Redis Enterprise push notifications\n- **Features Tested**: FAILING_OVER, FAILED_OVER, MIGRATING, MIGRATED, MOVING notifications\n- **Configuration**: Standard enterprise cluster setup\n- **Duration**: ~10 minutes\n- **Key Validations**: \n  - All notification types received\n  - Timeout behavior (relaxed/unrelaxed)\n  - Handoff success rates\n  - Connection pool management\n\n### 2. Endpoint Types Scenario (`scenario_endpoint_types_test.go`)\n**Different endpoint resolution strategies**\n- **Purpose**: Test push notifications with different endpoint types\n- **Features Tested**: ExternalIP, InternalIP, InternalFQDN, ExternalFQDN endpoint types\n- **Configuration**: Standard setup with varying endpoint types\n- **Duration**: ~5 minutes \n- **Key Validations**:\n  - Functionality with each endpoint type\n  - Proper endpoint resolution\n  - Notification delivery consistency\n  - Handoff behavior per endpoint type\n\n### 3. Unified Injector Scenarios (`scenario_unified_injector_test.go`)\n**Mock proxy-based notification testing**\n- **Purpose**: Test SMIGRATING and SMIGRATED notifications with simulated cluster topology changes\n- **Features Tested**:\n  - SMIGRATING notifications (slot migration in progress)\n  - SMIGRATED notifications (slot migration completed)\n  - Cluster topology changes (node swap simulation)\n  - Complex multi-step migration scenarios\n- **Configuration**: Uses local Docker proxy (cae-resp-proxy) with 4 nodes\n- **Duration**: ~10 seconds\n- **Key Validations**:\n  - Notification delivery and parsing\n  - Cluster state reload callbacks\n  - Client resilience during migrations\n  - Topology change handling\n- **Topology Simulation**:\n  - Starts with 4 proxy nodes (17000-17003)\n  - Initially exposes 3 nodes in CLUSTER SLOTS (17000, 17001, 17002)\n  - On SMIGRATED, swaps node 2 for node 3 (simulates node replacement)\n  - Verifies client continues to function after topology change\n\n### 4. Database Management Scenario (`scenario_database_management_test.go`)\n**Dynamic database creation and deletion**\n- **Purpose**: Test database lifecycle management via fault injector\n- **Features Tested**: CREATE_DATABASE, DELETE_DATABASE endpoints\n- **Configuration**: Various database configurations (simple, with modules, clustered)\n- **Duration**: ~10 minutes\n- **Key Validations**:\n  - Database creation with different configurations\n  - Database creation with Redis modules (ReJSON, search, timeseries, bf)\n  - Database deletion\n  - Complete lifecycle (create → use → delete)\n  - Configuration validation\n\nSee [DATABASE_MANAGEMENT.md](DATABASE_MANAGEMENT.md) for detailed documentation on database management endpoints.\n\n### 4. Timeout Configurations Scenario (`scenario_timeout_configs_test.go`)\n**Various timeout strategies**\n- **Purpose**: Test different timeout configurations and their impact\n- **Features Tested**: Conservative, Aggressive, HighLatency timeouts\n- **Configuration**:\n  - Conservative: 60s handoff, 20s relaxed, 5s post-handoff\n  - Aggressive: 5s handoff, 3s relaxed, 1s post-handoff\n  - HighLatency: 90s handoff, 30s relaxed, 10m post-handoff\n- **Duration**: ~10 minutes (3 sub-tests)\n- **Key Validations**:\n  - Timeout behavior matches configuration\n  - Recovery times appropriate for each strategy\n  - Error rates correlate with timeout aggressiveness\n\n### 5. TLS Configurations Scenario (`scenario_tls_configs_test.go`)\n**Security and encryption testing framework**\n- **Purpose**: Test push notifications with different TLS configurations\n- **Features Tested**: NoTLS, TLSInsecure, TLSSecure, TLSMinimal, TLSStrict\n- **Configuration**: Framework for testing various TLS settings (TLS config handled at connection level)\n- **Duration**: ~10 minutes (multiple sub-tests)\n- **Key Validations**:\n  - Functionality with each TLS configuration\n  - Performance impact of encryption\n  - Certificate handling (where applicable)\n  - Security compliance\n- **Note**: TLS configuration is handled at the Redis connection config level, not client options level\n\n### 6. Stress Test Scenario (`scenario_stress_test.go`)\n**Extreme load and concurrent operations**\n- **Purpose**: Test system limits and behavior under extreme stress\n- **Features Tested**: Maximum concurrent operations, multiple clients\n- **Configuration**:\n  - 4 clients with 150 pool size each\n  - 200 max connections per client\n  - 50 workers, 1000 queue size\n  - Concurrent failover/migration actions\n- **Duration**: ~15 minutes\n- **Key Validations**:\n  - System stability under extreme load\n  - Error rates within stress limits (<20%)\n  - Resource utilization and limits\n  - Concurrent fault injection handling\n\n\n## Running the Scenarios\n\n### Prerequisites\n- Set environment variable: `E2E_SCENARIO_TESTS=true`\n- Redis Enterprise cluster available\n- Fault injection service available\n- Appropriate network access and permissions\n- **Note**: Tests use standalone Redis clients only (cluster clients not supported)\n\n### Individual Scenario Execution\n```bash\n# Run a specific scenario\nE2E_SCENARIO_TESTS=true go test -v ./maintnotifications/e2e -run TestEndpointTypesPushNotifications\n\n# Run with timeout\nE2E_SCENARIO_TESTS=true go test -v -timeout 30m ./maintnotifications/e2e -run TestStressPushNotifications\n```\n\n### All Scenarios Execution\n```bash\n./scripts/run-e2e-tests.sh\n```\n## Expected Outcomes\n\n### Success Criteria\n- All notifications received and processed correctly\n- Error rates within acceptable limits for each scenario\n- No notification processing errors\n- Proper timeout behavior\n- Successful handoffs\n- Connection pool management within limits\n\n### Performance Benchmarks\n- **Basic**: >1000 operations, <1% errors\n- **Stress**: >10000 operations, <20% errors\n- **Others**: Functionality over performance\n\n## Troubleshooting\n\n### Common Issues\n1. **Enterprise cluster not available**: Most scenarios require Redis Enterprise\n2. **Fault injector unavailable**: Some scenarios need fault injection service\n3. **Network timeouts**: Increase test timeouts for slow networks\n4. **TLS certificate issues**: Some TLS scenarios may fail without proper certs\n5. **Resource limits**: Stress scenarios may hit system limits\n\n### Debug Options\n- Enable detailed logging in scenarios\n- Use `dump = true` to see full log analysis\n- Check pool statistics for connection issues\n- Monitor client resources during stress tests"
  },
  {
    "path": "maintnotifications/e2e/cluster_maintnotif_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// startBackgroundTraffic starts a background goroutine that continuously sends commands\n// to slots spread across all shards (0, 1001, 2002, ...). This ensures connections remain\n// active on all shards to receive notifications. Returns a stop function to call when done.\n// The slotKeys array is defined in slot_keys.go.\n// The stop function waits for the goroutine to finish before returning.\nfunc startBackgroundTraffic(ctx context.Context, client redis.UniversalClient) (stop func()) {\n\tstopChan := make(chan struct{})\n\tdoneChan := make(chan struct{})\n\tgo func() {\n\t\tdefer close(doneChan)\n\t\tfor {\n\t\t\t// Send commands to slots spread across all shards: 0, 1001, 2002, 3003, ...\n\t\t\tfor slot := 0; slot < 16384; slot += 1001 {\n\t\t\t\tselect {\n\t\t\t\tcase <-stopChan:\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tkey := slotKeys[slot]\n\t\t\t\tclient.Set(ctx, key, \"v\", 0)\n\t\t\t\tclient.Get(ctx, key)\n\t\t\t}\n\t\t}\n\t}()\n\treturn func() {\n\t\tclose(stopChan)\n\t\t<-doneChan // Wait for goroutine to finish\n\t}\n}\n\n// startPubSubConnections creates pubsub subscriptions on channels spread across different slots\n// to ensure pubsub connections are active during slot migrations. Returns a stop function.\n// The pubsub connections use sharded pubsub (SSUBSCRIBE) to ensure they're on specific shards.\n// Returns nil stop function if client is not a ClusterClient.\n// The stop function waits for all goroutines to finish before returning.\nfunc startPubSubConnections(ctx context.Context, t *testing.T, client redis.UniversalClient) (stop func()) {\n\tclusterClient, ok := client.(*redis.ClusterClient)\n\tif !ok {\n\t\tt.Log(\"Skipping pubsub connections - client is not a ClusterClient\")\n\t\treturn func() {}\n\t}\n\tstopChan := make(chan struct{})\n\tvar pubsubs []*redis.PubSub\n\tvar wg sync.WaitGroup\n\n\t// Create pubsub subscriptions on channels in different slots\n\t// Use channels that hash to slots spread across shards: 0, 1001, 2002, ...\n\tfor slot := 0; slot < 16384; slot += 1001 {\n\t\tchannelName := fmt.Sprintf(\"test-channel-%s\", slotKeys[slot])\n\n\t\t// Use sharded pubsub (SSUBSCRIBE) for cluster-aware subscriptions\n\t\tpubsub := clusterClient.SSubscribe(ctx, channelName)\n\t\tpubsubs = append(pubsubs, pubsub)\n\n\t\t// Start a goroutine to receive messages (keeps connection active)\n\t\twg.Add(1)\n\t\tgo func(ps *redis.PubSub, ch string) {\n\t\t\tdefer wg.Done()\n\t\t\tmsgCh := ps.Channel()\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-stopChan:\n\t\t\t\t\treturn\n\t\t\t\tcase msg, ok := <-msgCh:\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif debugE2E() {\n\t\t\t\t\t\tt.Logf(\"PubSub received message on %s: %v\", ch, msg)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}(pubsub, channelName)\n\t}\n\n\t// Start a goroutine to publish messages periodically to keep connections active\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tticker := time.NewTicker(100 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\t\tmsgCount := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-stopChan:\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\t// Publish to channels in different slots\n\t\t\t\tfor slot := 0; slot < 16384; slot += 1001 {\n\t\t\t\t\tchannelName := fmt.Sprintf(\"test-channel-%s\", slotKeys[slot])\n\t\t\t\t\t// Use SPublish for sharded pubsub\n\t\t\t\t\t_ = clusterClient.SPublish(ctx, channelName, fmt.Sprintf(\"msg-%d\", msgCount)).Err()\n\t\t\t\t}\n\t\t\t\tmsgCount++\n\t\t\t}\n\t\t}\n\t}()\n\n\tif debugE2E() {\n\t\tt.Logf(\"✓ Started %d pubsub connections across different slots\", len(pubsubs))\n\t}\n\n\treturn func() {\n\t\tclose(stopChan)\n\t\tfor _, ps := range pubsubs {\n\t\t\t_ = ps.Close()\n\t\t}\n\t\twg.Wait() // Wait for all goroutines to finish\n\t}\n}\n\n// waitForLogsToSettle waits for a fixed duration to allow the log collector\n// to collect all meaningful logs after notifications are received.\nfunc waitForLogsToSettle(t *testing.T, waitDuration time.Duration) {\n\tt.Logf(\"Waiting %v for logs to settle...\", waitDuration)\n\ttime.Sleep(waitDuration)\n\tt.Logf(\"✓ Wait complete\")\n}\n\n// waitForSMigratedOnShardsWithSMigrating waits for SMIGRATED notifications only on shards\n// that received SMIGRATING. This handles topology changes where some shards may be removed.\n// Returns the number of shards that received both SMIGRATING and SMIGRATED.\nfunc waitForSMigratedOnShardsWithSMigrating(t *testing.T, shards []*shardInfo, timeout time.Duration) int {\n\t// First, find shards that received SMIGRATING\n\tvar shardsWithSMigrating []*shardInfo\n\tfor _, shard := range shards {\n\t\tif _, found := shard.hook.FindNotification(\"SMIGRATING\"); found {\n\t\t\tshardsWithSMigrating = append(shardsWithSMigrating, shard)\n\t\t}\n\t}\n\n\tif len(shardsWithSMigrating) == 0 {\n\t\tt.Log(\"No shards received SMIGRATING, skipping SMIGRATED wait\")\n\t\treturn 0\n\t}\n\n\tt.Logf(\"Waiting for SMIGRATED on %d shards that received SMIGRATING...\", len(shardsWithSMigrating))\n\tdeadline := time.Now().Add(timeout)\n\n\tvar shardsWithBoth int\n\tfor time.Now().Before(deadline) {\n\t\tshardsWithBoth = 0\n\t\tfor _, shard := range shardsWithSMigrating {\n\t\t\tif _, found := shard.hook.FindNotification(\"SMIGRATED\"); found {\n\t\t\t\tshardsWithBoth++\n\t\t\t}\n\t\t}\n\t\tif shardsWithBoth == len(shardsWithSMigrating) {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\n\t// Log results for shards that received SMIGRATING\n\tfor _, shard := range shardsWithSMigrating {\n\t\tif _, found := shard.hook.FindNotification(\"SMIGRATED\"); found {\n\t\t\tt.Logf(\"  ✓ Shard %s (nodeAddress=%s): received SMIGRATING + SMIGRATED\",\n\t\t\t\tshard.addr, shard.nodeAddress)\n\t\t} else {\n\t\t\tt.Logf(\"  ✗ Shard %s (nodeAddress=%s): received SMIGRATING but NO SMIGRATED\",\n\t\t\t\tshard.addr, shard.nodeAddress)\n\t\t}\n\t}\n\n\t// Log shards that didn't receive SMIGRATING (for debugging)\n\tfor _, shard := range shards {\n\t\tif _, found := shard.hook.FindNotification(\"SMIGRATING\"); !found {\n\t\t\tt.Logf(\"  - Shard %s (nodeAddress=%s): no SMIGRATING received\",\n\t\t\t\tshard.addr, shard.nodeAddress)\n\t\t}\n\t}\n\n\tt.Logf(\"✓ SMIGRATED received on %d/%d shards (that had SMIGRATING)\", shardsWithBoth, len(shardsWithSMigrating))\n\treturn shardsWithBoth\n}\n\n// shardInfo holds information about a cluster shard for notification tracking\ntype shardInfo struct {\n\taddr        string\n\tnodeAddress string\n\thook        *TrackingNotificationsHook\n}\n\n// printPerShardNotificationSummary prints a summary of notifications received on each shard.\n// This helps debug whether notifications are being received on all nodes or just some.\nfunc printPerShardNotificationSummary(t *testing.T, shards []*shardInfo) {\n\tt.Logf(\"--- Per-Shard Notification Summary ---\")\n\tfor _, shard := range shards {\n\t\tif shard.hook == nil {\n\t\t\tt.Logf(\"  Shard %s (nodeAddress=%s): hook is nil\", shard.addr, shard.nodeAddress)\n\t\t\tcontinue\n\t\t}\n\t\tanalysis := shard.hook.GetAnalysis()\n\t\tt.Logf(\"  Shard %s (nodeAddress=%s): SMIGRATING=%d, SMIGRATED=%d, Total=%d\",\n\t\t\tshard.addr, shard.nodeAddress,\n\t\t\tanalysis.SMigratingCount, analysis.SMigratedCount, analysis.TotalNotifications)\n\t}\n\tt.Logf(\"--------------------------------------\")\n\n\t// Print detailed events from each shard\n\tt.Logf(\"--- Per-Shard Detailed Notification Events ---\")\n\tfor _, shard := range shards {\n\t\tif shard.hook == nil {\n\t\t\tcontinue\n\t\t}\n\t\tevents := shard.hook.GetDiagnosticsLog()\n\t\tif len(events) == 0 {\n\t\t\tt.Logf(\"  Shard %s: no events\", shard.addr)\n\t\t\tcontinue\n\t\t}\n\t\tt.Logf(\"  Shard %s (%d events):\", shard.addr, len(events))\n\t\tfor _, event := range events {\n\t\t\tif event.Pre {\n\t\t\t\tslots := extractSlotsFromEvent(event)\n\t\t\t\tif slots != \"\" {\n\t\t\t\t\tt.Logf(\"    [%s] type=%s connID=%d seqID=%d slots=%s\",\n\t\t\t\t\t\tevent.Timestamp.Format(\"15:04:05.000\"), event.Type, event.ConnID, event.SeqID, slots)\n\t\t\t\t} else {\n\t\t\t\t\tt.Logf(\"    [%s] type=%s connID=%d seqID=%d\",\n\t\t\t\t\t\tevent.Timestamp.Format(\"15:04:05.000\"), event.Type, event.ConnID, event.SeqID)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tt.Logf(\"----------------------------------------------\")\n}\n\n// clearAllTrackers clears the log collector, global tracker, and all per-shard hooks.\n// Call this after setup is complete but before triggering the fault injector\n// to ensure we only count notifications from the actual test action.\nfunc clearAllTrackers(logCollector *TestLogCollector, tracker *TrackingNotificationsHook, shards []*shardInfo) {\n\tif logCollector != nil {\n\t\tlogCollector.Clear()\n\t}\n\tif tracker != nil {\n\t\ttracker.Clear()\n\t}\n\tfor _, shard := range shards {\n\t\tif shard.hook != nil {\n\t\t\tshard.hook.Clear()\n\t\t}\n\t}\n}\n\n// TestProxyFaultInjectorServer_ExistingE2ETest demonstrates how existing e2e tests\n// can work unchanged with the proxy fault injector server\nfunc TestProxyFaultInjectorServer_ClusterExistingE2ETest(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx := context.Background()\n\n\t// Setup database configured for slot-shuffle effect\n\t// This queries the fault injector for the required database configuration\n\tbdbID, factory, testMode, fiClient, factoryCleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)\n\tdefer factoryCleanup()\n\n\t// Set up log collector to track cluster state reloads\n\tlocalLogCollector := NewTestLogCollector()\n\tlocalLogCollector.Clear() // Clear any previous logs\n\tif debugE2E() {\n\t\tlocalLogCollector.DoPrint() // Print logs for debugging\n\t}\n\tredis.SetLogger(localLogCollector)\n\tdefer redis.SetLogger(logCollector) // Restore global logger after test\n\n\tt.Logf(\"✓ Using fault injector client (mode: %s)\", testMode.Mode)\n\tt.Logf(\"  Fault injector base URL: %s\", fiClient.baseURL)\n\n\t// Test 2: Create Redis cluster client via factory\n\tredisClient, err := factory.Create(\"test-client\", &CreateClientOptions{\n\t\tProtocol: 3,\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeEnabled,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create Redis client: %v\", err)\n\t}\n\t// Note: factoryCleanup() handles closing clients via factory.DestroyAll()\n\n\t// Set up notification tracking\n\ttracker := NewTrackingNotificationsHook()\n\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tsetupNotificationHooks(redisClient, tracker, logger)\n\n\t// Verify connection\n\tif err := redisClient.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect to cluster: %v\", err)\n\t}\n\n\t// Debug: Check if maintnotifications manager exists on nodes\n\t// ForEachShard is cluster-specific, so we need to type assert\n\tif clusterClient, ok := redisClient.(*redis.ClusterClient); ok {\n\t\tif checkErr := clusterClient.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {\n\t\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\t\tif manager == nil {\n\t\t\t\tt.Logf(\"⚠️  WARNING: Maintnotifications manager is nil for node: %s\", nodeClient.Options().Addr)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"✓ Maintnotifications manager exists for node: %s\", nodeClient.Options().Addr)\n\t\t\t}\n\t\t\treturn nil\n\t\t}); checkErr != nil {\n\t\t\tt.Logf(\"Warning: Failed to check maintnotifications managers: %v\", checkErr)\n\t\t}\n\t}\n\n\t// Start background traffic to keep connections active on all shards\n\tstopTraffic := startBackgroundTraffic(ctx, redisClient)\n\tdefer stopTraffic()\n\n\t// Start pubsub connections to test notifications on pubsub connections\n\tstopPubSub := startPubSubConnections(ctx, t, redisClient)\n\tdefer stopPubSub()\n\n\t// Give the background traffic and pubsub time to establish connections on all shards\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Clear all trackers before triggering the fault injector\n\t// This ensures we only count notifications from the actual test action\n\tclearAllTrackers(localLogCollector, tracker, nil)\n\n\t// Test 3: Trigger slot migration using the /slot-migrate API\n\t// The database was configured for slot-shuffle effect via SetupTestDatabaseForSlotMigrate\n\tt.Log(\"Triggering slot migration via fault injector API...\")\n\tt.Logf(\"  Database ID: %d\", bdbID)\n\tt.Logf(\"  Effect: slot-shuffle\")\n\tt.Logf(\"  Trigger: migrate\")\n\n\tbdbIDStr := fmt.Sprintf(\"%d\", bdbID)\n\tresp, err := fiClient.TriggerSlotMigrateSlotShuffle(ctx, bdbIDStr, SlotMigrateVariantMigrate)\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to trigger slot migration: %v\", err)\n\t}\n\n\tif debugE2E() {\n\t\tt.Logf(\"✓ Action triggered: %s (status: %s)\", resp.ActionID, resp.Status)\n\t}\n\n\t// Wait for action to complete (commands continue running in background)\n\t// Use testMode timeout - proxy is fast, real fault injector can take up to 5 minutes\n\tstatus, err := fiClient.WaitForAction(ctx, resp.ActionID,\n\t\tWithMaxWaitTime(testMode.ActionWaitTimeout),\n\t\tWithPollInterval(testMode.ActionPollInterval))\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to wait for action: %v\", err)\n\t}\n\n\tif debugE2E() {\n\t\tt.Logf(\"✓ Action completed: %s (status: %s)\", status.ActionID, status.Status)\n\t\tt.Logf(\"  Output: %+v\", status.Output)\n\t}\n\n\t// Check if action failed\n\tif status.Status == StatusFailed {\n\t\tt.Logf(\"⚠️  Action failed with error: %v\", status.Error)\n\t}\n\n\t// Continue operations for a bit longer to ensure notifications are received\n\ttime.Sleep(2 * time.Second)\n\n\t// Verify notifications were received\n\tanalysis := tracker.GetAnalysis()\n\tif analysis.SMigratingCount == 0 {\n\t\tt.Error(\"[ERROR] Expected to receive SMIGRATING notification\")\n\t} else {\n\t\tt.Logf(\"✓ Received %d SMIGRATING notification(s)\", analysis.SMigratingCount)\n\t}\n\n\tif analysis.SMigratedCount == 0 {\n\t\tt.Error(\"[ERROR] Expected to receive SMIGRATED notification\")\n\t} else {\n\t\tt.Logf(\"✓ Received %d SMIGRATED notification(s)\", analysis.SMigratedCount)\n\t}\n\n\t// Print full analysis\n\tanalysis.Print(t)\n\n\t// Get log analysis for cluster state reloads\n\tlogAnalysis := localLogCollector.GetAnalysis()\n\tt.Logf(\"✓ Cluster state reloads: %d\", logAnalysis.ClusterStateReloadCount)\n\tlogAnalysis.Print(t)\n\n\tt.Log(\"✓ Test passed - existing e2e test works with proxy FI server!\")\n}\n\n// TestProxyFaultInjectorServer_ClusterReshard tests cluster resharding\nfunc TestProxyFaultInjectorServer_ClusterReshard(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx := context.Background()\n\n\t// Setup database configured for slot-shuffle effect\n\t// This queries the fault injector for the required database configuration\n\tbdbID, factory, testMode, fiClient, factoryCleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)\n\tdefer factoryCleanup()\n\n\t// Set up log collector to track cluster state reloads\n\tlocalLogCollector := NewTestLogCollector()\n\tlocalLogCollector.Clear() // Clear any previous logs\n\tif debugE2E() {\n\t\tlocalLogCollector.DoPrint() // Print logs for debugging\n\t}\n\tredis.SetLogger(localLogCollector)\n\tdefer redis.SetLogger(logCollector) // Restore global logger after test\n\n\tt.Logf(\"Running cluster reshard test in %s mode\", testMode.Mode)\n\tt.Logf(\"  Fault injector base URL: %s\", fiClient.baseURL)\n\n\t// Create Redis client via factory (uses correct addresses based on mode)\n\tredisClient, err := factory.Create(\"reshard-client\", &CreateClientOptions{\n\t\tProtocol: 3,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create Redis client: %v\", err)\n\t}\n\tdefer redisClient.Close()\n\n\t// Type assertion for cluster-specific methods\n\tclusterClient, ok := redisClient.(*redis.ClusterClient)\n\tif !ok {\n\t\tt.Fatal(\"[ERROR] Expected ClusterClient but got different type\")\n\t}\n\n\ttracker := NewTrackingNotificationsHook()\n\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tsetupNotificationHooks(clusterClient, tracker, logger)\n\n\tif err := clusterClient.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect: %v\", err)\n\t}\n\n\t// Set up per-shard notification tracking\n\tvar shards []*shardInfo\n\tvar shardsMu sync.Mutex\n\n\terr = clusterClient.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {\n\t\tif err := nodeClient.Ping(ctx).Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\taddr := nodeClient.Options().Addr\n\t\tnodeAddress := nodeClient.NodeAddress()\n\n\t\t// Create per-shard tracking hook\n\t\thook := NewTrackingNotificationsHookWithShard(addr)\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.AddNotificationHook(hook)\n\t\t\tt.Logf(\"  ✓ Added per-shard hook for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t} else {\n\t\t\tt.Logf(\"  ⚠️  WARNING: MaintNotificationsManager is nil for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t}\n\n\t\tshardsMu.Lock()\n\t\tshards = append(shards, &shardInfo{\n\t\t\taddr:        addr,\n\t\t\tnodeAddress: nodeAddress,\n\t\t\thook:        hook,\n\t\t})\n\t\tshardsMu.Unlock()\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to ping all shards: %v\", err)\n\t}\n\tt.Logf(\"✓ Discovered and pinged %d shards\", len(shards))\n\n\t// Start background traffic to keep connections active on all shards\n\tstopTraffic := startBackgroundTraffic(ctx, clusterClient)\n\tdefer stopTraffic()\n\n\t// Start pubsub connections to test notifications on pubsub connections\n\tstopPubSub := startPubSubConnections(ctx, t, clusterClient)\n\tdefer stopPubSub()\n\n\t// Give the background traffic and pubsub time to establish connections on all shards\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Clear all trackers before triggering the fault injector\n\t// This ensures we only count notifications from the actual test action\n\tclearAllTrackers(localLogCollector, tracker, shards)\n\n\t// Trigger slot migration with slot-shuffle effect\n\t// The database was configured for slot-shuffle effect via SetupTestDatabaseForSlotMigrate\n\tt.Logf(\"Attempting to trigger slot migration (slot-shuffle effect)...\")\n\tt.Logf(\"  Database ID: %d\", bdbID)\n\tt.Logf(\"  Trigger: %s\", SlotMigrateVariantMigrate)\n\tt.Logf(\"  Fault injector URL: %s\", fiClient.baseURL)\n\n\tbdbIDStr := fmt.Sprintf(\"%d\", bdbID)\n\tresp, err := fiClient.TriggerSlotMigrateSlotShuffle(ctx, bdbIDStr, SlotMigrateVariantMigrate)\n\tif err != nil {\n\t\tt.Logf(\"[ERROR] Failed to trigger slot migration\")\n\t\tt.Logf(\"  Error type: %T\", err)\n\t\tt.Logf(\"  Error details: %v\", err)\n\t\tt.Fatalf(\"[ERROR] Failed to trigger slot migration: %v\", err)\n\t}\n\n\tif debugE2E() {\n\t\tt.Logf(\"✓ Slot migration action triggered successfully\")\n\t\tt.Logf(\"  Action ID: %s\", resp.ActionID)\n\t\tt.Logf(\"  Status: %s\", resp.Status)\n\t}\n\n\t// Wait for completion - use testMode timeout (proxy is fast, real FI can take up to 5 minutes)\n\tstatus, err := fiClient.WaitForAction(ctx, resp.ActionID,\n\t\tWithMaxWaitTime(testMode.ActionWaitTimeout),\n\t\tWithPollInterval(testMode.ActionPollInterval))\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to wait for action: %v\", err)\n\t}\n\n\tif debugE2E() {\n\t\tt.Logf(\"Reshard completed: status=%s, output=%+v\", status.Status, status.Output)\n\t}\n\n\t// Check if action failed\n\tif status.Status == StatusFailed {\n\t\tt.Logf(\"⚠️  Action failed with error: %v\", status.Error)\n\t}\n\n\t// Wait for SMIGRATING notification\n\t_, foundMigrating := tracker.FindOrWaitForNotification(\"SMIGRATING\", testMode.ActionWaitTimeout)\n\tif !foundMigrating {\n\t\tt.Errorf(\"[ERROR] Timed out waiting for SMIGRATING notification\")\n\t} else {\n\t\tt.Logf(\"✓ Received SMIGRATING notification\")\n\t}\n\n\t// Wait for SMIGRATED notification (at least one)\n\t_, foundMigrated := tracker.FindOrWaitForNotification(\"SMIGRATED\", testMode.ActionWaitTimeout)\n\tif !foundMigrated {\n\t\tt.Errorf(\"[ERROR] Timed out waiting for SMIGRATED notification\")\n\t} else {\n\t\tt.Logf(\"✓ Received SMIGRATED notification\")\n\t}\n\n\t// Wait for SMIGRATED only on shards that received SMIGRATING\n\twaitForSMigratedOnShardsWithSMigrating(t, shards, testMode.ActionWaitTimeout)\n\n\t// Wait for logs to settle\n\twaitForLogsToSettle(t, 2*time.Second)\n\n\t// Print per-shard notification summary\n\tprintPerShardNotificationSummary(t, shards)\n\n\t// Print final analysis\n\tanalysis := tracker.GetAnalysis()\n\tanalysis.Print(t)\n\n\t// Get log analysis for cluster state reloads\n\tlogAnalysis := localLogCollector.GetAnalysis()\n\tt.Logf(\"✓ Cluster state reloads: %d\", logAnalysis.ClusterStateReloadCount)\n\tlogAnalysis.Print(t)\n\n\tt.Log(\"✓ Cluster reshard test passed\")\n}\n\n// TestProxyFaultInjectorServer_WithEnvironment shows how to use environment variables\n// to make existing tests work with either real FI or proxy FI\nfunc TestProxyFaultInjectorServer_WithEnvironment(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx := context.Background()\n\n\t// Setup database configured for slot-shuffle effect\n\t// This queries the fault injector for the required database configuration\n\tbdbID, factory, testMode, client, factoryCleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)\n\tdefer factoryCleanup()\n\n\t// Set up log collector to track cluster state reloads\n\tlocalLogCollector := NewTestLogCollector()\n\tlocalLogCollector.Clear() // Clear any previous logs\n\tif debugE2E() {\n\t\tlocalLogCollector.DoPrint() // Print logs for debugging\n\t}\n\tredis.SetLogger(localLogCollector)\n\tdefer redis.SetLogger(logCollector) // Restore global logger after test\n\n\tt.Logf(\"Running test in %s mode\", testMode.Mode)\n\n\t// Create Redis client via factory (uses correct addresses based on mode)\n\tredisClient, err := factory.Create(\"env-client\", &CreateClientOptions{\n\t\tProtocol: 3,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create Redis client: %v\", err)\n\t}\n\tdefer redisClient.Close()\n\n\ttracker := NewTrackingNotificationsHook()\n\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tsetupNotificationHooks(redisClient, tracker, logger)\n\n\tif err := redisClient.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect: %v\", err)\n\t}\n\n\t// Set up per-shard notification tracking for cluster clients\n\tvar shards []*shardInfo\n\tvar shardsMu sync.Mutex\n\n\t// Type assertion for cluster-specific methods\n\tclusterClient, isCluster := redisClient.(*redis.ClusterClient)\n\tif isCluster {\n\t\terr = clusterClient.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {\n\t\t\tif err := nodeClient.Ping(ctx).Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\taddr := nodeClient.Options().Addr\n\t\t\tnodeAddress := nodeClient.NodeAddress()\n\n\t\t\t// Create per-shard tracking hook\n\t\t\thook := NewTrackingNotificationsHookWithShard(addr)\n\t\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\t\tif manager != nil {\n\t\t\t\tmanager.AddNotificationHook(hook)\n\t\t\t\tt.Logf(\"  ✓ Added per-shard hook for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"  ⚠️  WARNING: MaintNotificationsManager is nil for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t\t}\n\n\t\t\tshardsMu.Lock()\n\t\t\tshards = append(shards, &shardInfo{\n\t\t\t\taddr:        addr,\n\t\t\t\tnodeAddress: nodeAddress,\n\t\t\t\thook:        hook,\n\t\t\t})\n\t\t\tshardsMu.Unlock()\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"[ERROR] Failed to ping all shards: %v\", err)\n\t\t}\n\t\tt.Logf(\"✓ Discovered and pinged %d shards\", len(shards))\n\n\t}\n\n\t// Start background traffic to keep connections active on all shards\n\tstopTraffic := startBackgroundTraffic(ctx, redisClient)\n\tdefer stopTraffic()\n\n\t// Start pubsub connections to test notifications on pubsub connections\n\tstopPubSub := startPubSubConnections(ctx, t, redisClient)\n\tdefer stopPubSub()\n\n\t// Give the background traffic and pubsub time to establish connections on all shards\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Clear all trackers before triggering the fault injector\n\t// This ensures we only count notifications from the actual test action\n\tclearAllTrackers(localLogCollector, tracker, shards)\n\n\t// Trigger slot migration using the /slot-migrate API with correct bdb_id\n\t// The database was configured for slot-shuffle effect via SetupTestDatabaseForSlotMigrate\n\tt.Logf(\"Triggering slot migration with bdb_id=%d (effect: slot-shuffle)\", bdbID)\n\tbdbIDStr := fmt.Sprintf(\"%d\", bdbID)\n\tresp, err := client.TriggerSlotMigrateSlotShuffle(ctx, bdbIDStr, SlotMigrateVariantMigrate)\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to trigger migration: %v\", err)\n\t}\n\n\tstatus, err := client.WaitForAction(ctx, resp.ActionID,\n\t\tWithMaxWaitTime(testMode.ActionWaitTimeout),\n\t\tWithPollInterval(testMode.ActionPollInterval))\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to wait for action: %v\", err)\n\t}\n\n\tif debugE2E() {\n\t\tt.Logf(\"Action completed: %s\", status.Status)\n\t}\n\tif status.Status == StatusFailed {\n\t\tt.Logf(\"⚠️  Action failed with error: %v\", status.Error)\n\t}\n\n\ttime.Sleep(2 * time.Second)\n\n\t// Wait for SMIGRATED only on shards that received SMIGRATING (for cluster clients)\n\tif isCluster && len(shards) > 0 {\n\t\twaitForSMigratedOnShardsWithSMigrating(t, shards, testMode.ActionWaitTimeout)\n\n\t\t// Wait for logs to settle\n\t\twaitForLogsToSettle(t, 2*time.Second)\n\t}\n\n\t// Print per-shard notification summary\n\tprintPerShardNotificationSummary(t, shards)\n\n\tanalysis := tracker.GetAnalysis()\n\tanalysis.Print(t)\n\n\t// Get log analysis for cluster state reloads\n\tlogAnalysis := localLogCollector.GetAnalysis()\n\tt.Logf(\"✓ Cluster state reloads: %d\", logAnalysis.ClusterStateReloadCount)\n\tlogAnalysis.Print(t)\n\n\tt.Log(\"✓ Test passed with either real or proxy fault injector!\")\n}\n\n// TestProxyFaultInjectorServer_MultipleActions tests multiple sequential actions\nfunc TestProxyFaultInjectorServer_ClusterMultipleActions(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx := context.Background()\n\n\t// Setup database configured for slot-shuffle effect\n\t// This queries the fault injector for the required database configuration\n\tbdbID, factory, testMode, client, factoryCleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)\n\tdefer factoryCleanup()\n\n\t// Set up log collector to track cluster state reloads\n\tlocalLogCollector := NewTestLogCollector()\n\tlocalLogCollector.Clear() // Clear any previous logs\n\tif debugE2E() {\n\t\tlocalLogCollector.DoPrint() // Print logs for debugging\n\t}\n\tredis.SetLogger(localLogCollector)\n\tdefer redis.SetLogger(logCollector) // Restore global logger after test\n\n\tt.Logf(\"Running test in %s mode\", testMode.Mode)\n\tt.Logf(\"Database ID: %d\", bdbID)\n\n\t// Create Redis client via factory (uses correct addresses based on mode)\n\tredisClient, err := factory.Create(\"multi-action-client\", &CreateClientOptions{\n\t\tProtocol: 3,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create Redis client: %v\", err)\n\t}\n\tdefer redisClient.Close()\n\n\ttracker := NewTrackingNotificationsHook()\n\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tsetupNotificationHooks(redisClient, tracker, logger)\n\n\tif err := redisClient.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect: %v\", err)\n\t}\n\n\t// Set up per-shard notification tracking for cluster clients\n\tvar shards []*shardInfo\n\tvar shardsMu sync.Mutex\n\n\t// Type assertion for cluster-specific methods\n\tclusterClient, isCluster := redisClient.(*redis.ClusterClient)\n\tif isCluster {\n\t\terr = clusterClient.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {\n\t\t\tif err := nodeClient.Ping(ctx).Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\taddr := nodeClient.Options().Addr\n\t\t\tnodeAddress := nodeClient.NodeAddress()\n\n\t\t\t// Create per-shard tracking hook\n\t\t\thook := NewTrackingNotificationsHookWithShard(addr)\n\t\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\t\tif manager != nil {\n\t\t\t\tmanager.AddNotificationHook(hook)\n\t\t\t\tt.Logf(\"  ✓ Added per-shard hook for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"  ⚠️  WARNING: MaintNotificationsManager is nil for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t\t}\n\n\t\t\tshardsMu.Lock()\n\t\t\tshards = append(shards, &shardInfo{\n\t\t\t\taddr:        addr,\n\t\t\t\tnodeAddress: nodeAddress,\n\t\t\t\thook:        hook,\n\t\t\t})\n\t\t\tshardsMu.Unlock()\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"[ERROR] Failed to ping all shards: %v\", err)\n\t\t}\n\t\tt.Logf(\"✓ Discovered and pinged %d shards\", len(shards))\n\n\t}\n\n\t// Start background traffic to keep connections active on all shards\n\tstopTraffic := startBackgroundTraffic(ctx, redisClient)\n\tdefer stopTraffic()\n\n\t// Start pubsub connections to test notifications on pubsub connections\n\tstopPubSub := startPubSubConnections(ctx, t, redisClient)\n\tdefer stopPubSub()\n\n\t// Give the background traffic and pubsub time to establish connections on all shards\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Clear all trackers before triggering the fault injector\n\t// This ensures we only count notifications from the actual test action\n\tclearAllTrackers(localLogCollector, tracker, shards)\n\n\t// Trigger multiple slot-shuffle migrations using the correct API\n\t// The database was configured for slot-shuffle effect via SetupTestDatabaseForSlotMigrate\n\tbdbIDStr := fmt.Sprintf(\"%d\", bdbID)\n\tnumMigrations := 3\n\n\tfor i := 0; i < numMigrations; i++ {\n\t\tif debugE2E() {\n\t\t\tt.Logf(\"Migration %d: triggering slot-shuffle\", i+1)\n\t\t}\n\n\t\tresp, err := client.TriggerSlotMigrateSlotShuffle(ctx, bdbIDStr, SlotMigrateVariantMigrate)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"[ERROR] Failed to trigger migration %d: %v\", i+1, err)\n\t\t}\n\n\t\tstatus, err := client.WaitForAction(ctx, resp.ActionID,\n\t\t\tWithMaxWaitTime(testMode.ActionWaitTimeout),\n\t\t\tWithPollInterval(testMode.ActionPollInterval))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"[ERROR] Failed to wait for migration %d: %v\", i+1, err)\n\t\t}\n\n\t\tif debugE2E() {\n\t\t\tt.Logf(\"  ✓ Migration %d completed: %s\", i+1, status.Status)\n\t\t}\n\t\tif status.Status == StatusFailed {\n\t\t\tt.Logf(\"  ⚠️  Migration %d failed with error: %v\", i+1, status.Error)\n\t\t}\n\n\t\t// Wait between migrations to allow notifications to be processed\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\n\ttime.Sleep(2 * time.Second)\n\n\tanalysis := tracker.GetAnalysis()\n\tt.Logf(\"Total SMIGRATING: %d\", analysis.SMigratingCount)\n\tt.Logf(\"Total SMIGRATED: %d\", analysis.SMigratedCount)\n\n\tif analysis.SMigratingCount < int64(numMigrations) {\n\t\tt.Errorf(\"[ERROR] Expected at least %d SMIGRATING notifications, got %d\",\n\t\t\tnumMigrations, analysis.SMigratingCount)\n\t}\n\n\t// Wait for SMIGRATED only on shards that received SMIGRATING (for cluster clients)\n\tif isCluster && len(shards) > 0 {\n\t\twaitForSMigratedOnShardsWithSMigrating(t, shards, testMode.ActionWaitTimeout)\n\n\t\t// Wait for logs to settle\n\t\twaitForLogsToSettle(t, 2*time.Second)\n\t}\n\n\t// Print per-shard notification summary\n\tprintPerShardNotificationSummary(t, shards)\n\n\tanalysis.Print(t)\n\n\t// Get log analysis for cluster state reloads\n\tlogAnalysis := localLogCollector.GetAnalysis()\n\tt.Logf(\"✓ Cluster state reloads: %d\", logAnalysis.ClusterStateReloadCount)\n\tlogAnalysis.Print(t)\n\n\tt.Log(\"✓ Multiple actions test passed\")\n}\n\n// TestProxyFaultInjectorServer_NewConnectionsReceiveNotifications tests that new connections\n// joining during an active migration receive the SMIGRATING notification\nfunc TestProxyFaultInjectorServer_ClusterNewConnectionsReceiveNotifications(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)\n\tdefer cancel()\n\n\t// Setup database configured for slot-shuffle effect\n\t// This queries the fault injector for the required database configuration\n\tbdbID, factory, testMode, fiClient, factoryCleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)\n\tdefer factoryCleanup()\n\n\t// Set up log collector to track cluster state reloads\n\tlocalLogCollector := NewTestLogCollector()\n\tlocalLogCollector.Clear() // Clear any previous logs\n\tif debugE2E() {\n\t\tlocalLogCollector.DoPrint() // Print logs for debugging\n\t}\n\tredis.SetLogger(localLogCollector)\n\tdefer redis.SetLogger(logCollector) // Restore global logger after test\n\n\tt.Logf(\"Running test in %s mode\", testMode.Mode)\n\tt.Logf(\"Database ID: %d\", bdbID)\n\n\t// Test 2: Create first Redis client via factory\n\tclient1Iface, err := factory.Create(\"client1\", &CreateClientOptions{\n\t\tProtocol: 3,\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeEnabled,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create client1: %v\", err)\n\t}\n\tclient1 := client1Iface.(*redis.ClusterClient)\n\tdefer client1.Close()\n\n\ttracker1 := NewTrackingNotificationsHook()\n\tlogger1 := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tsetupNotificationHooks(client1, tracker1, logger1)\n\n\tif err := client1.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect client1: %v\", err)\n\t}\n\n\t// Enable CLIENT TRACKING to ensure the proxy sends push notifications\n\tif err := client1.Do(ctx, \"CLIENT\", \"TRACKING\", \"ON\").Err(); err != nil {\n\t\tt.Logf(\"Warning: Failed to enable CLIENT TRACKING: %v\", err)\n\t}\n\n\t// Keep connections active by continuously executing commands\n\tstopChan1 := make(chan struct{})\n\tdefer close(stopChan1)\n\n\tgo func() {\n\t\ti := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-stopChan1:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tclient1.Set(ctx, fmt.Sprintf(\"key1-%d\", i%100), fmt.Sprintf(\"value-%d\", i), 0)\n\t\t\t\tclient1.Ping(ctx)\n\t\t\t\ti++\n\t\t\t\ttime.Sleep(5 * time.Millisecond)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Give the command loop time to establish connections\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Test 3: Trigger a slot migration (this will send SMIGRATING, wait 500ms, then send SMIGRATED)\n\t// We'll create a new client during the 500ms window\n\t// The database was configured for slot-shuffle effect via SetupTestDatabaseForSlotMigrate\n\tt.Log(\"Triggering slot migration (effect: slot-shuffle)...\")\n\tbdbIDStr := fmt.Sprintf(\"%d\", bdbID)\n\tresp, err := fiClient.TriggerSlotMigrateSlotShuffle(ctx, bdbIDStr, SlotMigrateVariantMigrate)\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to trigger slot migration: %v\", err)\n\t}\n\tif debugE2E() {\n\t\tt.Logf(\"Action triggered: %s\", resp.ActionID)\n\t}\n\n\t// Wait a bit for SMIGRATING to be sent\n\ttime.Sleep(200 * time.Millisecond)\n\n\t// Test 4: Create second client DURING the migration (should receive SMIGRATING)\n\tt.Log(\"Creating second client during migration...\")\n\tclient2Iface, err := factory.Create(\"client2\", &CreateClientOptions{\n\t\tProtocol: 3,\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeEnabled,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create client2: %v\", err)\n\t}\n\tclient2 := client2Iface.(*redis.ClusterClient)\n\tdefer client2.Close()\n\n\ttracker2 := NewTrackingNotificationsHook()\n\tlogger2 := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tsetupNotificationHooks(client2, tracker2, logger2)\n\n\tif err := client2.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect client2: %v\", err)\n\t}\n\n\t// Enable CLIENT TRACKING to ensure the proxy sends push notifications\n\tif err := client2.Do(ctx, \"CLIENT\", \"TRACKING\", \"ON\").Err(); err != nil {\n\t\tt.Logf(\"Warning: Failed to enable CLIENT TRACKING: %v\", err)\n\t}\n\n\t// Keep connections active for client2 as well\n\tstopChan2 := make(chan struct{})\n\tdefer close(stopChan2)\n\n\tgo func() {\n\t\ti := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-stopChan2:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tclient2.Set(ctx, fmt.Sprintf(\"key2-%d\", i%100), fmt.Sprintf(\"value-%d\", i), 0)\n\t\t\t\tclient2.Ping(ctx)\n\t\t\t\ti++\n\t\t\t\ttime.Sleep(5 * time.Millisecond)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Give the command loop time to establish connections\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Wait for migration to complete - use testMode timeout (proxy is fast, real FI can take up to 5 minutes)\n\tstatus, err := fiClient.WaitForAction(ctx, resp.ActionID,\n\t\tWithMaxWaitTime(testMode.ActionWaitTimeout),\n\t\tWithPollInterval(testMode.ActionPollInterval))\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to wait for action: %v\", err)\n\t}\n\tif status.Status == StatusFailed {\n\t\tt.Logf(\"⚠️  Action failed with error: %v\", status.Error)\n\t}\n\n\t// Wait a bit for notifications to be processed\n\ttime.Sleep(2 * time.Second)\n\n\t// Test 5: Verify both clients received notifications\n\tanalysis1 := tracker1.GetAnalysis()\n\tanalysis2 := tracker2.GetAnalysis()\n\n\tt.Logf(\"Client1 - SMIGRATING: %d, SMIGRATED: %d\",\n\t\tanalysis1.SMigratingCount,\n\t\tanalysis1.SMigratedCount)\n\tt.Logf(\"Client2 - SMIGRATING: %d, SMIGRATED: %d\",\n\t\tanalysis2.SMigratingCount,\n\t\tanalysis2.SMigratedCount)\n\n\t// Client1 should have received both SMIGRATING and SMIGRATED\n\tif analysis1.SMigratingCount == 0 {\n\t\tt.Error(\"[ERROR] Client1 should have received SMIGRATING notification\")\n\t}\n\tif analysis1.SMigratedCount == 0 {\n\t\tt.Error(\"[ERROR] Client1 should have received SMIGRATED notification\")\n\t}\n\n\t// Client2 (joined late) should have received at least SMIGRATING\n\t// It might also receive SMIGRATED if it joined early enough\n\tif analysis2.SMigratingCount == 0 {\n\t\tt.Error(\"[ERROR] Client2 (joined during migration) should have received SMIGRATING notification from active notification tracking\")\n\t}\n\n\t// Get log analysis for cluster state reloads\n\tlogAnalysis := localLogCollector.GetAnalysis()\n\tt.Logf(\"✓ Cluster state reloads: %d\", logAnalysis.ClusterStateReloadCount)\n\tlogAnalysis.Print(t)\n\n\tt.Log(\"✓ New connections receive active notifications test passed\")\n}\n\n// TestSlotMigrate_AllEffects tests the new /slot-migrate API endpoint with all effects\n// This tests the OSS Cluster API client behavior during endpoint topology changes\n// Each subtest creates a fresh database with the required configuration for that effect/variant\nfunc TestClusterSlotMigrate_AllEffects(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx := context.Background()\n\n\t// Test all valid effect-variant combinations\n\t// Based on ACTIONS.md compatibility matrix:\n\t// - remove-add: migrate, maintenance_mode, failover (3)\n\t// - remove: migrate, maintenance_mode (2)\n\t// - add: migrate, failover (2)\n\t// - slot-shuffle: migrate, failover (2)\n\t//\n\t// All slot-migrate effects should generate SMIGRATING/SMIGRATED notifications\n\t// that are received on ALL shards in the cluster.\n\ttestCases := []struct {\n\t\tname    string\n\t\teffect  SlotMigrateEffect\n\t\ttrigger SlotMigrateVariant\n\t}{\n\t\t// slot-shuffle effect (2 variants) - no node changes\n\t\t{\n\t\t\tname:    \"SlotShuffle_Migrate\",\n\t\t\teffect:  SlotMigrateEffectSlotShuffle,\n\t\t\ttrigger: SlotMigrateVariantMigrate,\n\t\t},\n\t\t{\n\t\t\tname:    \"SlotShuffle_Failover\",\n\t\t\teffect:  SlotMigrateEffectSlotShuffle,\n\t\t\ttrigger: SlotMigrateVariantFailover,\n\t\t\t// For cluster scenarios, slot-shuffle + failover should still send SMIGRATING/SMIGRATED\n\t\t\t// because the slots effectively move from one endpoint to another when master/replica swap\n\t\t},\n\t\t// add effect (2 variants) - one node added\n\t\t{\n\t\t\tname:    \"Add_Migrate\",\n\t\t\teffect:  SlotMigrateEffectAdd,\n\t\t\ttrigger: SlotMigrateVariantMigrate,\n\t\t},\n\t\t{\n\t\t\tname:    \"Add_Failover\",\n\t\t\teffect:  SlotMigrateEffectAdd,\n\t\t\ttrigger: SlotMigrateVariantFailover,\n\t\t},\n\t\t// remove-add effect (3 variants) - net zero node change\n\t\t{\n\t\t\tname:    \"RemoveAdd_Migrate\",\n\t\t\teffect:  SlotMigrateEffectRemoveAdd,\n\t\t\ttrigger: SlotMigrateVariantMigrate,\n\t\t},\n\t\t{\n\t\t\tname:    \"RemoveAdd_Failover\",\n\t\t\teffect:  SlotMigrateEffectRemoveAdd,\n\t\t\ttrigger: SlotMigrateVariantFailover,\n\t\t},\n\t\t// remove effect - one node removed\n\t\t{\n\t\t\tname:    \"Remove_Migrate\",\n\t\t\teffect:  SlotMigrateEffectRemove,\n\t\t\ttrigger: SlotMigrateVariantMigrate,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\t// Get the number of requirements for this effect/variant\n\t\t// Each requirement represents a different database configuration that should be tested\n\t\treqCount := GetSlotMigrateRequirementsCount(t, ctx, tc.effect, tc.trigger)\n\n\t\tfor reqIdx := 0; reqIdx < reqCount; reqIdx++ {\n\t\t\treqIdx := reqIdx // capture loop variable\n\t\t\ttestName := tc.name\n\t\t\tif reqCount > 1 {\n\t\t\t\ttestName = fmt.Sprintf(\"%s_Req%d\", tc.name, reqIdx)\n\t\t\t}\n\n\t\t\tt.Run(testName, func(t *testing.T) {\n\t\t\t\tt.Logf(\"Testing slot-migrate effect: %s, variant: %s (requirement %d/%d)\",\n\t\t\t\t\ttc.effect, tc.trigger, reqIdx+1, reqCount)\n\n\t\t\t\t// Create a fresh database configured for this specific effect/variant/requirement\n\t\t\t\t// This queries the fault injector for the required database configuration\n\t\t\t\tbdbID, factory, testMode, fiClient, factoryCleanup := SetupTestDatabaseForSlotMigrateWithRequirementIndex(t, ctx, tc.effect, tc.trigger, reqIdx)\n\t\t\t\tdefer factoryCleanup()\n\n\t\t\t\tt.Logf(\"✓ Created database %d for effect %s, variant %s, requirement %d (mode: %s)\",\n\t\t\t\t\tbdbID, tc.effect, tc.trigger, reqIdx, testMode.Mode)\n\n\t\t\t\t// Set up log collector to track cluster state reloads\n\t\t\t\tlocalLogCollector := NewTestLogCollector()\n\t\t\t\tlocalLogCollector.Clear() // Clear any previous logs\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tlocalLogCollector.DoPrint() // Print logs for debugging\n\t\t\t\t}\n\t\t\t\tredis.SetLogger(localLogCollector)\n\t\t\t\tdefer redis.SetLogger(logCollector) // Restore global logger after test\n\n\t\t\t\t// Create Redis cluster client connected to the new database\n\t\t\t\tredisClientIface, err := factory.Create(\"redisClient\", &CreateClientOptions{\n\t\t\t\t\tProtocol: 3,\n\t\t\t\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\t\t\t\tMode: maintnotifications.ModeEnabled,\n\t\t\t\t\t},\n\t\t\t\t\t// Disable automatic cluster state reloads so we only get notification-triggered reloads\n\t\t\t\t\tClusterStateReloadInterval: 10 * time.Minute,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"[ERROR] Failed to create Redis client: %v\", err)\n\t\t\t\t}\n\t\t\t\tredisClient := redisClientIface.(*redis.ClusterClient)\n\t\t\t\tdefer redisClient.Close()\n\n\t\t\t\t// Verify connection first - this triggers cluster discovery\n\t\t\t\tif err := redisClient.Ping(ctx).Err(); err != nil {\n\t\t\t\t\tt.Fatalf(\"[ERROR] Failed to connect to cluster: %v\", err)\n\t\t\t\t}\n\t\t\t\tt.Log(\"✓ Connected to cluster\")\n\n\t\t\t\t// Force discovery of all shards by pinging each one\n\t\t\t\t// This ensures ForEachShard will iterate over all shards\n\t\t\t\t// Also set up per-shard notification tracking\n\t\t\t\tvar shards []*shardInfo\n\t\t\t\tvar shardsMu sync.Mutex\n\n\t\t\t\terr = redisClient.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {\n\t\t\t\t\tif err := nodeClient.Ping(ctx).Err(); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\taddr := nodeClient.Options().Addr\n\t\t\t\t\tnodeAddress := nodeClient.NodeAddress()\n\n\t\t\t\t\t// Create per-shard tracking hook\n\t\t\t\t\thook := NewTrackingNotificationsHookWithShard(addr)\n\t\t\t\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\t\t\t\tif manager != nil {\n\t\t\t\t\t\tmanager.AddNotificationHook(hook)\n\t\t\t\t\t\tt.Logf(\"  ✓ Added per-shard hook for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Logf(\"  ⚠️  WARNING: MaintNotificationsManager is nil for %s (nodeAddress=%s)\", addr, nodeAddress)\n\t\t\t\t\t}\n\n\t\t\t\t\tshardsMu.Lock()\n\t\t\t\t\tshards = append(shards, &shardInfo{\n\t\t\t\t\t\taddr:        addr,\n\t\t\t\t\t\tnodeAddress: nodeAddress,\n\t\t\t\t\t\thook:        hook,\n\t\t\t\t\t})\n\t\t\t\t\tshardsMu.Unlock()\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"[ERROR] Failed to ping all shards: %v\", err)\n\t\t\t\t}\n\t\t\t\tt.Logf(\"✓ Discovered and pinged %d shards\", len(shards))\n\n\t\t\t\t// Also set up a global tracker for backward compatibility\n\t\t\t\ttracker := NewTrackingNotificationsHook()\n\t\t\t\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\t\t\t\tsetupNotificationHooks(redisClient, tracker, logger)\n\n\t\t\t\t// Start background traffic to keep connections active on all shards\n\t\t\t\tstopTraffic := startBackgroundTraffic(ctx, redisClient)\n\t\t\t\tdefer stopTraffic()\n\n\t\t\t\t// Start pubsub connections to test notifications on pubsub connections\n\t\t\t\tstopPubSub := startPubSubConnections(ctx, t, redisClient)\n\t\t\t\tdefer stopPubSub()\n\n\t\t\t\t// Give the background traffic and pubsub time to establish connections on all shards\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t\t\t// Clear all trackers before triggering the fault injector\n\t\t\t\t// This ensures we only count notifications from the actual test action\n\t\t\t\tclearAllTrackers(localLogCollector, tracker, shards)\n\n\t\t\t\t// Trigger slot migration on the database we just created\n\t\t\t\tbdbIDStr := fmt.Sprintf(\"%d\", bdbID)\n\t\t\t\tresp, err := fiClient.TriggerSlotMigrate(ctx, SlotMigrateRequest{\n\t\t\t\t\tEffect:  tc.effect,\n\t\t\t\t\tBdbID:   bdbIDStr,\n\t\t\t\t\tTrigger: tc.trigger,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"[ERROR] Failed to trigger slot migration: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"✓ Action triggered: %s (status: %s)\", resp.ActionID, resp.Status)\n\t\t\t\t}\n\n\t\t\t\t// Wait for action to complete - use testMode timeout\n\t\t\t\tstatus, err := fiClient.WaitForAction(ctx, resp.ActionID,\n\t\t\t\t\tWithMaxWaitTime(testMode.ActionWaitTimeout),\n\t\t\t\t\tWithPollInterval(testMode.ActionPollInterval))\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"[ERROR] Failed to wait for action: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"✓ Action completed: %s (status: %s)\", status.ActionID, status.Status)\n\t\t\t\t}\n\n\t\t\t\t// Wait for appropriate notifications based on the effect/variant combination\n\t\t\t\t// For all slot-migrate effects, expect SMIGRATING/SMIGRATED notifications\n\t\t\t\t_, foundMigrating := tracker.FindOrWaitForNotification(\"SMIGRATING\", testMode.ActionWaitTimeout)\n\t\t\t\tif !foundMigrating {\n\t\t\t\t\tt.Errorf(\"[ERROR] Timed out waiting for SMIGRATING notification for effect %s\", tc.effect)\n\t\t\t\t} else {\n\t\t\t\t\tt.Logf(\"✓ Received SMIGRATING notification\")\n\t\t\t\t}\n\n\t\t\t\t// Wait for SMIGRATED notification (may take longer to arrive)\n\t\t\t\t// Use the same timeout as action wait - 5 minutes for real fault injector\n\t\t\t\t_, foundMigrated := tracker.FindOrWaitForNotification(\"SMIGRATED\", testMode.ActionWaitTimeout)\n\t\t\t\tif !foundMigrated {\n\t\t\t\t\tt.Errorf(\"[ERROR] Timed out waiting for SMIGRATED notification for effect %s\", tc.effect)\n\t\t\t\t} else {\n\t\t\t\t\tt.Logf(\"✓ Received SMIGRATED notification\")\n\t\t\t\t}\n\n\t\t\t\t// Wait for SMIGRATED only on shards that received SMIGRATING\n\t\t\t\twaitForSMigratedOnShardsWithSMigrating(t, shards, testMode.ActionWaitTimeout)\n\n\t\t\t\t// Wait for logs to settle\n\t\t\t\twaitForLogsToSettle(t, 2*time.Second)\n\n\t\t\t\t// Print per-shard notification summary\n\t\t\t\tprintPerShardNotificationSummary(t, shards)\n\n\t\t\t\t// Print final notification tracker analysis\n\t\t\t\tanalysis := tracker.GetAnalysis()\n\t\t\t\tanalysis.Print(t)\n\n\t\t\t\tt.Logf(\"✓ Received %d SMIGRATING notification(s)\", analysis.SMigratingCount)\n\t\t\t\tt.Logf(\"✓ Received %d SMIGRATED notification(s)\", analysis.SMigratedCount)\n\n\t\t\t\tif analysis.NotificationProcessingErrors > 0 {\n\t\t\t\t\tt.Errorf(\"[ERROR] Got %d notification processing errors\", analysis.NotificationProcessingErrors)\n\t\t\t\t}\n\n\t\t\t\t// Get log analysis for cluster state reloads\n\t\t\t\tlogAnalysis := localLogCollector.GetAnalysis()\n\t\t\t\tt.Logf(\"✓ Cluster state reloads: %d\", logAnalysis.ClusterStateReloadCount)\n\t\t\t\tlogAnalysis.Print(t)\n\t\t\t})\n\t\t}\n\t}\n\n\tt.Log(\"✓ All slot-migrate effects tested successfully!\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/cmd/proxy-fi-server/Dockerfile",
    "content": "# Build stage\nFROM golang:1.24-alpine AS builder\n\nWORKDIR /build\n\n# Copy go mod files\nCOPY go.mod go.sum ./\nRUN go mod download\n\n# Copy source code\nCOPY . .\n\n# Build the proxy-fi-server binary\nRUN cd maintnotifications/e2e/cmd/proxy-fi-server && \\\n    CGO_ENABLED=0 GOOS=linux go build -o /proxy-fi-server .\n\n# Runtime stage\nFROM alpine:latest\n\nRUN apk --no-cache add ca-certificates\n\n# Create a non-root user\nRUN addgroup -g 1000 appuser && \\\n    adduser -D -u 1000 -G appuser appuser\n\nWORKDIR /app\n\n# Copy the binary from builder\nCOPY --from=builder /proxy-fi-server .\n\n# Change ownership of the app directory\nRUN chown -R appuser:appuser /app\n\n# Switch to non-root user\nUSER appuser\n\n# Expose the fault injector API port\nEXPOSE 5000\n\n# Run the server\nENTRYPOINT [\"/app/proxy-fi-server\"]\nCMD [\"--listen\", \"0.0.0.0:5000\", \"--proxy-api-url\", \"http://cae-resp-proxy:3000\"]\n\n"
  },
  {
    "path": "maintnotifications/e2e/cmd/proxy-fi-server/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\te2e \"github.com/redis/go-redis/v9/maintnotifications/e2e\"\n)\n\nfunc main() {\n\tlistenAddr := flag.String(\"listen\", \"0.0.0.0:5000\", \"Address to listen on for fault injector API\")\n\tproxyAPIURL := flag.String(\"proxy-api-url\", \"http://localhost:18100\", \"URL of the cae-resp-proxy API (updated to avoid macOS Control Center conflict)\")\n\tflag.Parse()\n\n\tfmt.Printf(\"Starting Proxy Fault Injector Server...\\n\")\n\tfmt.Printf(\"  Listen address: %s\\n\", *listenAddr)\n\tfmt.Printf(\"  Proxy API URL: %s\\n\", *proxyAPIURL)\n\n\tserver := e2e.NewProxyFaultInjectorServerWithURL(*listenAddr, *proxyAPIURL)\n\tif err := server.Start(); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to start server: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\n\tfmt.Printf(\"Proxy Fault Injector Server started successfully\\n\")\n\tfmt.Printf(\"Fault Injector API available at http://%s\\n\", *listenAddr)\n\n\t// Wait for interrupt signal\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n\t<-sigChan\n\n\tfmt.Println(\"\\nShutting down...\")\n\tif err := server.Stop(); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error during shutdown: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\tfmt.Println(\"Server stopped\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/command_runner_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype CommandRunnerStats struct {\n\tOperations    int64\n\tErrors        int64\n\tTimeoutErrors int64\n\tErrorsList    []error\n}\n\n// CommandRunner provides utilities for running commands during tests\ntype CommandRunner struct {\n\texecuting      atomic.Bool\n\tclient         redis.UniversalClient\n\tstopCh         chan struct{}\n\toperationCount atomic.Int64\n\terrorCount     atomic.Int64\n\ttimeoutErrors  atomic.Int64\n\terrors         []error\n\terrorsMutex    sync.Mutex\n}\n\n// NewCommandRunner creates a new command runner\nfunc NewCommandRunner(client redis.UniversalClient) (*CommandRunner, func()) {\n\t// Use buffered channel so Stop() can always send without blocking\n\tstopCh := make(chan struct{}, 1)\n\treturn &CommandRunner{\n\t\t\tclient: client,\n\t\t\tstopCh: stopCh,\n\t\t\terrors: make([]error, 0),\n\t\t}, func() {\n\t\t\tselect {\n\t\t\tcase stopCh <- struct{}{}:\n\t\t\tdefault:\n\t\t\t\t// Already stopped\n\t\t\t}\n\t\t}\n}\n\nfunc (cr *CommandRunner) Stop() {\n\t// Non-blocking send to buffered channel\n\tselect {\n\tcase cr.stopCh <- struct{}{}:\n\tdefault:\n\t\t// Already has a stop signal pending\n\t}\n}\n\nfunc (cr *CommandRunner) Close() {\n\tclose(cr.stopCh)\n}\n\n// FireCommandsUntilStop runs commands continuously until stop signal\nfunc (cr *CommandRunner) FireCommandsUntilStop(ctx context.Context) {\n\tif !cr.executing.CompareAndSwap(false, true) {\n\t\treturn\n\t}\n\tdefer cr.executing.Store(false)\n\tfmt.Printf(\"[CR] Starting command runner...\\n\")\n\tdefer fmt.Printf(\"[CR] Command runner stopped\\n\")\n\t// High frequency for timeout testing\n\tticker := time.NewTicker(100 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tcounter := 0\n\tfor {\n\t\tselect {\n\t\tcase <-cr.stopCh:\n\t\t\treturn\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\tpoolSize := cr.client.PoolStats().IdleConns\n\t\t\tif poolSize == 0 {\n\t\t\t\tpoolSize = 1\n\t\t\t}\n\t\t\t// simulate at least 20 connections\n\t\t\tif poolSize < 20 {\n\t\t\t\tpoolSize = 20\n\t\t\t}\n\t\t\twg := sync.WaitGroup{}\n\t\t\t// run 2x pool size operations\n\t\t\tfor i := 0; i < int(poolSize)*2; i++ {\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func(i int) {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tkey := fmt.Sprintf(\"timeout-test-key-%d-%d\", counter, i)\n\t\t\t\t\tvalue := fmt.Sprintf(\"timeout-test-value-%d-%d\", counter, i)\n\n\t\t\t\t\t// Use the parent context directly - let the client's configured timeouts\n\t\t\t\t\t// (including relaxed timeouts during failover) handle timing\n\t\t\t\t\terr := cr.client.Set(ctx, key, value, time.Minute).Err()\n\n\t\t\t\t\tcr.operationCount.Add(1)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tif err == redis.ErrClosed || strings.Contains(err.Error(), \"client is closed\") {\n\t\t\t\t\t\t\tselect {\n\t\t\t\t\t\t\tcase <-cr.stopCh:\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcr.errorCount.Add(1)\n\n\t\t\t\t\t\t// Check if it's a timeout error\n\t\t\t\t\t\tif isTimeoutError(err) {\n\t\t\t\t\t\t\tcr.timeoutErrors.Add(1)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcr.errorsMutex.Lock()\n\t\t\t\t\t\tcr.errors = append(cr.errors, err)\n\t\t\t\t\t\tcr.errorsMutex.Unlock()\n\t\t\t\t\t}\n\t\t\t\t}(i)\n\t\t\t}\n\t\t\twg.Wait()\n\t\t\tcounter++\n\t\t}\n\t}\n}\n\n// GetStats returns operation statistics\nfunc (cr *CommandRunner) GetStats() CommandRunnerStats {\n\tcr.errorsMutex.Lock()\n\tdefer cr.errorsMutex.Unlock()\n\n\terrorList := make([]error, len(cr.errors))\n\tcopy(errorList, cr.errors)\n\n\tstats := CommandRunnerStats{\n\t\tOperations:    cr.operationCount.Load(),\n\t\tErrors:        cr.errorCount.Load(),\n\t\tTimeoutErrors: cr.timeoutErrors.Load(),\n\t\tErrorsList:    errorList,\n\t}\n\n\treturn stats\n}\n"
  },
  {
    "path": "maintnotifications/e2e/config_autostart_logic_test.go",
    "content": "package e2e\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\n// TestCreateTestFaultInjectorLogic_WithoutEnv verifies the auto-start logic\n// when REDIS_ENDPOINTS_CONFIG_PATH is not set\nfunc TestCreateTestFaultInjectorLogic_WithoutEnv(t *testing.T) {\n\t// Save original environment\n\torigConfigPath := os.Getenv(\"REDIS_ENDPOINTS_CONFIG_PATH\")\n\torigFIURL := os.Getenv(\"FAULT_INJECTION_API_URL\")\n\n\t// Clear environment to simulate no setup\n\tos.Unsetenv(\"REDIS_ENDPOINTS_CONFIG_PATH\")\n\tos.Unsetenv(\"FAULT_INJECTION_API_URL\")\n\n\t// Restore environment after test\n\tdefer func() {\n\t\tif origConfigPath != \"\" {\n\t\t\tos.Setenv(\"REDIS_ENDPOINTS_CONFIG_PATH\", origConfigPath)\n\t\t}\n\t\tif origFIURL != \"\" {\n\t\t\tos.Setenv(\"FAULT_INJECTION_API_URL\", origFIURL)\n\t\t}\n\t}()\n\n\t// Test GetEnvConfig - should fail when REDIS_ENDPOINTS_CONFIG_PATH is not set\n\tenvConfig, err := GetEnvConfig()\n\tif err == nil {\n\t\tt.Fatal(\"Expected GetEnvConfig() to fail when REDIS_ENDPOINTS_CONFIG_PATH is not set\")\n\t}\n\tif envConfig != nil {\n\t\tt.Fatal(\"Expected envConfig to be nil when GetEnvConfig() fails\")\n\t}\n\n\tt.Log(\"✅ GetEnvConfig() correctly fails when REDIS_ENDPOINTS_CONFIG_PATH is not set\")\n\tt.Log(\"✅ This means CreateTestFaultInjectorWithCleanup() will auto-start the proxy\")\n}\n\n// TestCreateTestFaultInjectorLogic_WithEnv verifies the logic\n// when REDIS_ENDPOINTS_CONFIG_PATH is set\nfunc TestCreateTestFaultInjectorLogic_WithEnv(t *testing.T) {\n\t// Create a temporary config file\n\ttmpFile := \"/tmp/test_endpoints.json\"\n\tcontent := `{\n\t\t\"standalone\": {\n\t\t\t\"endpoints\": [\"redis://localhost:6379\"]\n\t\t}\n\t}`\n\n\tif err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {\n\t\tt.Fatalf(\"Failed to create temp file: %v\", err)\n\t}\n\tdefer os.Remove(tmpFile)\n\n\t// Save original environment\n\torigConfigPath := os.Getenv(\"REDIS_ENDPOINTS_CONFIG_PATH\")\n\torigFIURL := os.Getenv(\"FAULT_INJECTION_API_URL\")\n\n\t// Set environment\n\tos.Setenv(\"REDIS_ENDPOINTS_CONFIG_PATH\", tmpFile)\n\tos.Setenv(\"FAULT_INJECTION_API_URL\", \"http://test-fi:9999\")\n\n\t// Restore environment after test\n\tdefer func() {\n\t\tif origConfigPath != \"\" {\n\t\t\tos.Setenv(\"REDIS_ENDPOINTS_CONFIG_PATH\", origConfigPath)\n\t\t} else {\n\t\t\tos.Unsetenv(\"REDIS_ENDPOINTS_CONFIG_PATH\")\n\t\t}\n\t\tif origFIURL != \"\" {\n\t\t\tos.Setenv(\"FAULT_INJECTION_API_URL\", origFIURL)\n\t\t} else {\n\t\t\tos.Unsetenv(\"FAULT_INJECTION_API_URL\")\n\t\t}\n\t}()\n\n\t// Test GetEnvConfig - should succeed when REDIS_ENDPOINTS_CONFIG_PATH is set\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\tt.Fatalf(\"Expected GetEnvConfig() to succeed when REDIS_ENDPOINTS_CONFIG_PATH is set, got error: %v\", err)\n\t}\n\tif envConfig == nil {\n\t\tt.Fatal(\"Expected envConfig to be non-nil when GetEnvConfig() succeeds\")\n\t}\n\n\t// Verify the fault injector URL is correct\n\tif envConfig.FaultInjectorURL != \"http://test-fi:9999\" {\n\t\tt.Errorf(\"Expected FaultInjectorURL to be 'http://test-fi:9999', got '%s'\", envConfig.FaultInjectorURL)\n\t}\n\n\tt.Log(\"✅ GetEnvConfig() correctly succeeds when REDIS_ENDPOINTS_CONFIG_PATH is set\")\n\tt.Log(\"✅ This means CreateTestFaultInjectorWithCleanup() will use the real fault injector\")\n\tt.Logf(\"✅ Fault injector URL: %s\", envConfig.FaultInjectorURL)\n}\n\n// TestCreateTestFaultInjectorLogic_DefaultFIURL verifies the default fault injector URL\nfunc TestCreateTestFaultInjectorLogic_DefaultFIURL(t *testing.T) {\n\t// Create a temporary config file\n\ttmpFile := \"/tmp/test_endpoints2.json\"\n\tcontent := `{\n\t\t\"standalone\": {\n\t\t\t\"endpoints\": [\"redis://localhost:6379\"]\n\t\t}\n\t}`\n\n\tif err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {\n\t\tt.Fatalf(\"Failed to create temp file: %v\", err)\n\t}\n\tdefer os.Remove(tmpFile)\n\n\t// Save original environment\n\torigConfigPath := os.Getenv(\"REDIS_ENDPOINTS_CONFIG_PATH\")\n\torigFIURL := os.Getenv(\"FAULT_INJECTION_API_URL\")\n\n\t// Set only config path, not fault injector URL\n\tos.Setenv(\"REDIS_ENDPOINTS_CONFIG_PATH\", tmpFile)\n\tos.Unsetenv(\"FAULT_INJECTION_API_URL\")\n\n\t// Restore environment after test\n\tdefer func() {\n\t\tif origConfigPath != \"\" {\n\t\t\tos.Setenv(\"REDIS_ENDPOINTS_CONFIG_PATH\", origConfigPath)\n\t\t} else {\n\t\t\tos.Unsetenv(\"REDIS_ENDPOINTS_CONFIG_PATH\")\n\t\t}\n\t\tif origFIURL != \"\" {\n\t\t\tos.Setenv(\"FAULT_INJECTION_API_URL\", origFIURL)\n\t\t}\n\t}()\n\n\t// Test GetEnvConfig - should succeed and use default FI URL\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\tt.Fatalf(\"Expected GetEnvConfig() to succeed, got error: %v\", err)\n\t}\n\n\t// Verify the default fault injector URL\n\tif envConfig.FaultInjectorURL != \"http://localhost:8080\" {\n\t\tt.Errorf(\"Expected default FaultInjectorURL to be 'http://localhost:8080', got '%s'\", envConfig.FaultInjectorURL)\n\t}\n\n\tt.Log(\"✅ GetEnvConfig() uses default fault injector URL when FAULT_INJECTION_API_URL is not set\")\n\tt.Logf(\"✅ Default fault injector URL: %s\", envConfig.FaultInjectorURL)\n}\n\n// TestFaultInjectorClientCreation verifies that FaultInjectorClient can be created\nfunc TestFaultInjectorClientCreation(t *testing.T) {\n\t// Test creating client with different URLs\n\ttestCases := []struct {\n\t\tname string\n\t\turl  string\n\t}{\n\t\t{\"localhost\", \"http://localhost:15000\"}, // Updated to avoid macOS Control Center conflict\n\t\t{\"with port\", \"http://test:9999\"},\n\t\t{\"with trailing slash\", \"http://test:9999/\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tclient := NewFaultInjectorClient(tc.url)\n\t\t\tif client == nil {\n\t\t\t\tt.Fatal(\"Expected non-nil client\")\n\t\t\t}\n\n\t\t\t// Verify the base URL (should have trailing slash removed)\n\t\t\texpectedURL := tc.url\n\t\t\tif expectedURL[len(expectedURL)-1] == '/' {\n\t\t\t\texpectedURL = expectedURL[:len(expectedURL)-1]\n\t\t\t}\n\n\t\t\tif client.GetBaseURL() != expectedURL {\n\t\t\t\tt.Errorf(\"Expected base URL '%s', got '%s'\", expectedURL, client.GetBaseURL())\n\t\t\t}\n\n\t\t\tt.Logf(\"✅ Client created successfully with URL: %s\", client.GetBaseURL())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/e2e/config_parser_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// debugE2E returns true if E2E_DEBUG environment variable is set to \"true\"\n// Use this to control verbose debug logging in e2e tests\nfunc debugE2E() bool {\n\treturn os.Getenv(\"E2E_DEBUG\") == \"true\"\n}\n\n// isPortUnavailableErrorString checks if the error string indicates a port unavailable error\nfunc isPortUnavailableErrorString(errStr string) bool {\n\treturn strings.Contains(errStr, \"port_unavailable\") || strings.Contains(errStr, \"Unavailable or invalid port\")\n}\n\n// DatabaseEndpoint represents a single database endpoint configuration\ntype DatabaseEndpoint struct {\n\tAddr                               []string `json:\"addr\"`\n\tAddrType                           string   `json:\"addr_type\"`\n\tDNSName                            string   `json:\"dns_name\"`\n\tOSSClusterAPIPreferredEndpointType string   `json:\"oss_cluster_api_preferred_endpoint_type\"`\n\tOSSClusterAPIPreferredIPType       string   `json:\"oss_cluster_api_preferred_ip_type\"`\n\tPort                               int      `json:\"port\"`\n\tProxyPolicy                        string   `json:\"proxy_policy\"`\n\tUID                                string   `json:\"uid\"`\n}\n\n// EnvDatabaseConfig represents the configuration for a single database\ntype EnvDatabaseConfig struct {\n\tBdbID                interface{}        `json:\"bdb_id,omitempty\"`\n\tUsername             string             `json:\"username,omitempty\"`\n\tPassword             string             `json:\"password,omitempty\"`\n\tTLS                  bool               `json:\"tls\"`\n\tCertificatesLocation string             `json:\"certificatesLocation,omitempty\"`\n\tRawEndpoints         []DatabaseEndpoint `json:\"raw_endpoints,omitempty\"`\n\tEndpoints            []string           `json:\"endpoints\"`\n\tOSSCluster           bool               `json:\"oss_cluster,omitempty\"`\n}\n\n// EnvDatabasesConfig represents the complete configuration file structure\ntype EnvDatabasesConfig map[string]EnvDatabaseConfig\n\n// EnvConfig represents environment configuration for test scenarios\ntype EnvConfig struct {\n\tRedisEndpointsConfigPath string\n\tFaultInjectorURL         string\n}\n\n// RedisConnectionConfig represents Redis connection parameters\ntype RedisConnectionConfig struct {\n\tHost                 string\n\tPort                 int\n\tUsername             string\n\tPassword             string\n\tTLS                  bool\n\tBdbID                int\n\tCertificatesLocation string\n\tEndpoints            []string\n\tIsClusterMode        bool // Force cluster mode even with single endpoint\n}\n\n// GetEnvConfig reads environment variables required for the test scenario\nfunc GetEnvConfig() (*EnvConfig, error) {\n\tredisConfigPath := os.Getenv(\"REDIS_ENDPOINTS_CONFIG_PATH\")\n\tif redisConfigPath == \"\" {\n\t\treturn nil, fmt.Errorf(\"REDIS_ENDPOINTS_CONFIG_PATH environment variable must be set\")\n\t}\n\n\tfaultInjectorURL := os.Getenv(\"FAULT_INJECTION_API_URL\")\n\tif faultInjectorURL == \"\" {\n\t\t// Default to localhost if not set\n\t\tfaultInjectorURL = \"http://localhost:8080\"\n\t}\n\n\treturn &EnvConfig{\n\t\tRedisEndpointsConfigPath: redisConfigPath,\n\t\tFaultInjectorURL:         faultInjectorURL,\n\t}, nil\n}\n\n// GetDatabaseConfigFromEnv reads database configuration from a file\nfunc GetDatabaseConfigFromEnv(filePath string) (EnvDatabasesConfig, error) {\n\tfileContent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read database config from %s: %w\", filePath, err)\n\t}\n\n\tvar config EnvDatabasesConfig\n\tif err := json.Unmarshal(fileContent, &config); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse database config from %s: %w\", filePath, err)\n\t}\n\n\treturn config, nil\n}\n\n// GetDatabaseConfig gets Redis connection parameters for a specific database\nfunc GetDatabaseConfig(databasesConfig EnvDatabasesConfig, databaseName string) (*RedisConnectionConfig, error) {\n\tvar dbConfig EnvDatabaseConfig\n\tvar exists bool\n\n\tif databaseName == \"\" {\n\t\t// Get the first database if no name is provided\n\t\tfor _, config := range databasesConfig {\n\t\t\tdbConfig = config\n\t\t\texists = true\n\t\t\tbreak\n\t\t}\n\t} else {\n\t\tdbConfig, exists = databasesConfig[databaseName]\n\t}\n\n\tif !exists {\n\t\treturn nil, fmt.Errorf(\"database %s not found in configuration\", databaseName)\n\t}\n\n\t// Parse connection details from endpoints or raw_endpoints\n\tvar host string\n\tvar port int\n\n\tif len(dbConfig.RawEndpoints) > 0 {\n\t\t// Use raw_endpoints if available (for more complex configurations)\n\t\tendpoint := dbConfig.RawEndpoints[0] // Use the first endpoint\n\t\thost = endpoint.DNSName\n\t\tport = endpoint.Port\n\t} else if len(dbConfig.Endpoints) > 0 {\n\t\t// Parse from endpoints URLs\n\t\tendpointURL, err := url.Parse(dbConfig.Endpoints[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse endpoint URL %s: %w\", dbConfig.Endpoints[0], err)\n\t\t}\n\n\t\thost = endpointURL.Hostname()\n\t\tportStr := endpointURL.Port()\n\t\tif portStr == \"\" {\n\t\t\t// Default ports based on scheme\n\t\t\tswitch endpointURL.Scheme {\n\t\t\tcase \"redis\":\n\t\t\t\tport = 6379\n\t\t\tcase \"rediss\":\n\t\t\t\tport = 6380\n\t\t\tdefault:\n\t\t\t\tport = 6379\n\t\t\t}\n\t\t} else {\n\t\t\tport, err = strconv.Atoi(portStr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid port in endpoint URL %s: %w\", dbConfig.Endpoints[0], err)\n\t\t\t}\n\t\t}\n\n\t\t// Override TLS setting based on scheme if not explicitly set\n\t\tif endpointURL.Scheme == \"rediss\" {\n\t\t\tdbConfig.TLS = true\n\t\t}\n\t} else {\n\t\treturn nil, fmt.Errorf(\"no endpoints found in database configuration\")\n\t}\n\n\tvar bdbId int\n\tswitch (dbConfig.BdbID).(type) {\n\tcase int:\n\t\tbdbId = dbConfig.BdbID.(int)\n\tcase float64:\n\t\tbdbId = int(dbConfig.BdbID.(float64))\n\tcase string:\n\t\tbdbId, _ = strconv.Atoi(dbConfig.BdbID.(string))\n\t}\n\n\treturn &RedisConnectionConfig{\n\t\tHost:                 host,\n\t\tPort:                 port,\n\t\tUsername:             dbConfig.Username,\n\t\tPassword:             dbConfig.Password,\n\t\tTLS:                  dbConfig.TLS,\n\t\tBdbID:                bdbId,\n\t\tCertificatesLocation: dbConfig.CertificatesLocation,\n\t\tEndpoints:            dbConfig.Endpoints,\n\t}, nil\n}\n\n// ConvertEnvDatabaseConfigToRedisConnectionConfig converts EnvDatabaseConfig to RedisConnectionConfig\nfunc ConvertEnvDatabaseConfigToRedisConnectionConfig(dbConfig EnvDatabaseConfig) (*RedisConnectionConfig, error) {\n\t// Parse connection details from endpoints or raw_endpoints\n\tvar host string\n\tvar port int\n\n\tif len(dbConfig.RawEndpoints) > 0 {\n\t\t// Use raw_endpoints if available (for more complex configurations)\n\t\tendpoint := dbConfig.RawEndpoints[0] // Use the first endpoint\n\t\thost = endpoint.DNSName\n\t\tport = endpoint.Port\n\t} else if len(dbConfig.Endpoints) > 0 {\n\t\t// Parse from endpoints URLs\n\t\tendpointURL, err := url.Parse(dbConfig.Endpoints[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse endpoint URL %s: %w\", dbConfig.Endpoints[0], err)\n\t\t}\n\n\t\thost = endpointURL.Hostname()\n\t\tportStr := endpointURL.Port()\n\t\tif portStr == \"\" {\n\t\t\t// Default ports based on scheme\n\t\t\tswitch endpointURL.Scheme {\n\t\t\tcase \"redis\":\n\t\t\t\tport = 6379\n\t\t\tcase \"rediss\":\n\t\t\t\tport = 6380\n\t\t\tdefault:\n\t\t\t\tport = 6379\n\t\t\t}\n\t\t} else {\n\t\t\tport, err = strconv.Atoi(portStr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid port in endpoint URL %s: %w\", dbConfig.Endpoints[0], err)\n\t\t\t}\n\t\t}\n\n\t\t// Override TLS setting based on scheme if not explicitly set\n\t\tif endpointURL.Scheme == \"rediss\" {\n\t\t\tdbConfig.TLS = true\n\t\t}\n\t} else {\n\t\treturn nil, fmt.Errorf(\"no endpoints found in database configuration\")\n\t}\n\n\tvar bdbId int\n\tswitch dbConfig.BdbID.(type) {\n\tcase int:\n\t\tbdbId = dbConfig.BdbID.(int)\n\tcase float64:\n\t\tbdbId = int(dbConfig.BdbID.(float64))\n\tcase string:\n\t\tbdbId, _ = strconv.Atoi(dbConfig.BdbID.(string))\n\t}\n\n\treturn &RedisConnectionConfig{\n\t\tHost:                 host,\n\t\tPort:                 port,\n\t\tUsername:             dbConfig.Username,\n\t\tPassword:             dbConfig.Password,\n\t\tTLS:                  dbConfig.TLS,\n\t\tBdbID:                bdbId,\n\t\tCertificatesLocation: dbConfig.CertificatesLocation,\n\t\tEndpoints:            dbConfig.Endpoints,\n\t\tIsClusterMode:        dbConfig.OSSCluster,\n\t}, nil\n}\n\n// ClientFactory manages Redis client creation and lifecycle\ntype ClientFactory struct {\n\tconfig  *RedisConnectionConfig\n\tclients map[string]redis.UniversalClient\n\tmutex   sync.RWMutex\n}\n\n// NewClientFactory creates a new client factory with the specified configuration\nfunc NewClientFactory(config *RedisConnectionConfig) *ClientFactory {\n\treturn &ClientFactory{\n\t\tconfig:  config,\n\t\tclients: make(map[string]redis.UniversalClient),\n\t}\n}\n\n// CreateClientOptions represents options for creating Redis clients\ntype CreateClientOptions struct {\n\tProtocol                   int\n\tMaintNotificationsConfig   *maintnotifications.Config\n\tMaxRetries                 int\n\tPoolSize                   int\n\tMinIdleConns               int\n\tMaxActiveConns             int\n\tClientName                 string\n\tDB                         int\n\tReadTimeout                time.Duration\n\tWriteTimeout               time.Duration\n\tClusterStateReloadInterval time.Duration // For cluster clients, interval for automatic state reloads\n}\n\n// DefaultCreateClientOptions returns default options for creating Redis clients\nfunc DefaultCreateClientOptions() *CreateClientOptions {\n\treturn &CreateClientOptions{\n\t\tProtocol: 3, // RESP3 by default for push notifications\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode:           maintnotifications.ModeEnabled,\n\t\t\tHandoffTimeout: 30 * time.Second,\n\t\t\tRelaxedTimeout: 10 * time.Second,\n\t\t\tMaxWorkers:     20,\n\t\t},\n\t\tMaxRetries:     3,\n\t\tPoolSize:       10,\n\t\tMinIdleConns:   10,\n\t\tMaxActiveConns: 10,\n\t}\n}\n\nfunc (cf *ClientFactory) PrintPoolStats(t *testing.T) {\n\tcf.mutex.RLock()\n\tdefer cf.mutex.RUnlock()\n\n\tfor key, client := range cf.clients {\n\t\tstats := client.PoolStats()\n\t\tt.Logf(\"Pool stats for client %s: %+v\", key, stats)\n\t}\n}\n\n// Create creates a new Redis client with the specified options and connects it\nfunc (cf *ClientFactory) Create(key string, options *CreateClientOptions) (redis.UniversalClient, error) {\n\tif options == nil {\n\t\toptions = DefaultCreateClientOptions()\n\t}\n\n\tcf.mutex.Lock()\n\tdefer cf.mutex.Unlock()\n\n\t// Check if client already exists\n\tif client, exists := cf.clients[key]; exists {\n\t\treturn client, nil\n\t}\n\n\tvar client redis.UniversalClient\n\tvar opts interface{}\n\n\t// Determine if this is a cluster configuration\n\tif len(cf.config.Endpoints) > 1 || cf.isClusterEndpoint() {\n\t\t// Create cluster client\n\t\tclusterOptions := &redis.ClusterOptions{\n\t\t\tAddrs:                      cf.getAddresses(),\n\t\t\tUsername:                   cf.config.Username,\n\t\t\tPassword:                   cf.config.Password,\n\t\t\tProtocol:                   options.Protocol,\n\t\t\tMaintNotificationsConfig:   options.MaintNotificationsConfig,\n\t\t\tMaxRetries:                 options.MaxRetries,\n\t\t\tPoolSize:                   options.PoolSize,\n\t\t\tMinIdleConns:               options.MinIdleConns,\n\t\t\tMaxActiveConns:             options.MaxActiveConns,\n\t\t\tClientName:                 options.ClientName,\n\t\t\tClusterStateReloadInterval: 10 * time.Minute,\n\t\t}\n\n\t\tif options.ReadTimeout > 0 {\n\t\t\tclusterOptions.ReadTimeout = options.ReadTimeout\n\t\t}\n\t\tif options.WriteTimeout > 0 {\n\t\t\tclusterOptions.WriteTimeout = options.WriteTimeout\n\t\t}\n\t\tif options.ClusterStateReloadInterval > 0 {\n\t\t\tclusterOptions.ClusterStateReloadInterval = options.ClusterStateReloadInterval\n\t\t}\n\n\t\tif cf.config.TLS {\n\t\t\tclusterOptions.TLSConfig = &tls.Config{\n\t\t\t\tInsecureSkipVerify: true, // For testing purposes\n\t\t\t}\n\t\t}\n\n\t\topts = clusterOptions\n\t\tclient = redis.NewClusterClient(clusterOptions)\n\t} else {\n\t\t// Create single client\n\t\tclientOptions := &redis.Options{\n\t\t\tAddr:                     fmt.Sprintf(\"%s:%d\", cf.config.Host, cf.config.Port),\n\t\t\tUsername:                 cf.config.Username,\n\t\t\tPassword:                 cf.config.Password,\n\t\t\tDB:                       options.DB,\n\t\t\tProtocol:                 options.Protocol,\n\t\t\tMaintNotificationsConfig: options.MaintNotificationsConfig,\n\t\t\tMaxRetries:               options.MaxRetries,\n\t\t\tPoolSize:                 options.PoolSize,\n\t\t\tMinIdleConns:             options.MinIdleConns,\n\t\t\tMaxActiveConns:           options.MaxActiveConns,\n\t\t\tClientName:               options.ClientName,\n\t\t}\n\n\t\tif options.ReadTimeout > 0 {\n\t\t\tclientOptions.ReadTimeout = options.ReadTimeout\n\t\t}\n\t\tif options.WriteTimeout > 0 {\n\t\t\tclientOptions.WriteTimeout = options.WriteTimeout\n\t\t}\n\n\t\tif cf.config.TLS {\n\t\t\tclientOptions.TLSConfig = &tls.Config{\n\t\t\t\tInsecureSkipVerify: true, // For testing purposes\n\t\t\t}\n\t\t}\n\n\t\topts = clientOptions\n\t\tclient = redis.NewClient(clientOptions)\n\t}\n\n\tif err := client.Ping(context.Background()).Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to connect to Redis: %w\\nOptions: %+v\", err, opts)\n\t}\n\n\t// Store the client\n\tcf.clients[key] = client\n\n\treturn client, nil\n}\n\n// Get retrieves an existing client by key or the first one if no key is provided\nfunc (cf *ClientFactory) Get(key string) redis.UniversalClient {\n\tcf.mutex.RLock()\n\tdefer cf.mutex.RUnlock()\n\n\tif key != \"\" {\n\t\treturn cf.clients[key]\n\t}\n\n\t// Return the first client if no key is provided\n\tfor _, client := range cf.clients {\n\t\treturn client\n\t}\n\n\treturn nil\n}\n\n// GetAll returns all created clients\nfunc (cf *ClientFactory) GetAll() map[string]redis.UniversalClient {\n\tcf.mutex.RLock()\n\tdefer cf.mutex.RUnlock()\n\n\tresult := make(map[string]redis.UniversalClient)\n\tfor key, client := range cf.clients {\n\t\tresult[key] = client\n\t}\n\n\treturn result\n}\n\n// DestroyAll closes and removes all created clients\nfunc (cf *ClientFactory) DestroyAll() error {\n\tcf.mutex.Lock()\n\tdefer cf.mutex.Unlock()\n\n\tvar lastErr error\n\tfor key, client := range cf.clients {\n\t\tif err := client.Close(); err != nil {\n\t\t\tlastErr = err\n\t\t}\n\t\tdelete(cf.clients, key)\n\t}\n\n\treturn lastErr\n}\n\n// Destroy closes and removes a specific client\nfunc (cf *ClientFactory) Destroy(key string) error {\n\tcf.mutex.Lock()\n\tdefer cf.mutex.Unlock()\n\n\tclient, exists := cf.clients[key]\n\tif !exists {\n\t\treturn fmt.Errorf(\"client %s not found\", key)\n\t}\n\n\terr := client.Close()\n\tdelete(cf.clients, key)\n\treturn err\n}\n\n// GetConfig returns the connection configuration\nfunc (cf *ClientFactory) GetConfig() *RedisConnectionConfig {\n\treturn cf.config\n}\n\n// Helper methods\n\n// isClusterEndpoint determines if the configuration represents a cluster\nfunc (cf *ClientFactory) isClusterEndpoint() bool {\n\t// Check if cluster mode is explicitly set\n\tif cf.config.IsClusterMode {\n\t\treturn true\n\t}\n\n\t// Check if any endpoint contains cluster-related keywords\n\tfor _, endpoint := range cf.config.Endpoints {\n\t\tif strings.Contains(strings.ToLower(endpoint), \"cluster\") {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Check if we have multiple raw endpoints\n\tif len(cf.config.Endpoints) > 1 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// getAddresses returns a list of addresses for cluster configuration\nfunc (cf *ClientFactory) getAddresses() []string {\n\tif len(cf.config.Endpoints) > 0 {\n\t\taddresses := make([]string, 0, len(cf.config.Endpoints))\n\t\tfor _, endpoint := range cf.config.Endpoints {\n\t\t\tif parsedURL, err := url.Parse(endpoint); err == nil {\n\t\t\t\taddr := parsedURL.Host\n\t\t\t\tif addr != \"\" {\n\t\t\t\t\taddresses = append(addresses, addr)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(addresses) > 0 {\n\t\t\treturn addresses\n\t\t}\n\t}\n\n\t// Fallback to single address\n\treturn []string{fmt.Sprintf(\"%s:%d\", cf.config.Host, cf.config.Port)}\n}\n\n// Utility functions for common test scenarios\n\n// CreateTestClientFactory creates a client factory from environment configuration\nfunc CreateTestClientFactory(databaseName string) (*ClientFactory, error) {\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get environment config: %w\", err)\n\t}\n\n\tdatabasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get database config: %w\", err)\n\t}\n\n\tdbConfig, err := GetDatabaseConfig(databasesConfig, databaseName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get database config for %s: %w\", databaseName, err)\n\t}\n\n\treturn NewClientFactory(dbConfig), nil\n}\n\n// CreateTestClientFactoryWithBdbID creates a client factory using a specific bdb_id\n// This is useful when you've created a fresh database and want to connect to it\nfunc CreateTestClientFactoryWithBdbID(databaseName string, bdbID int) (*ClientFactory, error) {\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get environment config: %w\", err)\n\t}\n\n\tdatabasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get database config: %w\", err)\n\t}\n\n\tdbConfig, err := GetDatabaseConfig(databasesConfig, databaseName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get database config for %s: %w\", databaseName, err)\n\t}\n\n\t// Override the bdb_id with the newly created database ID\n\tdbConfig.BdbID = bdbID\n\n\treturn NewClientFactory(dbConfig), nil\n}\n\n// CreateTestFaultInjector creates a fault injector client from environment configuration\n// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it automatically starts a proxy fault injector server\n//\n// Deprecated: Use CreateTestFaultInjectorWithCleanup instead for proper cleanup\nfunc CreateTestFaultInjector() (*FaultInjectorClient, error) {\n\tclient, _, err := CreateTestFaultInjectorWithCleanup()\n\treturn client, err\n}\n\n// CreateTestFaultInjectorWithCleanup creates a fault injector client and returns a cleanup function\n//\n// Decision logic based on environment:\n// 1. If REDIS_ENDPOINTS_CONFIG_PATH is set -> use real fault injector from FAULT_INJECTION_API_URL\n// 2. If REDIS_ENDPOINTS_CONFIG_PATH is NOT set -> use Docker fault injector at http://localhost:15000\n//\n// Both the Docker proxy and fault injector should already be running (started via Docker Compose)\n// This function does NOT start any services - it only connects to existing ones\n//\n// Usage:\n//\n//\tclient, cleanup, err := CreateTestFaultInjectorWithCleanup()\n//\tif err != nil { ... }\n//\tdefer cleanup()\nfunc CreateTestFaultInjectorWithCleanup() (*FaultInjectorClient, func(), error) {\n\t// Try to get environment config\n\tenvConfig, err := GetEnvConfig()\n\n\t// If environment config fails, use Docker fault injector\n\t// Note: GetEnvConfig() only fails if REDIS_ENDPOINTS_CONFIG_PATH is not set\n\tif err != nil {\n\t\t// Use Docker fault injector at http://localhost:15000 (updated to avoid macOS Control Center conflict)\n\t\t// The fault injector should already be running via docker-compose\n\t\tfaultInjectorURL := \"http://localhost:15000\"\n\n\t\t// Check if fault injector is accessible\n\t\tclient := &http.Client{Timeout: 2 * time.Second}\n\t\tresp, err := client.Get(faultInjectorURL + \"/actions\")\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"Docker fault injector not accessible at %s (did you run 'make docker.e2e.start'?): %w\", faultInjectorURL, err)\n\t\t}\n\t\tresp.Body.Close()\n\n\t\tfmt.Printf(\"✓ Using Docker fault injector at %s\\n\", faultInjectorURL)\n\n\t\t// Return client with no-op cleanup (Docker manages the fault injector lifecycle)\n\t\tnoopCleanup := func() {}\n\t\treturn NewFaultInjectorClient(faultInjectorURL), noopCleanup, nil\n\t}\n\n\t// Using real fault injector - no cleanup needed\n\tfmt.Printf(\"✓ Using real fault injector at %s\\n\", envConfig.FaultInjectorURL)\n\tnoopCleanup := func() {}\n\treturn NewFaultInjectorClient(envConfig.FaultInjectorURL), noopCleanup, nil\n}\n\n// GetAvailableDatabases returns a list of available database names from the configuration\nfunc GetAvailableDatabases(configPath string) ([]string, error) {\n\tdatabasesConfig, err := GetDatabaseConfigFromEnv(configPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdatabases := make([]string, 0, len(databasesConfig))\n\tfor name := range databasesConfig {\n\t\tdatabases = append(databases, name)\n\t}\n\n\treturn databases, nil\n}\n\n// ConvertEnvDatabaseConfigToFaultInjectorConfig converts EnvDatabaseConfig to fault injector DatabaseConfig\n// This creates an OSS Cluster database by default for backward compatibility.\n// Use ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode for explicit control over cluster mode.\nfunc ConvertEnvDatabaseConfigToFaultInjectorConfig(envConfig EnvDatabaseConfig, name string) (DatabaseConfig, error) {\n\treturn ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envConfig, name, true)\n}\n\n// ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode converts EnvDatabaseConfig to fault injector DatabaseConfig\n// with explicit control over whether to create a cluster or standalone database.\n// Parameters:\n//   - envConfig: The environment database configuration\n//   - name: The name for the new database\n//   - clusterMode: If true, creates an OSS Cluster database (ClusterClient). If false, creates a standalone database (Client).\nfunc ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envConfig EnvDatabaseConfig, name string, clusterMode bool) (DatabaseConfig, error) {\n\tvar port int\n\n\t// Extract port and DNS name from raw_endpoints or endpoints\n\tif len(envConfig.RawEndpoints) > 0 {\n\t\tendpoint := envConfig.RawEndpoints[0]\n\t\tport = endpoint.Port\n\t} else if len(envConfig.Endpoints) > 0 {\n\t\tendpointURL, err := url.Parse(envConfig.Endpoints[0])\n\t\tif err != nil {\n\t\t\treturn DatabaseConfig{}, fmt.Errorf(\"failed to parse endpoint URL: %w\", err)\n\t\t}\n\t\tportStr := endpointURL.Port()\n\t\tif portStr != \"\" {\n\t\t\tport, err = strconv.Atoi(portStr)\n\t\t\tif err != nil {\n\t\t\t\treturn DatabaseConfig{}, fmt.Errorf(\"invalid port: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tport = 6379 * 2 // default*2\n\t\t}\n\t} else {\n\t\treturn DatabaseConfig{}, fmt.Errorf(\"no endpoints found in configuration\")\n\t}\n\n\trandomPortOffset := 1 + rand.Intn(5) + rand.Intn(10) + rand.Intn(10) + rand.Intn(10) // Random port offset to avoid conflicts\n\n\tvar dbConfig DatabaseConfig\n\n\tif clusterMode {\n\t\t// Build OSS Cluster database config for ClusterClient support\n\t\t// Supports cluster-specific features like SMIGRATING notifications\n\t\tdbConfig = DatabaseConfig{\n\t\t\tName:           name,\n\t\t\tPort:           port + randomPortOffset,\n\t\t\tMemorySize:     268435456, // 256MB default\n\t\t\tReplication:    true,\n\t\t\tEvictionPolicy: \"noeviction\",\n\t\t\tProxyPolicy:    \"all-master-shards\", // OSS Cluster API requires all-master-shards\n\t\t\tAutoUpgrade:    true,\n\t\t\tSharding:       true,\n\t\t\tShardsCount:    2,\n\t\t\tShardKeyRegex: []ShardKeyRegexPattern{\n\t\t\t\t{Regex: \".*\\\\{(?<tag>.*)\\\\}.*\"},\n\t\t\t\t{Regex: \"(?<tag>.*)\"},\n\t\t\t},\n\t\t\tShardsPlacement: \"sparse\", // Use sparse placement to distribute shards across nodes (required for slot-shuffle)\n\t\t\tModuleList: []DatabaseModule{\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"ReJSON\"},\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"search\"},\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"timeseries\"},\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"bf\"},\n\t\t\t},\n\t\t\tOSSCluster: true, // Enable OSS Cluster API for ClusterClient support\n\t\t}\n\n\t\t// If we have raw_endpoints with cluster info, configure for cluster\n\t\tif len(envConfig.RawEndpoints) > 0 {\n\t\t\tendpoint := envConfig.RawEndpoints[0]\n\n\t\t\t// Check if this is a cluster configuration\n\t\t\tif endpoint.ProxyPolicy != \"\" && endpoint.ProxyPolicy != \"single\" {\n\t\t\t\tdbConfig.OSSCluster = true\n\t\t\t\tdbConfig.Sharding = true\n\t\t\t\tdbConfig.ShardsCount = 3 // default for cluster\n\t\t\t\tdbConfig.ProxyPolicy = endpoint.ProxyPolicy\n\t\t\t\tdbConfig.Replication = true\n\t\t\t}\n\n\t\t\t// Note: We ignore endpoint.OSSClusterAPIPreferredIPType here because\n\t\t\t// e2e tests run from outside the cluster network and need external IPs\n\t\t}\n\n\t\t// For OSS Cluster databases, always use external IPs since e2e tests\n\t\t// run from outside the cluster network and cannot reach internal IPs\n\t\tdbConfig.OSSClusterAPIPreferredIPType = \"external\"\n\t} else {\n\t\t// Build standalone database config for regular Client support\n\t\t// Supports MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER notifications\n\t\tdbConfig = DatabaseConfig{\n\t\t\tName:           name,\n\t\t\tPort:           port + randomPortOffset,\n\t\t\tMemorySize:     268435456, // 256MB default\n\t\t\tReplication:    true,      // Enable replication for failover support\n\t\t\tEvictionPolicy: \"noeviction\",\n\t\t\tProxyPolicy:    \"single\", // Single proxy for standalone database\n\t\t\tAutoUpgrade:    true,\n\t\t\tSharding:       false, // No sharding for standalone\n\t\t\tShardsCount:    1,     // Single shard\n\t\t\tModuleList: []DatabaseModule{\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"ReJSON\"},\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"search\"},\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"timeseries\"},\n\t\t\t\t{ModuleArgs: \"\", ModuleName: \"bf\"},\n\t\t\t},\n\t\t\tOSSCluster: false, // Disable OSS Cluster API for standalone Client support\n\t\t}\n\t}\n\n\treturn dbConfig, nil\n}\n\n// TestDatabaseManager manages database lifecycle for tests\ntype TestDatabaseManager struct {\n\tfaultInjector *FaultInjectorClient\n\tclusterIndex  int\n\tcreatedBdbID  int\n\tdbConfig      DatabaseConfig\n\tt             *testing.T\n}\n\n// NewTestDatabaseManager creates a new test database manager\nfunc NewTestDatabaseManager(t *testing.T, faultInjector *FaultInjectorClient, clusterIndex int) *TestDatabaseManager {\n\treturn &TestDatabaseManager{\n\t\tfaultInjector: faultInjector,\n\t\tclusterIndex:  clusterIndex,\n\t\tt:             t,\n\t}\n}\n\n// CreateDatabaseFromEnvConfig creates a database using EnvDatabaseConfig\nfunc (m *TestDatabaseManager) CreateDatabaseFromEnvConfig(ctx context.Context, envConfig EnvDatabaseConfig, name string) (int, error) {\n\t// Convert EnvDatabaseConfig to DatabaseConfig\n\tdbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfig(envConfig, name)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to convert config: %w\", err)\n\t}\n\n\tm.dbConfig = dbConfig\n\treturn m.CreateDatabase(ctx, dbConfig)\n}\n\n// CreateDatabase creates a database and waits for it to be ready\n// Returns the bdb_id of the created database\n// If the port is unavailable, it will retry with incremented port numbers\nfunc (m *TestDatabaseManager) CreateDatabase(ctx context.Context, dbConfig DatabaseConfig) (int, error) {\n\tresp, finalPort, err := m.faultInjector.CreateDatabaseConfigWithPortRetry(ctx, m.clusterIndex, dbConfig, 100)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to trigger database creation: %w\", err)\n\t}\n\n\tif finalPort != dbConfig.Port && debugE2E() {\n\t\tfmt.Printf(\"[TestDatabaseManager] Database created on port %d (originally requested %d)\\n\", finalPort, dbConfig.Port)\n\t}\n\n\t// Wait for creation to complete\n\tstatus, err := m.faultInjector.WaitForAction(ctx, resp.ActionID,\n\t\tWithMaxWaitTime(5*time.Minute),\n\t\tWithPollInterval(5*time.Second))\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to wait for database creation: %w\", err)\n\t}\n\n\tif status.Status != StatusSuccess {\n\t\treturn 0, fmt.Errorf(\"database creation failed: %v\", status.Error)\n\t}\n\n\t// Extract bdb_id from output\n\tvar bdbID int\n\tif status.Output != nil {\n\t\tif id, ok := status.Output[\"bdb_id\"].(float64); ok {\n\t\t\tbdbID = int(id)\n\t\t} else if resultMap, ok := status.Output[\"result\"].(map[string]interface{}); ok {\n\t\t\tif id, ok := resultMap[\"bdb_id\"].(float64); ok {\n\t\t\t\tbdbID = int(id)\n\t\t\t}\n\t\t}\n\t}\n\n\tif bdbID == 0 {\n\t\treturn 0, fmt.Errorf(\"failed to extract bdb_id from creation output\")\n\t}\n\n\tm.createdBdbID = bdbID\n\n\treturn bdbID, nil\n}\n\n// CreateDatabaseAndGetConfig creates a database and returns both the bdb_id and the full connection config from the fault injector response\n// This includes endpoints, username, password, TLS settings, and raw_endpoints\n// If the port is unavailable, it will retry with incremented port numbers\nfunc (m *TestDatabaseManager) CreateDatabaseAndGetConfig(ctx context.Context, dbConfig DatabaseConfig) (int, EnvDatabaseConfig, error) {\n\tconst maxPortRetries = 100\n\tcurrentPort := dbConfig.Port\n\n\tvar status *ActionStatusResponse\n\n\t// Retry loop for port unavailable errors\n\tfor attempt := 0; attempt < maxPortRetries; attempt++ {\n\t\t// Update the port in the config\n\t\tdbConfig.Port = currentPort\n\n\t\tresp, err := m.faultInjector.CreateDatabase(ctx, m.clusterIndex, dbConfig)\n\t\tif err != nil {\n\t\t\t// Check if it's a port unavailable error at trigger time\n\t\t\tif isPortUnavailableErrorString(err.Error()) {\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tfmt.Printf(\"[TestDatabaseManager] Port %d unavailable at trigger, trying port %d\\n\", currentPort, currentPort+1)\n\t\t\t\t}\n\t\t\t\tcurrentPort++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn 0, EnvDatabaseConfig{}, fmt.Errorf(\"failed to trigger database creation: %w\", err)\n\t\t}\n\n\t\t// Wait for creation to complete\n\t\tstatus, err = m.faultInjector.WaitForAction(ctx, resp.ActionID,\n\t\t\tWithMaxWaitTime(5*time.Minute),\n\t\t\tWithPollInterval(5*time.Second))\n\t\tif err != nil {\n\t\t\treturn 0, EnvDatabaseConfig{}, fmt.Errorf(\"failed to wait for database creation: %w\", err)\n\t\t}\n\n\t\tif status.Status == StatusSuccess {\n\t\t\t// Success! Break out of retry loop\n\t\t\tif currentPort != dbConfig.Port && debugE2E() {\n\t\t\t\tfmt.Printf(\"[TestDatabaseManager] Database created on port %d (originally requested %d)\\n\", currentPort, dbConfig.Port)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\t// Check if the error is port unavailable\n\t\terrorStr := \"\"\n\t\tif status.Error != nil {\n\t\t\terrorStr = fmt.Sprintf(\"%v\", status.Error)\n\t\t}\n\t\tif isPortUnavailableErrorString(errorStr) {\n\t\t\tif debugE2E() {\n\t\t\t\tfmt.Printf(\"[TestDatabaseManager] Port %d unavailable after action, trying port %d\\n\", currentPort, currentPort+1)\n\t\t\t}\n\t\t\tcurrentPort++\n\t\t\tcontinue\n\t\t}\n\n\t\t// Different error, don't retry\n\t\treturn 0, EnvDatabaseConfig{}, fmt.Errorf(\"database creation failed: %v\", status.Error)\n\t}\n\n\t// Check if we exhausted retries\n\tif status == nil || status.Status != StatusSuccess {\n\t\treturn 0, EnvDatabaseConfig{}, fmt.Errorf(\"failed to create database after %d port retries (last port tried: %d)\", maxPortRetries, currentPort)\n\t}\n\n\t// Extract database configuration from output\n\tvar envConfig EnvDatabaseConfig\n\tif status.Output == nil {\n\t\treturn 0, EnvDatabaseConfig{}, fmt.Errorf(\"no output in creation response\")\n\t}\n\n\t// Extract bdb_id\n\tvar bdbID int\n\tif id, ok := status.Output[\"bdb_id\"].(float64); ok {\n\t\tbdbID = int(id)\n\t\tenvConfig.BdbID = bdbID\n\t} else {\n\t\treturn 0, EnvDatabaseConfig{}, fmt.Errorf(\"failed to extract bdb_id from creation output\")\n\t}\n\n\t// Extract username\n\tif username, ok := status.Output[\"username\"].(string); ok {\n\t\tenvConfig.Username = username\n\t}\n\n\t// Extract password\n\tif password, ok := status.Output[\"password\"].(string); ok {\n\t\tenvConfig.Password = password\n\t}\n\n\t// Extract TLS setting\n\tif tls, ok := status.Output[\"tls\"].(bool); ok {\n\t\tenvConfig.TLS = tls\n\t}\n\n\t// Extract endpoints\n\tif endpoints, ok := status.Output[\"endpoints\"].([]interface{}); ok {\n\t\tenvConfig.Endpoints = make([]string, 0, len(endpoints))\n\t\tfor _, ep := range endpoints {\n\t\t\tif epStr, ok := ep.(string); ok {\n\t\t\t\tenvConfig.Endpoints = append(envConfig.Endpoints, epStr)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Extract raw_endpoints\n\tif rawEndpoints, ok := status.Output[\"raw_endpoints\"].([]interface{}); ok {\n\t\tenvConfig.RawEndpoints = make([]DatabaseEndpoint, 0, len(rawEndpoints))\n\t\tfor _, rawEp := range rawEndpoints {\n\t\t\tif rawEpMap, ok := rawEp.(map[string]interface{}); ok {\n\t\t\t\tvar dbEndpoint DatabaseEndpoint\n\n\t\t\t\t// Extract addr\n\t\t\t\tif addr, ok := rawEpMap[\"addr\"].([]interface{}); ok {\n\t\t\t\t\tdbEndpoint.Addr = make([]string, 0, len(addr))\n\t\t\t\t\tfor _, a := range addr {\n\t\t\t\t\t\tif aStr, ok := a.(string); ok {\n\t\t\t\t\t\t\tdbEndpoint.Addr = append(dbEndpoint.Addr, aStr)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Extract other fields\n\t\t\t\tif addrType, ok := rawEpMap[\"addr_type\"].(string); ok {\n\t\t\t\t\tdbEndpoint.AddrType = addrType\n\t\t\t\t}\n\t\t\t\tif dnsName, ok := rawEpMap[\"dns_name\"].(string); ok {\n\t\t\t\t\tdbEndpoint.DNSName = dnsName\n\t\t\t\t}\n\t\t\t\tif preferredEndpointType, ok := rawEpMap[\"oss_cluster_api_preferred_endpoint_type\"].(string); ok {\n\t\t\t\t\tdbEndpoint.OSSClusterAPIPreferredEndpointType = preferredEndpointType\n\t\t\t\t}\n\t\t\t\tif preferredIPType, ok := rawEpMap[\"oss_cluster_api_preferred_ip_type\"].(string); ok {\n\t\t\t\t\tdbEndpoint.OSSClusterAPIPreferredIPType = preferredIPType\n\t\t\t\t}\n\t\t\t\tif port, ok := rawEpMap[\"port\"].(float64); ok {\n\t\t\t\t\tdbEndpoint.Port = int(port)\n\t\t\t\t}\n\t\t\t\tif proxyPolicy, ok := rawEpMap[\"proxy_policy\"].(string); ok {\n\t\t\t\t\tdbEndpoint.ProxyPolicy = proxyPolicy\n\t\t\t\t}\n\t\t\t\tif uid, ok := rawEpMap[\"uid\"].(string); ok {\n\t\t\t\t\tdbEndpoint.UID = uid\n\t\t\t\t}\n\n\t\t\t\tenvConfig.RawEndpoints = append(envConfig.RawEndpoints, dbEndpoint)\n\t\t\t}\n\t\t}\n\t}\n\n\tm.createdBdbID = bdbID\n\treturn bdbID, envConfig, nil\n}\n\n// DeleteDatabase deletes the created database\nfunc (m *TestDatabaseManager) DeleteDatabase(ctx context.Context) error {\n\tif m.createdBdbID == 0 {\n\t\treturn fmt.Errorf(\"no database to delete (bdb_id is 0)\")\n\t}\n\n\tresp, err := m.faultInjector.DeleteDatabase(ctx, m.clusterIndex, m.createdBdbID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to trigger database deletion: %w\", err)\n\t}\n\n\t// Wait for deletion to complete\n\tstatus, err := m.faultInjector.WaitForAction(ctx, resp.ActionID,\n\t\tWithMaxWaitTime(2*time.Minute),\n\t\tWithPollInterval(3*time.Second))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to wait for database deletion: %w\", err)\n\t}\n\n\tif status.Status != StatusSuccess {\n\t\treturn fmt.Errorf(\"database deletion failed: %v\", status.Error)\n\t}\n\n\tm.createdBdbID = 0\n\n\treturn nil\n}\n\n// GetBdbID returns the created database ID\nfunc (m *TestDatabaseManager) GetBdbID() int {\n\treturn m.createdBdbID\n}\n\n// Cleanup ensures the database is deleted (safe to call multiple times)\nfunc (m *TestDatabaseManager) Cleanup(ctx context.Context) {\n\tif m.createdBdbID != 0 {\n\t\tif err := m.DeleteDatabase(ctx); err != nil {\n\t\t\tm.t.Logf(\"Warning: Failed to cleanup database: %v\", err)\n\t\t}\n\t}\n}\n\n// SetupTestDatabaseFromEnv creates a database from environment config and returns a cleanup function\n// Usage:\n//\n//\tcleanup := SetupTestDatabaseFromEnv(t, ctx, \"my-test-db\")\n//\tdefer cleanup()\nfunc SetupTestDatabaseFromEnv(t *testing.T, ctx context.Context, databaseName string) (bdbID int, cleanup func()) {\n\t// Get environment config\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get environment config: %v\", err)\n\t}\n\n\t// Get database config from environment\n\tdatabasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get database config: %v\", err)\n\t}\n\n\t// Get the specific database config\n\tvar envDbConfig EnvDatabaseConfig\n\tvar exists bool\n\tif databaseName == \"\" {\n\t\t// Get first database if no name provided\n\t\tfor _, config := range databasesConfig {\n\t\t\tenvDbConfig = config\n\t\t\texists = true\n\t\t\tbreak\n\t\t}\n\t} else {\n\t\tenvDbConfig, exists = databasesConfig[databaseName]\n\t}\n\n\tif !exists {\n\t\tt.Fatalf(\"Database %s not found in configuration\", databaseName)\n\t}\n\n\t// Create fault injector with cleanup\n\tfaultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create fault injector: %v\", err)\n\t}\n\n\t// Create database manager\n\tdbManager := NewTestDatabaseManager(t, faultInjector, 0)\n\n\t// Create the database\n\ttestDBName := fmt.Sprintf(\"e2e-test-%s-%d\", databaseName, time.Now().Unix())\n\tbdbID, err = dbManager.CreateDatabaseFromEnvConfig(ctx, envDbConfig, testDBName)\n\tif err != nil {\n\t\tfiCleanup()\n\t\tt.Fatalf(\"[ERROR] Failed to create test database: %v\", err)\n\t}\n\n\t// Return combined cleanup function\n\tcleanup = func() {\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t}\n\n\treturn bdbID, cleanup\n}\n\n// SetupTestDatabaseWithConfig creates a database with custom config and returns a cleanup function\n// Usage:\n//\n//\tbdbID, cleanup := SetupTestDatabaseWithConfig(t, ctx, dbConfig)\n//\tdefer cleanup()\nfunc SetupTestDatabaseWithConfig(t *testing.T, ctx context.Context, dbConfig DatabaseConfig) (bdbID int, cleanup func()) {\n\t// Create fault injector with cleanup\n\tfaultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create fault injector: %v\", err)\n\t}\n\n\t// Create database manager\n\tdbManager := NewTestDatabaseManager(t, faultInjector, 0)\n\n\t// Create the database\n\tbdbID, err = dbManager.CreateDatabase(ctx, dbConfig)\n\tif err != nil {\n\t\tfiCleanup()\n\t\tt.Fatalf(\"[ERROR] Failed to create test database: %v\", err)\n\t}\n\n\t// Return combined cleanup function\n\tcleanup = func() {\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t}\n\n\treturn bdbID, cleanup\n}\n\n// SetupTestDatabaseAndFactory creates a database from environment config and returns both bdbID, factory, test mode config, and cleanup function\n// This is the recommended way to setup tests as it ensures the client factory connects to the newly created database\n//\n// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it will use the Docker proxy setup (127.0.0.1:17000) instead of creating a new database.\n// This allows tests to work with either the real fault injector OR the Docker proxy setup.\n//\n// Usage:\n//\n//\tbdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, \"standalone\")\n//\tdefer cleanup()\nfunc SetupTestDatabaseAndFactory(t *testing.T, ctx context.Context, databaseName string) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, cleanup func()) {\n\t// Get environment config\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\t// No environment config - use Docker proxy setup\n\t\tt.Logf(\"No environment config found, using Docker proxy setup at 127.0.0.1:17000\")\n\n\t\t// Determine cluster mode based on database name\n\t\t// \"standalone\" creates a non-cluster client for testing FAILING_OVER/FAILED_OVER notifications\n\t\tisClusterMode := databaseName != \"standalone\"\n\n\t\t// Create a simple Redis connection config for Docker proxy\n\t\tredisConfig := &RedisConnectionConfig{\n\t\t\tHost:          \"127.0.0.1\", // Use 127.0.0.1 to force IPv4\n\t\t\tPort:          17000,\n\t\t\tUsername:      \"\",\n\t\t\tPassword:      \"\",\n\t\t\tTLS:           false,\n\t\t\tBdbID:         0,\n\t\t\tIsClusterMode: isClusterMode,\n\t\t}\n\n\t\tfactory = NewClientFactory(redisConfig)\n\n\t\t// Get proxy mock test mode config\n\t\ttestMode = GetTestModeConfig()\n\n\t\t// No-op cleanup since we're not creating a database\n\t\tcleanup = func() {\n\t\t\tfactory.DestroyAll()\n\t\t}\n\n\t\treturn 0, factory, testMode, cleanup\n\t}\n\n\t// Get database config from environment\n\tdatabasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get database config: %v\", err)\n\t}\n\n\t// Get the specific database config\n\tvar envDbConfig EnvDatabaseConfig\n\tvar exists bool\n\tif databaseName == \"\" {\n\t\t// Get first database if no name provided\n\t\tfor _, config := range databasesConfig {\n\t\t\tenvDbConfig = config\n\t\t\texists = true\n\t\t\tbreak\n\t\t}\n\t} else {\n\t\tenvDbConfig, exists = databasesConfig[databaseName]\n\t}\n\n\tif !exists {\n\t\tt.Fatalf(\"Database %s not found in configuration\", databaseName)\n\t}\n\n\t// Determine cluster mode based on database name\n\t// \"standalone\" creates a non-OSS-Cluster database for testing FAILING_OVER/FAILED_OVER notifications\n\t// Other names create OSS Cluster databases for testing SMIGRATING/SMIGRATED notifications\n\tclusterMode := databaseName != \"standalone\"\n\n\t// Convert to DatabaseConfig with appropriate cluster mode\n\tdbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envDbConfig, fmt.Sprintf(\"e2e-test-%s-%d\", databaseName, time.Now().Unix()), clusterMode)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to convert config: %v\", err)\n\t}\n\n\t// Create fault injector with cleanup\n\tfaultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create fault injector: %v\", err)\n\t}\n\n\t// Create database manager\n\tdbManager := NewTestDatabaseManager(t, faultInjector, 0)\n\n\t// Create the database and get the actual connection config from fault injector\n\tbdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)\n\tif err != nil {\n\t\tfiCleanup()\n\t\tt.Fatalf(\"[ERROR] Failed to create test database: %v\", err)\n\t}\n\n\t// Use certificate location from original config if not provided by fault injector\n\tif newEnvConfig.CertificatesLocation == \"\" && envDbConfig.CertificatesLocation != \"\" {\n\t\tnewEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation\n\t}\n\n\t// Propagate OSSCluster flag since fault injector response may not include it\n\tnewEnvConfig.OSSCluster = dbConfig.OSSCluster\n\n\t// Convert EnvDatabaseConfig to RedisConnectionConfig\n\tredisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)\n\tif err != nil {\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t\tt.Fatalf(\"Failed to convert database config: %v\", err)\n\t}\n\n\t// Create client factory with the actual config from fault injector\n\tfactory = NewClientFactory(redisConfig)\n\n\t// Get real fault injector test mode config\n\ttestMode = GetTestModeConfig()\n\n\t// Combined cleanup function\n\tcleanup = func() {\n\t\tfactory.DestroyAll()\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t}\n\n\treturn bdbID, factory, testMode, cleanup\n}\n\n// SetupTestDatabaseAndFactoryWithConfig creates a database with custom config and returns both bdbID, factory, test mode config, and cleanup function\n//\n// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it will use the Docker proxy setup (127.0.0.1:17000) instead of creating a new database.\n// This allows tests to work with either the real fault injector OR the Docker proxy setup.\n//\n// Usage:\n//\n//\tbdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactoryWithConfig(t, ctx, \"standalone\", dbConfig)\n//\tdefer cleanup()\nfunc SetupTestDatabaseAndFactoryWithConfig(t *testing.T, ctx context.Context, databaseName string, dbConfig DatabaseConfig) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, cleanup func()) {\n\t// Get environment config to use as template for connection details\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\t// No environment config - use Docker proxy setup\n\t\tt.Logf(\"No environment config found, using Docker proxy setup at 127.0.0.1:17000\")\n\n\t\t// Create a simple Redis connection config for Docker proxy\n\t\t// The proxy simulates a cluster, so we set IsClusterMode to true\n\t\tredisConfig := &RedisConnectionConfig{\n\t\t\tHost:          \"127.0.0.1\", // Use 127.0.0.1 to force IPv4\n\t\t\tPort:          17000,\n\t\t\tUsername:      \"\",\n\t\t\tPassword:      \"\",\n\t\t\tTLS:           false,\n\t\t\tBdbID:         0,\n\t\t\tIsClusterMode: true, // Docker proxy simulates a cluster\n\t\t}\n\n\t\tfactory = NewClientFactory(redisConfig)\n\n\t\t// Get proxy mock test mode config\n\t\ttestMode = GetTestModeConfig()\n\n\t\t// No-op cleanup since we're not creating a database\n\t\tcleanup = func() {\n\t\t\tfactory.DestroyAll()\n\t\t}\n\n\t\treturn 0, factory, testMode, cleanup\n\t}\n\n\t// Get database config from environment\n\tdatabasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get database config: %v\", err)\n\t}\n\n\t// Get the specific database config as template\n\tvar envDbConfig EnvDatabaseConfig\n\tvar exists bool\n\tif databaseName == \"\" {\n\t\t// Get first database if no name provided\n\t\tfor _, config := range databasesConfig {\n\t\t\tenvDbConfig = config\n\t\t\texists = true\n\t\t\tbreak\n\t\t}\n\t} else {\n\t\tenvDbConfig, exists = databasesConfig[databaseName]\n\t}\n\n\tif !exists {\n\t\tt.Fatalf(\"Database %s not found in configuration\", databaseName)\n\t}\n\n\t// Create fault injector with cleanup\n\tfaultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create fault injector: %v\", err)\n\t}\n\n\t// Create database manager\n\tdbManager := NewTestDatabaseManager(t, faultInjector, 0)\n\n\t// Create the database and get the actual connection config from fault injector\n\tbdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)\n\tif err != nil {\n\t\tfiCleanup()\n\t\tt.Fatalf(\"[ERROR] Failed to create test database: %v\", err)\n\t}\n\n\t// Use certificate location from original config if not provided by fault injector\n\tif newEnvConfig.CertificatesLocation == \"\" && envDbConfig.CertificatesLocation != \"\" {\n\t\tnewEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation\n\t}\n\n\t// Convert EnvDatabaseConfig to RedisConnectionConfig\n\tredisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)\n\tif err != nil {\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t\tt.Fatalf(\"Failed to convert database config: %v\", err)\n\t}\n\n\t// Create client factory with the actual config from fault injector\n\tfactory = NewClientFactory(redisConfig)\n\n\t// Get real fault injector test mode config\n\ttestMode = GetTestModeConfig()\n\n\t// Combined cleanup function\n\tcleanup = func() {\n\t\tfactory.DestroyAll()\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t}\n\n\treturn bdbID, factory, testMode, cleanup\n}\n\n// GetSlotMigrateRequirementsCount returns the number of database requirements for a given effect/variant.\n// This is useful for iterating over all required databases when running tests.\n// Returns 0 if the effect/variant combination is not found or if in proxy mode (which always returns 1).\nfunc GetSlotMigrateRequirementsCount(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant) int {\n\t// Get environment config\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\t// No environment config - use Docker proxy setup (always 1 requirement)\n\t\treturn 1\n\t}\n\n\t// Create fault injector client using the URL from environment config\n\tfiClient := NewFaultInjectorClient(envConfig.FaultInjectorURL)\n\n\t// Query the fault injector for the required database configuration\n\ttriggersResp, err := fiClient.GetSlotMigrateTriggers(ctx, effect, 0)\n\tif err != nil {\n\t\tt.Logf(\"Warning: Failed to get slot-migrate triggers: %v\", err)\n\t\treturn 1\n\t}\n\n\t// Find the trigger that matches our variant\n\tfor _, trigger := range triggersResp.Triggers {\n\t\tif trigger.Name == string(variant) || (variant == SlotMigrateVariantDefault && trigger.Name == \"migrate\") {\n\t\t\treturn len(trigger.Requirements)\n\t\t}\n\t}\n\n\treturn 1\n}\n\n// SetupTestDatabaseForSlotMigrate creates a database configured for a specific slot-migrate effect\n// It queries the fault injector's GET /slot-migrate endpoint to get the required database configuration\n// and creates the database with those settings.\n//\n// Usage:\n//\n//\tbdbID, factory, testMode, fiClient, cleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)\n//\tdefer cleanup()\nfunc SetupTestDatabaseForSlotMigrate(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, fiClient *FaultInjectorClient, cleanup func()) {\n\treturn SetupTestDatabaseForSlotMigrateWithRequirementIndex(t, ctx, effect, variant, 0)\n}\n\n// SetupTestDatabaseForSlotMigrateWithRequirementIndex creates a database configured for a specific slot-migrate effect\n// using the requirement at the specified index. This allows testing against all required database configurations.\n//\n// Usage:\n//\n//\treqCount := GetSlotMigrateRequirementsCount(t, ctx, effect, variant)\n//\tfor i := 0; i < reqCount; i++ {\n//\t    bdbID, factory, testMode, fiClient, cleanup := SetupTestDatabaseForSlotMigrateWithRequirementIndex(t, ctx, effect, variant, i)\n//\t    defer cleanup()\n//\t    // run test...\n//\t}\nfunc SetupTestDatabaseForSlotMigrateWithRequirementIndex(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant, requirementIndex int) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, fiClient *FaultInjectorClient, cleanup func()) {\n\t// Get environment config\n\tenvConfig, err := GetEnvConfig()\n\tif err != nil {\n\t\t// No environment config - use Docker proxy setup\n\t\tt.Logf(\"No environment config found, using Docker proxy setup at 127.0.0.1:17000\")\n\n\t\tredisConfig := &RedisConnectionConfig{\n\t\t\tHost:          \"127.0.0.1\",\n\t\t\tPort:          17000,\n\t\t\tUsername:      \"\",\n\t\t\tPassword:      \"\",\n\t\t\tTLS:           false,\n\t\t\tBdbID:         0,\n\t\t\tIsClusterMode: true,\n\t\t}\n\n\t\tfactory = NewClientFactory(redisConfig)\n\t\ttestMode = GetTestModeConfig()\n\n\t\t// Create fault injector client for proxy mode using Docker fault injector URL\n\t\tfiClient = NewFaultInjectorClient(\"http://localhost:15000\")\n\n\t\t// Call CreateDatabase to initialize the proxy fault injector with 2 nodes\n\t\t// This is required for slot-shuffle, remove, and remove-add effects which need >= 2 nodes\n\t\tresp, err := fiClient.CreateDatabase(ctx, 0, DatabaseConfig{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to trigger CreateDatabase on proxy fault injector: %v\", err)\n\t\t}\n\n\t\t// Wait for the action to complete\n\t\tstatus, err := fiClient.WaitForAction(ctx, resp.ActionID,\n\t\t\tWithMaxWaitTime(10*time.Second),\n\t\t\tWithPollInterval(100*time.Millisecond))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to wait for CreateDatabase action: %v\", err)\n\t\t}\n\t\tif status.Status != StatusSuccess {\n\t\t\tt.Fatalf(\"CreateDatabase action failed: %v\", status.Error)\n\t\t}\n\n\t\tcleanup = func() {\n\t\t\tfactory.DestroyAll()\n\t\t}\n\n\t\treturn 0, factory, testMode, fiClient, cleanup\n\t}\n\n\t// Create fault injector client using the URL from environment config\n\tfiClient = NewFaultInjectorClient(envConfig.FaultInjectorURL)\n\n\t// Query the fault injector for the required database configuration\n\tt.Logf(\"Querying fault injector for %s effect requirements...\", effect)\n\tif debugE2E() {\n\t\tslotMigrateURL := fmt.Sprintf(\"%s/slot-migrate?effect=%s&cluster_index=0\", envConfig.FaultInjectorURL, effect)\n\t\tt.Logf(\"  URL: %s\", slotMigrateURL)\n\n\t\t// Get raw response for debugging\n\t\trawResp, err := fiClient.GetSlotMigrateTriggersRaw(ctx, effect, 0)\n\t\tif err != nil {\n\t\t\tt.Logf(\"Warning: Failed to get raw slot-migrate triggers: %v\", err)\n\t\t} else {\n\t\t\tt.Logf(\"  Raw response: %s\", string(rawResp))\n\t\t}\n\t}\n\n\ttriggersResp, err := fiClient.GetSlotMigrateTriggers(ctx, effect, 0)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get slot-migrate triggers: %v\", err)\n\t}\n\n\t// Log the full response (only in debug mode)\n\tif debugE2E() {\n\t\tt.Logf(\"  Response: Effect=%s, Triggers=%d\", triggersResp.Effect, len(triggersResp.Triggers))\n\t\tfor i, trigger := range triggersResp.Triggers {\n\t\t\tt.Logf(\"    Trigger[%d]: Name=%s, Description=%s, Requirements=%d\", i, trigger.Name, trigger.Description, len(trigger.Requirements))\n\t\t\tfor j, req := range trigger.Requirements {\n\t\t\t\tt.Logf(\"      Requirement[%d]: DBConfig=%v, Cluster=%v, Description=%s\", j, req.DBConfig, req.Cluster, req.Description)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Find the trigger that matches our variant\n\tvar requiredDBConfig map[string]interface{}\n\tfor _, trigger := range triggersResp.Triggers {\n\t\tif trigger.Name == string(variant) || (variant == SlotMigrateVariantDefault && trigger.Name == \"migrate\") {\n\t\t\tif requirementIndex < len(trigger.Requirements) {\n\t\t\t\trequiredDBConfig = trigger.Requirements[requirementIndex].DBConfig\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"Found required DB config for %s/%s (requirement %d/%d): %v\",\n\t\t\t\t\t\teffect, variant, requirementIndex+1, len(trigger.Requirements), requiredDBConfig)\n\t\t\t\t}\n\t\t\t} else if len(trigger.Requirements) > 0 {\n\t\t\t\t// Fall back to first requirement if index is out of bounds\n\t\t\t\trequiredDBConfig = trigger.Requirements[0].DBConfig\n\t\t\t\tt.Logf(\"Warning: requirement index %d out of bounds (max %d), using first requirement\",\n\t\t\t\t\trequirementIndex, len(trigger.Requirements)-1)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Get database config from environment\n\tdatabasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get database config: %v\", err)\n\t}\n\n\t// Get the first database config\n\tvar envDbConfig EnvDatabaseConfig\n\tfor _, config := range databasesConfig {\n\t\tenvDbConfig = config\n\t\tbreak\n\t}\n\n\t// Convert to DatabaseConfig\n\tdbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfig(envDbConfig, fmt.Sprintf(\"e2e-slotmigrate-%d\", time.Now().Unix()))\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to convert config: %v\", err)\n\t}\n\n\t// Apply the required configuration from fault injector\n\tif requiredDBConfig != nil {\n\t\tif shardsCount, ok := requiredDBConfig[\"shards_count\"]; ok {\n\t\t\tif sc, ok := shardsCount.(float64); ok {\n\t\t\t\tdbConfig.ShardsCount = int(sc)\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"Setting ShardsCount to %d (from fault injector)\", dbConfig.ShardsCount)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif shardsPlacement, ok := requiredDBConfig[\"shards_placement\"]; ok {\n\t\t\tif sp, ok := shardsPlacement.(string); ok {\n\t\t\t\tdbConfig.ShardsPlacement = sp\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"Setting ShardsPlacement to %s (from fault injector)\", dbConfig.ShardsPlacement)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif replication, ok := requiredDBConfig[\"replication\"]; ok {\n\t\t\tif r, ok := replication.(bool); ok {\n\t\t\t\tdbConfig.Replication = r\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"Setting Replication to %v (from fault injector)\", dbConfig.Replication)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ossCluster, ok := requiredDBConfig[\"oss_cluster\"]; ok {\n\t\t\tif oc, ok := ossCluster.(bool); ok {\n\t\t\t\tdbConfig.OSSCluster = oc\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"Setting OSSCluster to %v (from fault injector)\", dbConfig.OSSCluster)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif sharding, ok := requiredDBConfig[\"sharding\"]; ok {\n\t\t\tif s, ok := sharding.(bool); ok {\n\t\t\t\tdbConfig.Sharding = s\n\t\t\t\tif debugE2E() {\n\t\t\t\t\tt.Logf(\"Setting Sharding to %v (from fault injector)\", dbConfig.Sharding)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif debugE2E() {\n\t\tt.Logf(\"Creating database with config: ShardsCount=%d, ShardsPlacement=%s, Replication=%v, OSSCluster=%v\",\n\t\t\tdbConfig.ShardsCount, dbConfig.ShardsPlacement, dbConfig.Replication, dbConfig.OSSCluster)\n\t}\n\n\t// Create fault injector with cleanup\n\tfaultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create fault injector: %v\", err)\n\t}\n\n\t// Create database manager\n\tdbManager := NewTestDatabaseManager(t, faultInjector, 0)\n\n\t// Create the database and get the actual connection config from fault injector\n\tbdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)\n\tif err != nil {\n\t\tfiCleanup()\n\t\tt.Fatalf(\"[ERROR] Failed to create test database: %v\", err)\n\t}\n\n\t// Use certificate location from original config if not provided by fault injector\n\tif newEnvConfig.CertificatesLocation == \"\" && envDbConfig.CertificatesLocation != \"\" {\n\t\tnewEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation\n\t}\n\n\t// Propagate OSSCluster flag since fault injector response may not include it\n\tnewEnvConfig.OSSCluster = dbConfig.OSSCluster\n\n\t// Convert EnvDatabaseConfig to RedisConnectionConfig\n\tredisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)\n\tif err != nil {\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t\tt.Fatalf(\"Failed to convert database config: %v\", err)\n\t}\n\n\t// Create client factory with the actual config from fault injector\n\tfactory = NewClientFactory(redisConfig)\n\n\t// Get real fault injector test mode config\n\ttestMode = GetTestModeConfig()\n\n\t// Combined cleanup function\n\tcleanup = func() {\n\t\tfactory.DestroyAll()\n\t\tdbManager.Cleanup(ctx)\n\t\tfiCleanup()\n\t}\n\n\treturn bdbID, factory, testMode, fiClient, cleanup\n}\n"
  },
  {
    "path": "maintnotifications/e2e/doc.go",
    "content": "// Package e2e provides end-to-end testing scenarios for the maintenance notifications system.\n//\n// This package contains comprehensive test scenarios that validate the maintenance notifications\n// functionality in realistic environments. The tests are designed to work with Redis Enterprise\n// clusters and require specific environment configuration.\n//\n// Environment Variables:\n//   - E2E_SCENARIO_TESTS: Set to \"true\" to enable scenario tests\n//   - REDIS_ENDPOINTS_CONFIG_PATH: Path to endpoints configuration file\n//   - FAULT_INJECTION_API_URL: URL for fault injection API (optional)\n//\n// Test Scenarios:\n//   - Basic Push Notifications: Core functionality testing\n//   - Endpoint Types: Different endpoint resolution strategies\n//   - Timeout Configurations: Various timeout strategies\n//   - TLS Configurations: Different TLS setups\n//   - Stress Testing: Extreme load and concurrent operations\n//\n// Note: Maintenance notifications are currently supported only in standalone Redis clients.\n// Cluster clients (ClusterClient, FailoverClient, etc.) do not yet support this functionality.\npackage e2e\n"
  },
  {
    "path": "maintnotifications/e2e/examples/endpoints.json",
    "content": "{\n  \"standalone0\": {\n    \"password\": \"foobared\",\n    \"tls\": false,\n    \"endpoints\": [\n      \"redis://localhost:6379\"\n    ]\n  },\n  \"standalone0-tls\": {\n    \"username\": \"default\",\n    \"password\": \"foobared\",\n    \"tls\": true,\n    \"certificatesLocation\": \"redis1-2-5-8-sentinel/work/tls\",\n    \"endpoints\": [\n      \"rediss://localhost:6390\"\n    ]\n  },\n  \"standalone0-acl\": {\n    \"username\": \"acljedis\",\n    \"password\": \"fizzbuzz\",\n    \"tls\": false,\n    \"endpoints\": [\n      \"redis://localhost:6379\"\n    ]\n  },\n  \"standalone0-acl-tls\": {\n    \"username\": \"acljedis\",\n    \"password\": \"fizzbuzz\",\n    \"tls\": true,\n    \"certificatesLocation\": \"redis1-2-5-8-sentinel/work/tls\",\n    \"endpoints\": [\n      \"rediss://localhost:6390\"\n    ]\n  },\n  \"cluster0\": {\n    \"username\": \"default\",\n    \"password\": \"foobared\",\n    \"tls\": false,\n    \"endpoints\": [\n      \"redis://localhost:7001\",\n      \"redis://localhost:7002\",\n      \"redis://localhost:7003\",\n      \"redis://localhost:7004\",\n      \"redis://localhost:7005\",\n      \"redis://localhost:7006\"\n    ]\n  },\n  \"cluster0-tls\": {\n    \"username\": \"default\",\n    \"password\": \"foobared\",\n    \"tls\": true,\n    \"certificatesLocation\": \"redis1-2-5-8-sentinel/work/tls\",\n    \"endpoints\": [\n      \"rediss://localhost:7011\",\n      \"rediss://localhost:7012\",\n      \"rediss://localhost:7013\",\n      \"rediss://localhost:7014\",\n      \"rediss://localhost:7015\",\n      \"rediss://localhost:7016\"\n    ]\n  },\n  \"sentinel0\": {\n    \"username\": \"default\",\n    \"password\": \"foobared\",\n    \"tls\": false,\n    \"endpoints\": [\n      \"redis://localhost:26379\",\n      \"redis://localhost:26380\",\n      \"redis://localhost:26381\"\n    ]\n  },\n  \"modules-docker\": {\n    \"tls\": false,\n    \"endpoints\": [\n      \"redis://localhost:6479\"\n    ]\n  },\n  \"enterprise-cluster\": {\n    \"bdb_id\": 1,\n    \"username\": \"default\",\n    \"password\": \"enterprise-password\",\n    \"tls\": true,\n    \"raw_endpoints\": [\n      {\n        \"addr\": [\"10.0.0.1\"],\n        \"addr_type\": \"ipv4\",\n        \"dns_name\": \"redis-enterprise-cluster.example.com\",\n        \"oss_cluster_api_preferred_endpoint_type\": \"internal\",\n        \"oss_cluster_api_preferred_ip_type\": \"ipv4\",\n        \"port\": 12000,\n        \"proxy_policy\": \"single\",\n        \"uid\": \"endpoint-1\"\n      },\n      {\n        \"addr\": [\"10.0.0.2\"],\n        \"addr_type\": \"ipv4\", \n        \"dns_name\": \"redis-enterprise-cluster-2.example.com\",\n        \"oss_cluster_api_preferred_endpoint_type\": \"internal\",\n        \"oss_cluster_api_preferred_ip_type\": \"ipv4\",\n        \"port\": 12000,\n        \"proxy_policy\": \"single\",\n        \"uid\": \"endpoint-2\"\n      }\n    ],\n    \"endpoints\": [\n      \"rediss://redis-enterprise-cluster.example.com:12000\",\n      \"rediss://redis-enterprise-cluster-2.example.com:12000\"\n    ]\n  }\n}\n"
  },
  {
    "path": "maintnotifications/e2e/fault_injector.go",
    "content": "package e2e\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// ActionType represents the type of fault injection action\ntype ActionType string\n\n// ActionListItem represents a single action in the list actions response\ntype ActionListItem struct {\n\tJobID       string `json:\"job_id\"`\n\tActionType  string `json:\"action_type\"`\n\tStatus      string `json:\"status\"`\n\tSubmittedAt string `json:\"submitted_at\"`\n}\n\nconst (\n\t// Redis cluster actions\n\tActionClusterFailover   ActionType = \"cluster_failover\"\n\tActionClusterReshard    ActionType = \"cluster_reshard\"\n\tActionClusterAddNode    ActionType = \"cluster_add_node\"\n\tActionClusterRemoveNode ActionType = \"cluster_remove_node\"\n\tActionClusterMigrate    ActionType = \"cluster_migrate\"\n\n\t// Node-level actions\n\tActionNodeRestart ActionType = \"node_restart\"\n\tActionNodeStop    ActionType = \"node_stop\"\n\tActionNodeStart   ActionType = \"node_start\"\n\tActionNodeKill    ActionType = \"node_kill\"\n\n\t// Network simulation actions\n\tActionNetworkPartition  ActionType = \"network_partition\"\n\tActionNetworkLatency    ActionType = \"network_latency\"\n\tActionNetworkPacketLoss ActionType = \"network_packet_loss\"\n\tActionNetworkBandwidth  ActionType = \"network_bandwidth\"\n\tActionNetworkRestore    ActionType = \"network_restore\"\n\n\t// Redis configuration actions\n\tActionConfigChange    ActionType = \"config_change\"\n\tActionMaintenanceMode ActionType = \"maintenance_mode\"\n\tActionSlotMigration   ActionType = \"slot_migrate\"\n\n\t// Sequence and complex actions\n\tActionSequence       ActionType = \"sequence_of_actions\"\n\tActionExecuteCommand ActionType = \"execute_command\"\n\n\t// Database management actions\n\tActionDeleteDatabase ActionType = \"delete_database\"\n\tActionCreateDatabase ActionType = \"create_database\"\n\tActionFailover       ActionType = \"failover\"\n\tActionMigrate        ActionType = \"migrate\"\n\tActionBind           ActionType = \"bind\"\n\n\t// Slot migrate action (OSS Cluster API testing)\n\tActionSlotMigrate ActionType = \"slot_migrate\"\n)\n\n// SlotMigrateEffect represents the effect type for slot migration\ntype SlotMigrateEffect string\n\nconst (\n\t// SlotMigrateEffectRemoveAdd migrates all shards from source node to empty node\n\t// Result: One endpoint removed, one endpoint added\n\tSlotMigrateEffectRemoveAdd SlotMigrateEffect = \"remove-add\"\n\n\t// SlotMigrateEffectRemove migrates all shards from source node to existing node\n\t// Result: One endpoint removed\n\tSlotMigrateEffectRemove SlotMigrateEffect = \"remove\"\n\n\t// SlotMigrateEffectAdd migrates one shard to empty node\n\t// Result: One endpoint added\n\tSlotMigrateEffectAdd SlotMigrateEffect = \"add\"\n\n\t// SlotMigrateEffectSlotShuffle migrates one shard between existing nodes\n\t// Result: Slots move, endpoints unchanged\n\tSlotMigrateEffectSlotShuffle SlotMigrateEffect = \"slot-shuffle\"\n)\n\n// SlotMigrateVariant represents the mechanism to achieve the slot migration effect\ntype SlotMigrateVariant string\n\nconst (\n\t// SlotMigrateVariantDefault is an alias for migrate\n\tSlotMigrateVariantDefault SlotMigrateVariant = \"default\"\n\n\t// SlotMigrateVariantMigrate uses rladmin migrate to move shards\n\tSlotMigrateVariantMigrate SlotMigrateVariant = \"migrate\"\n\n\t// SlotMigrateVariantFailover triggers failover to swap master/replica roles\n\t// Requires replication to be enabled\n\tSlotMigrateVariantFailover SlotMigrateVariant = \"failover\"\n)\n\n// SlotMigrateRequest represents a request to trigger a slot migration\ntype SlotMigrateRequest struct {\n\tEffect       SlotMigrateEffect  `json:\"effect\"`\n\tBdbID        string             `json:\"bdb_id\"`\n\tClusterIndex int                `json:\"cluster_index,omitempty\"`\n\tTrigger      SlotMigrateVariant `json:\"variant,omitempty\"`\n\tSourceNode   *int               `json:\"source_node,omitempty\"`\n\tTargetNode   *int               `json:\"target_node,omitempty\"`\n}\n\n// SlotMigrateTrigger represents a trigger configuration for slot migration\ntype SlotMigrateTrigger struct {\n\tName         string                          `json:\"name\"`\n\tDescription  string                          `json:\"description\"`\n\tRequirements []SlotMigrateTriggerRequirement `json:\"requirements\"`\n}\n\n// SlotMigrateTriggerRequirement represents database configuration requirements\ntype SlotMigrateTriggerRequirement struct {\n\tDBConfig    map[string]interface{} `json:\"dbconfig\"`\n\tCluster     map[string]interface{} `json:\"cluster\"`\n\tDescription string                 `json:\"description\"`\n}\n\n// SlotMigrateTriggersResponse represents the response from GET /slot-migrate\ntype SlotMigrateTriggersResponse struct {\n\tEffect   SlotMigrateEffect      `json:\"effect\"`\n\tCluster  map[string]interface{} `json:\"cluster\"`\n\tTriggers []SlotMigrateTrigger   `json:\"triggers\"`\n}\n\n// ActionStatus represents the status of an action\ntype ActionStatus string\n\nconst (\n\tStatusPending   ActionStatus = \"pending\"\n\tStatusRunning   ActionStatus = \"running\"\n\tStatusFinished  ActionStatus = \"finished\"\n\tStatusFailed    ActionStatus = \"failed\"\n\tStatusSuccess   ActionStatus = \"success\"\n\tStatusCancelled ActionStatus = \"cancelled\"\n)\n\n// ActionRequest represents a request to trigger an action\ntype ActionRequest struct {\n\tType       ActionType             `json:\"type\"`\n\tParameters map[string]interface{} `json:\"parameters,omitempty\"`\n}\n\n// ActionResponse represents the response from triggering an action\ntype ActionResponse struct {\n\tActionID string `json:\"action_id\"`\n\tStatus   string `json:\"status\"`\n\tMessage  string `json:\"message,omitempty\"`\n}\n\n// ActionStatusResponse represents the status of an action\ntype ActionStatusResponse struct {\n\tActionID  string                 `json:\"action_id\"`\n\tStatus    ActionStatus           `json:\"status\"`\n\tError     interface{}            `json:\"error,omitempty\"`\n\tOutput    map[string]interface{} `json:\"output,omitempty\"`\n\tProgress  float64                `json:\"progress,omitempty\"`\n\tStartTime time.Time              `json:\"start_time,omitempty\"`\n\tEndTime   time.Time              `json:\"end_time,omitempty\"`\n}\n\n// SequenceAction represents an action in a sequence\ntype SequenceAction struct {\n\tType       ActionType             `json:\"type\"`\n\tParameters map[string]interface{} `json:\"params,omitempty\"`\n\tDelay      time.Duration          `json:\"delay,omitempty\"`\n}\n\n// FaultInjectorClient provides programmatic control over test infrastructure\ntype FaultInjectorClient struct {\n\tbaseURL    string\n\thttpClient *http.Client\n}\n\n// NewFaultInjectorClient creates a new fault injector client\nfunc NewFaultInjectorClient(baseURL string) *FaultInjectorClient {\n\treturn &FaultInjectorClient{\n\t\tbaseURL: strings.TrimSuffix(baseURL, \"/\"),\n\t\thttpClient: &http.Client{\n\t\t\tTimeout: 30 * time.Second,\n\t\t},\n\t}\n}\n\n// GetBaseURL returns the base URL of the fault injector server\nfunc (c *FaultInjectorClient) GetBaseURL() string {\n\treturn c.baseURL\n}\n\n// ActionsListResponse is the wrapper response from the /action GET endpoint\ntype ActionsListResponse struct {\n\tActions []ActionListItem `json:\"actions\"`\n}\n\n// ListActions lists all available actions\nfunc (c *FaultInjectorClient) ListActions(ctx context.Context) ([]ActionListItem, error) {\n\tvar response ActionsListResponse\n\terr := c.request(ctx, \"GET\", \"/action\", nil, &response)\n\treturn response.Actions, err\n}\n\n// TriggerAction triggers a specific action\nfunc (c *FaultInjectorClient) TriggerAction(ctx context.Context, action ActionRequest) (*ActionResponse, error) {\n\tvar response ActionResponse\n\tfmt.Printf(\"[FI] Triggering action: %+v\\n\", action)\n\terr := c.request(ctx, \"POST\", \"/action\", action, &response)\n\treturn &response, err\n}\n\nfunc (c *FaultInjectorClient) TriggerSequence(ctx context.Context, bdbID int, actions []SequenceAction) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionSequence,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"bdb_id\":  bdbID,\n\t\t\t\"actions\": actions,\n\t\t},\n\t})\n}\n\n// GetActionStatus gets the status of a specific action\nfunc (c *FaultInjectorClient) GetActionStatus(ctx context.Context, actionID string) (*ActionStatusResponse, error) {\n\tvar status ActionStatusResponse\n\terr := c.request(ctx, \"GET\", fmt.Sprintf(\"/action/%s\", actionID), nil, &status)\n\treturn &status, err\n}\n\n// WaitForAction waits for an action to complete\nfunc (c *FaultInjectorClient) WaitForAction(ctx context.Context, actionID string, options ...WaitOption) (*ActionStatusResponse, error) {\n\tconfig := &waitConfig{\n\t\tpollInterval: 1 * time.Second,\n\t\tmaxWaitTime:  60 * time.Second,\n\t}\n\n\tfor _, opt := range options {\n\t\topt(config)\n\t}\n\n\tdeadline := time.Now().Add(config.maxWaitTime)\n\tticker := time.NewTicker(config.pollInterval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tcase <-time.After(time.Until(deadline)):\n\t\t\treturn nil, fmt.Errorf(\"timeout waiting for action %s after %v\", actionID, config.maxWaitTime)\n\t\tcase <-ticker.C:\n\t\t\tstatus, err := c.GetActionStatus(ctx, actionID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to get action status: %w\", err)\n\t\t\t}\n\n\t\t\tswitch status.Status {\n\t\t\tcase StatusFinished, StatusSuccess, StatusFailed, StatusCancelled:\n\t\t\t\treturn status, nil\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Cluster Management Actions\n\n// TriggerClusterFailover triggers a cluster failover\nfunc (c *FaultInjectorClient) TriggerClusterFailover(ctx context.Context, nodeID string, force bool) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionClusterFailover,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\": nodeID,\n\t\t\t\"force\":   force,\n\t\t},\n\t})\n}\n\n// TriggerClusterReshard is DEPRECATED - use TriggerSlotMigrateSlotShuffle instead\n// The fault injector does not support a 'cluster_reshard' action type.\n// Use the /slot-migrate endpoint with effect=slot-shuffle instead.\nfunc (c *FaultInjectorClient) TriggerClusterReshard(ctx context.Context, slots []int, sourceNode, targetNode string) (*ActionResponse, error) {\n\treturn nil, fmt.Errorf(\"TriggerClusterReshard is deprecated: action type 'cluster_reshard' does not exist in fault injector. Use TriggerSlotMigrateSlotShuffle instead\")\n}\n\n// TriggerSlotMigration triggers migration of specific slots (legacy API)\nfunc (c *FaultInjectorClient) TriggerSlotMigration(ctx context.Context, startSlot, endSlot int, sourceNode, targetNode string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionSlotMigration,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"start_slot\":  startSlot,\n\t\t\t\"end_slot\":    endSlot,\n\t\t\t\"source_node\": sourceNode,\n\t\t\t\"target_node\": targetNode,\n\t\t},\n\t})\n}\n\n// Slot Migrate Actions (OSS Cluster API Testing)\n// These methods use the /slot-migrate endpoint for testing cluster topology changes\n\n// GetSlotMigrateTriggers returns available triggers for a slot migration effect\n// This is useful for discovering what database configurations are needed for each effect/variant\nfunc (c *FaultInjectorClient) GetSlotMigrateTriggers(ctx context.Context, effect SlotMigrateEffect, clusterIndex int) (*SlotMigrateTriggersResponse, error) {\n\tvar response SlotMigrateTriggersResponse\n\tpath := fmt.Sprintf(\"/slot-migrate?effect=%s&cluster_index=%d\", effect, clusterIndex)\n\tfmt.Printf(\"[FI] GET slot-migrate: %+v\\n\", path)\n\terr := c.request(ctx, \"GET\", path, nil, &response)\n\treturn &response, err\n}\n\n// GetSlotMigrateTriggersRaw returns the raw JSON response from GET /slot-migrate\n// This is useful for debugging when the response structure doesn't match expectations\nfunc (c *FaultInjectorClient) GetSlotMigrateTriggersRaw(ctx context.Context, effect SlotMigrateEffect, clusterIndex int) ([]byte, error) {\n\tpath := fmt.Sprintf(\"/slot-migrate?effect=%s&cluster_index=%d\", effect, clusterIndex)\n\turl := c.baseURL + path\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response body: %w\", err)\n\t}\n\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, string(respBody))\n\t}\n\n\treturn respBody, nil\n}\n\n// TriggerSlotMigrate triggers a slot migration with the specified effect and variant\n// This is the new API for testing OSS Cluster API client behavior during endpoint changes\n//\n// Effects:\n//   - remove-add: One endpoint removed, one added (migrate all shards to empty node)\n//   - remove: One endpoint removed (migrate all shards to existing node)\n//   - add: One endpoint added (migrate one shard to empty node)\n//   - slot-shuffle: Slots moved without endpoint change (migrate one shard between existing nodes)\n//\n// Variants:\n//   - default/migrate: Use rladmin migrate to move shards\n//   - maintenance_mode: Put node in maintenance mode (only for remove-add, remove)\n//   - failover: Trigger failover to swap master/replica roles (requires replication)\nfunc (c *FaultInjectorClient) TriggerSlotMigrate(ctx context.Context, req SlotMigrateRequest) (*ActionResponse, error) {\n\tvar response ActionResponse\n\n\t// Build query parameters\n\tpath := fmt.Sprintf(\"/slot-migrate?effect=%s&bdb_id=%s&cluster_index=%d\",\n\t\treq.Effect, req.BdbID, req.ClusterIndex)\n\n\tif req.Trigger != \"\" {\n\t\tpath += fmt.Sprintf(\"&trigger=%s\", req.Trigger)\n\t}\n\tif req.SourceNode != nil {\n\t\tpath += fmt.Sprintf(\"&source_node=%d\", *req.SourceNode)\n\t}\n\tif req.TargetNode != nil {\n\t\tpath += fmt.Sprintf(\"&target_node=%d\", *req.TargetNode)\n\t}\n\n\tfmt.Printf(\"[FI] POST slot-migrate: %+v\\n %+v\\n\", path, req)\n\terr := c.request(ctx, \"POST\", path, nil, &response)\n\treturn &response, err\n}\n\n// TriggerSlotMigrateRemoveAdd triggers a remove-add slot migration\n// This migrates all shards from source node to an empty node\n// Result: One endpoint removed, one endpoint added\nfunc (c *FaultInjectorClient) TriggerSlotMigrateRemoveAdd(ctx context.Context, bdbID string, trigger SlotMigrateVariant) (*ActionResponse, error) {\n\treturn c.TriggerSlotMigrate(ctx, SlotMigrateRequest{\n\t\tEffect:  SlotMigrateEffectRemoveAdd,\n\t\tBdbID:   bdbID,\n\t\tTrigger: trigger,\n\t})\n}\n\n// TriggerSlotMigrateRemove triggers a remove slot migration\n// This migrates all shards from source node to an existing node\n// Result: One endpoint removed\nfunc (c *FaultInjectorClient) TriggerSlotMigrateRemove(ctx context.Context, bdbID string, trigger SlotMigrateVariant) (*ActionResponse, error) {\n\treturn c.TriggerSlotMigrate(ctx, SlotMigrateRequest{\n\t\tEffect:  SlotMigrateEffectRemove,\n\t\tBdbID:   bdbID,\n\t\tTrigger: trigger,\n\t})\n}\n\n// TriggerSlotMigrateAdd triggers an add slot migration\n// This migrates one shard to an empty node\n// Result: One endpoint added\nfunc (c *FaultInjectorClient) TriggerSlotMigrateAdd(ctx context.Context, bdbID string, trigger SlotMigrateVariant) (*ActionResponse, error) {\n\treturn c.TriggerSlotMigrate(ctx, SlotMigrateRequest{\n\t\tEffect:  SlotMigrateEffectAdd,\n\t\tBdbID:   bdbID,\n\t\tTrigger: trigger,\n\t})\n}\n\n// TriggerSlotMigrateSlotShuffle triggers a slot-shuffle migration\n// This migrates one shard between existing nodes\n// Result: Slots move, endpoints unchanged\nfunc (c *FaultInjectorClient) TriggerSlotMigrateSlotShuffle(ctx context.Context, bdbID string, trigger SlotMigrateVariant) (*ActionResponse, error) {\n\treturn c.TriggerSlotMigrate(ctx, SlotMigrateRequest{\n\t\tEffect:  SlotMigrateEffectSlotShuffle,\n\t\tBdbID:   bdbID,\n\t\tTrigger: trigger,\n\t})\n}\n\n// Node Management Actions\n\n// RestartNode restarts a specific Redis node\nfunc (c *FaultInjectorClient) RestartNode(ctx context.Context, nodeID string, graceful bool) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNodeRestart,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\":  nodeID,\n\t\t\t\"graceful\": graceful,\n\t\t},\n\t})\n}\n\n// StopNode stops a specific Redis node\nfunc (c *FaultInjectorClient) StopNode(ctx context.Context, nodeID string, graceful bool) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNodeStop,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\":  nodeID,\n\t\t\t\"graceful\": graceful,\n\t\t},\n\t})\n}\n\n// StartNode starts a specific Redis node\nfunc (c *FaultInjectorClient) StartNode(ctx context.Context, nodeID string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNodeStart,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\": nodeID,\n\t\t},\n\t})\n}\n\n// KillNode forcefully kills a Redis node\nfunc (c *FaultInjectorClient) KillNode(ctx context.Context, nodeID string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNodeKill,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\": nodeID,\n\t\t},\n\t})\n}\n\n// Network Simulation Actions\n\n// SimulateNetworkPartition simulates a network partition\nfunc (c *FaultInjectorClient) SimulateNetworkPartition(ctx context.Context, nodes []string, duration time.Duration) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNetworkPartition,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"nodes\":    nodes,\n\t\t\t\"duration\": duration.String(),\n\t\t},\n\t})\n}\n\n// SimulateNetworkLatency adds network latency\nfunc (c *FaultInjectorClient) SimulateNetworkLatency(ctx context.Context, nodes []string, latency time.Duration, jitter time.Duration) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNetworkLatency,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"nodes\":   nodes,\n\t\t\t\"latency\": latency.String(),\n\t\t\t\"jitter\":  jitter.String(),\n\t\t},\n\t})\n}\n\n// SimulatePacketLoss simulates packet loss\nfunc (c *FaultInjectorClient) SimulatePacketLoss(ctx context.Context, nodes []string, lossPercent float64) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNetworkPacketLoss,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"nodes\":        nodes,\n\t\t\t\"loss_percent\": lossPercent,\n\t\t},\n\t})\n}\n\n// LimitBandwidth limits network bandwidth\nfunc (c *FaultInjectorClient) LimitBandwidth(ctx context.Context, nodes []string, bandwidth string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNetworkBandwidth,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"nodes\":     nodes,\n\t\t\t\"bandwidth\": bandwidth,\n\t\t},\n\t})\n}\n\n// RestoreNetwork restores normal network conditions\nfunc (c *FaultInjectorClient) RestoreNetwork(ctx context.Context, nodes []string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionNetworkRestore,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"nodes\": nodes,\n\t\t},\n\t})\n}\n\n// Configuration Actions\n\n// ChangeConfig changes Redis configuration\nfunc (c *FaultInjectorClient) ChangeConfig(ctx context.Context, nodeID string, config map[string]string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionConfigChange,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\": nodeID,\n\t\t\t\"config\":  config,\n\t\t},\n\t})\n}\n\n// EnableMaintenanceMode enables maintenance mode\nfunc (c *FaultInjectorClient) EnableMaintenanceMode(ctx context.Context, nodeID string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionMaintenanceMode,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\": nodeID,\n\t\t\t\"enabled\": true,\n\t\t},\n\t})\n}\n\n// DisableMaintenanceMode disables maintenance mode\nfunc (c *FaultInjectorClient) DisableMaintenanceMode(ctx context.Context, nodeID string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionMaintenanceMode,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\": nodeID,\n\t\t\t\"enabled\": false,\n\t\t},\n\t})\n}\n\n// Database Management Actions\n\n// EnvDatabaseConfig represents the configuration for creating a database\ntype DatabaseConfig struct {\n\tName                         string                 `json:\"name\"`\n\tPort                         int                    `json:\"port\"`\n\tMemorySize                   int64                  `json:\"memory_size\"`\n\tReplication                  bool                   `json:\"replication\"`\n\tEvictionPolicy               string                 `json:\"eviction_policy\"`\n\tSharding                     bool                   `json:\"sharding\"`\n\tAutoUpgrade                  bool                   `json:\"auto_upgrade\"`\n\tShardsCount                  int                    `json:\"shards_count\"`\n\tModuleList                   []DatabaseModule       `json:\"module_list,omitempty\"`\n\tOSSCluster                   bool                   `json:\"oss_cluster\"`\n\tOSSClusterAPIPreferredIPType string                 `json:\"oss_cluster_api_preferred_ip_type,omitempty\"`\n\tProxyPolicy                  string                 `json:\"proxy_policy,omitempty\"`\n\tShardsPlacement              string                 `json:\"shards_placement,omitempty\"`\n\tShardKeyRegex                []ShardKeyRegexPattern `json:\"shard_key_regex,omitempty\"`\n}\n\n// DatabaseModule represents a Redis module configuration\ntype DatabaseModule struct {\n\tModuleArgs string `json:\"module_args\"`\n\tModuleName string `json:\"module_name\"`\n}\n\n// ShardKeyRegexPattern represents a shard key regex pattern\ntype ShardKeyRegexPattern struct {\n\tRegex string `json:\"regex\"`\n}\n\n// DeleteDatabase deletes a database\n// Parameters:\n//   - clusterIndex: The index of the cluster\n//   - bdbID: The database ID to delete\nfunc (c *FaultInjectorClient) DeleteDatabase(ctx context.Context, clusterIndex int, bdbID int) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionDeleteDatabase,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"cluster_index\": clusterIndex,\n\t\t\t\"bdb_id\":        bdbID,\n\t\t},\n\t})\n}\n\n// CreateDatabase creates a new database\n// Parameters:\n//   - clusterIndex: The index of the cluster\n//   - databaseConfig: The database configuration\nfunc (c *FaultInjectorClient) CreateDatabase(ctx context.Context, clusterIndex int, databaseConfig DatabaseConfig) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionCreateDatabase,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"cluster_index\":   clusterIndex,\n\t\t\t\"database_config\": databaseConfig,\n\t\t},\n\t})\n}\n\n// CreateDatabaseFromMap creates a new database using a map for configuration\n// This is useful when you want to pass a raw configuration map\n// Parameters:\n//   - clusterIndex: The index of the cluster\n//   - databaseConfig: The database configuration as a map\nfunc (c *FaultInjectorClient) CreateDatabaseFromMap(ctx context.Context, clusterIndex int, databaseConfig map[string]interface{}) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionCreateDatabase,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"cluster_index\":   clusterIndex,\n\t\t\t\"database_config\": databaseConfig,\n\t\t},\n\t})\n}\n\n// isPortUnavailableError checks if the error is a port unavailable error\nfunc isPortUnavailableError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\terrStr := err.Error()\n\treturn strings.Contains(errStr, \"port_unavailable\") || strings.Contains(errStr, \"Unavailable or invalid port\")\n}\n\n// CreateDatabaseWithPortRetry creates a new database, retrying with incremented port numbers if the port is unavailable\n// Parameters:\n//   - clusterIndex: The index of the cluster\n//   - databaseConfig: The database configuration as a map (must contain \"port\" key)\n//   - maxRetries: Maximum number of port increments to try (default 100 if <= 0)\n//\n// Returns the ActionResponse and the final port used\nfunc (c *FaultInjectorClient) CreateDatabaseWithPortRetry(ctx context.Context, clusterIndex int, databaseConfig map[string]interface{}, maxRetries int) (*ActionResponse, int, error) {\n\tif maxRetries <= 0 {\n\t\tmaxRetries = 100\n\t}\n\n\t// Get the initial port from the config\n\tport, ok := databaseConfig[\"port\"]\n\tif !ok {\n\t\t// No port specified, just try once\n\t\tresp, err := c.CreateDatabaseFromMap(ctx, clusterIndex, databaseConfig)\n\t\treturn resp, 0, err\n\t}\n\n\t// Convert port to int\n\tvar currentPort int\n\tswitch p := port.(type) {\n\tcase int:\n\t\tcurrentPort = p\n\tcase int64:\n\t\tcurrentPort = int(p)\n\tcase float64:\n\t\tcurrentPort = int(p)\n\tdefault:\n\t\t// Can't handle this port type, just try once\n\t\tresp, err := c.CreateDatabaseFromMap(ctx, clusterIndex, databaseConfig)\n\t\treturn resp, 0, err\n\t}\n\n\t// Try creating the database, incrementing port on failure\n\tfor attempt := 0; attempt < maxRetries; attempt++ {\n\t\t// Update the port in the config\n\t\tdatabaseConfig[\"port\"] = currentPort\n\n\t\tresp, err := c.CreateDatabaseFromMap(ctx, clusterIndex, databaseConfig)\n\t\tif err == nil {\n\t\t\treturn resp, currentPort, nil\n\t\t}\n\n\t\t// Check if it's a port unavailable error\n\t\tif !isPortUnavailableError(err) {\n\t\t\t// Different error, don't retry\n\t\t\treturn nil, currentPort, err\n\t\t}\n\n\t\t// Port unavailable, try next port\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[FI] Port %d unavailable, trying port %d\\n\", currentPort, currentPort+1)\n\t\t}\n\t\tcurrentPort++\n\t}\n\n\treturn nil, currentPort, fmt.Errorf(\"failed to create database after %d port retries (last port tried: %d)\", maxRetries, currentPort-1)\n}\n\n// CreateDatabaseConfigWithPortRetry creates a new database using DatabaseConfig, retrying with incremented port numbers if the port is unavailable\n// Parameters:\n//   - clusterIndex: The index of the cluster\n//   - databaseConfig: The database configuration\n//   - maxRetries: Maximum number of port increments to try (default 100 if <= 0)\n//\n// Returns the ActionResponse and the final port used\nfunc (c *FaultInjectorClient) CreateDatabaseConfigWithPortRetry(ctx context.Context, clusterIndex int, databaseConfig DatabaseConfig, maxRetries int) (*ActionResponse, int, error) {\n\tif maxRetries <= 0 {\n\t\tmaxRetries = 100\n\t}\n\n\tcurrentPort := databaseConfig.Port\n\n\t// Try creating the database, incrementing port on failure\n\tfor attempt := 0; attempt < maxRetries; attempt++ {\n\t\t// Update the port in the config\n\t\tdatabaseConfig.Port = currentPort\n\n\t\tresp, err := c.CreateDatabase(ctx, clusterIndex, databaseConfig)\n\t\tif err == nil {\n\t\t\treturn resp, currentPort, nil\n\t\t}\n\n\t\t// Check if it's a port unavailable error\n\t\tif !isPortUnavailableError(err) {\n\t\t\t// Different error, don't retry\n\t\t\treturn nil, currentPort, err\n\t\t}\n\n\t\t// Port unavailable, try next port\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[FI] Port %d unavailable, trying port %d\\n\", currentPort, currentPort+1)\n\t\t}\n\t\tcurrentPort++\n\t}\n\n\treturn nil, currentPort, fmt.Errorf(\"failed to create database after %d port retries (last port tried: %d)\", maxRetries, currentPort-1)\n}\n\n// Complex Actions\n\n// ExecuteSequence executes a sequence of actions\nfunc (c *FaultInjectorClient) ExecuteSequence(ctx context.Context, actions []SequenceAction) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionSequence,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"actions\": actions,\n\t\t},\n\t})\n}\n\n// ExecuteCommand executes a custom command\nfunc (c *FaultInjectorClient) ExecuteCommand(ctx context.Context, nodeID, command string) (*ActionResponse, error) {\n\treturn c.TriggerAction(ctx, ActionRequest{\n\t\tType: ActionExecuteCommand,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"node_id\": nodeID,\n\t\t\t\"command\": command,\n\t\t},\n\t})\n}\n\n// Convenience Methods\n\n// SimulateClusterUpgrade simulates a complete cluster upgrade scenario\nfunc (c *FaultInjectorClient) SimulateClusterUpgrade(ctx context.Context, nodes []string) (*ActionResponse, error) {\n\tactions := make([]SequenceAction, 0, len(nodes)*2)\n\n\t// Rolling restart of all nodes\n\tfor i, nodeID := range nodes {\n\t\tactions = append(actions, SequenceAction{\n\t\t\tType: ActionNodeRestart,\n\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\"node_id\":  nodeID,\n\t\t\t\t\"graceful\": true,\n\t\t\t},\n\t\t\tDelay: time.Duration(i*10) * time.Second, // Stagger restarts\n\t\t})\n\t}\n\n\treturn c.ExecuteSequence(ctx, actions)\n}\n\n// SimulateNetworkIssues simulates various network issues\nfunc (c *FaultInjectorClient) SimulateNetworkIssues(ctx context.Context, nodes []string) (*ActionResponse, error) {\n\tactions := []SequenceAction{\n\t\t{\n\t\t\tType: ActionNetworkLatency,\n\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\"nodes\":   nodes,\n\t\t\t\t\"latency\": \"100ms\",\n\t\t\t\t\"jitter\":  \"20ms\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tType: ActionNetworkPacketLoss,\n\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\"nodes\":        nodes,\n\t\t\t\t\"loss_percent\": 2.0,\n\t\t\t},\n\t\t\tDelay: 30 * time.Second,\n\t\t},\n\t\t{\n\t\t\tType: ActionNetworkRestore,\n\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\"nodes\": nodes,\n\t\t\t},\n\t\t\tDelay: 60 * time.Second,\n\t\t},\n\t}\n\n\treturn c.ExecuteSequence(ctx, actions)\n}\n\n// Helper types and functions\n\ntype waitConfig struct {\n\tpollInterval time.Duration\n\tmaxWaitTime  time.Duration\n}\n\ntype WaitOption func(*waitConfig)\n\n// WithPollInterval sets the polling interval for waiting\nfunc WithPollInterval(interval time.Duration) WaitOption {\n\treturn func(c *waitConfig) {\n\t\tc.pollInterval = interval\n\t}\n}\n\n// WithMaxWaitTime sets the maximum wait time\nfunc WithMaxWaitTime(maxWait time.Duration) WaitOption {\n\treturn func(c *waitConfig) {\n\t\tc.maxWaitTime = maxWait\n\t}\n}\n\n// Internal HTTP request method\nfunc (c *FaultInjectorClient) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {\n\turl := c.baseURL + path\n\n\tvar reqBody io.Reader\n\tif body != nil {\n\t\tjsonData, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal request body: %w\", err)\n\t\t}\n\t\treqBody = bytes.NewReader(jsonData)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, url, reqBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tif body != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to execute request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read response body: %w\", err)\n\t}\n\n\tif resp.StatusCode >= 400 {\n\t\treturn fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tif result != nil {\n\t\tif err := json.Unmarshal(respBody, result); err != nil {\n\t\t\t// happens when the API changes and the response structure changes\n\t\t\t// sometimes the output of the action status is map, sometimes it is json.\n\t\t\t// since we don't have a proper response structure we are going to handle it here\n\t\t\tif result, ok := result.(*ActionStatusResponse); ok {\n\t\t\t\tmapResult := map[string]interface{}{}\n\t\t\t\terr = json.Unmarshal(respBody, &mapResult)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Println(\"Failed to unmarshal response:\", string(respBody))\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t\tresult.Error = mapResult[\"error\"]\n\t\t\t\tresult.Output = map[string]interface{}{\"result\": mapResult[\"output\"]}\n\t\t\t\tif status, ok := mapResult[\"status\"].(string); ok {\n\t\t\t\t\tresult.Status = ActionStatus(status)\n\t\t\t\t}\n\t\t\t\tif result.Status == StatusSuccess || result.Status == StatusFailed || result.Status == StatusCancelled {\n\t\t\t\t\tresult.EndTime = time.Now()\n\t\t\t\t}\n\t\t\t\tif progress, ok := mapResult[\"progress\"].(float64); ok {\n\t\t\t\t\tresult.Progress = progress\n\t\t\t\t}\n\t\t\t\tif actionID, ok := mapResult[\"action_id\"].(string); ok {\n\t\t\t\t\tresult.ActionID = actionID\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tfmt.Println(\"Failed to unmarshal response:\", string(respBody))\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Utility functions for common scenarios\n\n// GetClusterNodes returns a list of cluster node IDs\nfunc GetClusterNodes() []string {\n\t// TODO Implement\n\t// This would typically be configured via environment or discovery\n\treturn []string{\"node-1\", \"node-2\", \"node-3\", \"node-4\", \"node-5\", \"node-6\"}\n}\n\n// GetMasterNodes returns a list of master node IDs\nfunc GetMasterNodes() []string {\n\t// TODO Implement\n\treturn []string{\"node-1\", \"node-2\", \"node-3\"}\n}\n\n// GetSlaveNodes returns a list of slave node IDs\nfunc GetSlaveNodes() []string {\n\t// TODO Implement\n\treturn []string{\"node-4\", \"node-5\", \"node-6\"}\n}\n\n// ParseNodeID extracts node ID from various formats\nfunc ParseNodeID(nodeAddr string) string {\n\t// Extract node ID from address like \"redis-node-1:7001\" -> \"node-1\"\n\tparts := strings.Split(nodeAddr, \":\")\n\tif len(parts) > 0 {\n\t\taddr := parts[0]\n\t\tif strings.Contains(addr, \"redis-\") {\n\t\t\treturn strings.TrimPrefix(addr, \"redis-\")\n\t\t}\n\t\treturn addr\n\t}\n\treturn nodeAddr\n}\n\n// FormatSlotRange formats a slot range for Redis commands\nfunc FormatSlotRange(start, end int) string {\n\tif start == end {\n\t\treturn strconv.Itoa(start)\n\t}\n\treturn fmt.Sprintf(\"%d-%d\", start, end)\n}\n\n// DebugE2E returns true if E2E_DEBUG environment variable is set to \"true\"\n// Use this to control verbose debug logging in e2e tests\nfunc DebugE2E() bool {\n\treturn os.Getenv(\"E2E_DEBUG\") == \"true\"\n}\n\n// formatSMigratingNotification formats an SMIGRATING notification in RESP3 wire format\nfunc formatSMigratingNotification(seqID int64, slots ...string) string {\n\t// Format: [\"SMIGRATING\", seqID, slot1, slot2, ...]\n\tparts := []string{\n\t\tfmt.Sprintf(\">%d\\r\\n\", len(slots)+2),\n\t\t\"$10\\r\\nSMIGRATING\\r\\n\",\n\t\tfmt.Sprintf(\":%d\\r\\n\", seqID),\n\t}\n\n\tfor _, slot := range slots {\n\t\tparts = append(parts, fmt.Sprintf(\"$%d\\r\\n%s\\r\\n\", len(slot), slot))\n\t}\n\n\treturn strings.Join(parts, \"\")\n}\n\n// formatSMigratedNotification formats an SMIGRATED notification in RESP3 wire format\nfunc formatSMigratedNotification(seqID int64, endpoints ...string) string {\n\t// Correct Format: [\"SMIGRATED\", SeqID, [[host:port, slots], [host:port, slots], ...]]\n\t// RESP3 wire format:\n\t//   >3\n\t//   +SMIGRATED\n\t//   :SeqID\n\t//   *<num_entries>\n\t//     *2\n\t//       +<host:port>\n\t//       +<slots-or-ranges>\n\t// Each endpoint is formatted as: \"host:port slot1,slot2,range1-range2\"\n\tparts := []string{\">3\\r\\n\"}\n\tparts = append(parts, \"+SMIGRATED\\r\\n\")\n\tparts = append(parts, fmt.Sprintf(\":%d\\r\\n\", seqID))\n\n\tcount := len(endpoints)\n\tparts = append(parts, fmt.Sprintf(\"*%d\\r\\n\", count))\n\n\tfor _, endpoint := range endpoints {\n\t\t// Split endpoint into host:port and slots\n\t\t// endpoint format: \"host:port slot1,slot2,range1-range2\"\n\t\tendpointParts := strings.SplitN(endpoint, \" \", 2)\n\t\tif len(endpointParts) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\thostPort := endpointParts[0]\n\t\tslots := endpointParts[1]\n\n\t\t// Each entry is an array with 2 elements\n\t\tparts = append(parts, \"*2\\r\\n\")\n\t\tparts = append(parts, fmt.Sprintf(\"+%s\\r\\n\", hostPort))\n\t\tparts = append(parts, fmt.Sprintf(\"+%s\\r\\n\", slots))\n\t}\n\n\treturn strings.Join(parts, \"\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/logcollector_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\tlogs2 \"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n)\n\n// logs is a slice of strings that provides additional functionality\n// for filtering and analysis\ntype logs []string\n\nfunc (l logs) Contains(searchString string) bool {\n\tfor _, log := range l {\n\t\tif log == searchString {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (l logs) GetCount() int {\n\treturn len(l)\n}\n\nfunc (l logs) GetCountThatContain(searchString string) int {\n\tcount := 0\n\tfor _, log := range l {\n\t\tif strings.Contains(log, searchString) {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\nfunc (l logs) GetLogsFiltered(filter func(string) bool) []string {\n\tfilteredLogs := make([]string, 0, len(l))\n\tfor _, log := range l {\n\t\tif filter(log) {\n\t\t\tfilteredLogs = append(filteredLogs, log)\n\t\t}\n\t}\n\treturn filteredLogs\n}\n\nfunc (l logs) GetTimedOutLogs() logs {\n\treturn l.GetLogsFiltered(isTimeout)\n}\n\nfunc (l logs) GetLogsPerConn(connID uint64) logs {\n\treturn l.GetLogsFiltered(func(log string) bool {\n\t\treturn strings.Contains(log, fmt.Sprintf(\"conn[%d]\", connID))\n\t})\n}\n\nfunc (l logs) GetAnalysis() *LogAnalisis {\n\treturn NewLogAnalysis(l)\n}\n\n// TestLogCollector is a simple logger that captures logs for analysis\n// It is thread safe and can be used to capture logs from multiple clients\n// It uses type logs to provide additional functionality like filtering\n// and analysis\ntype TestLogCollector struct {\n\tl               logs\n\tdoPrint         bool\n\tmatchFuncs      []*MatchFunc\n\tmatchFuncsMutex sync.Mutex\n\tmu              sync.Mutex\n}\n\nfunc (tlc *TestLogCollector) DontPrint() {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\ttlc.doPrint = false\n}\n\nfunc (tlc *TestLogCollector) DoPrint() {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\ttlc.l = make([]string, 0)\n\ttlc.doPrint = true\n}\n\n// MatchFunc is a slice of functions that check the logs for a specific condition\n// use in WaitForLogMatchFunc\ntype MatchFunc struct {\n\tcompleted atomic.Bool\n\tF         func(lstring string) bool\n\tmatches   []string\n\tmatchesMu sync.Mutex    // protects matches slice\n\tfound     chan struct{} // channel to notify when match is found, will be closed\n\tdone      func()\n}\n\nfunc (tlc *TestLogCollector) Printf(_ context.Context, format string, v ...interface{}) {\n\ttlc.mu.Lock()\n\tlstr := fmt.Sprintf(format, v...)\n\n\t// Check if there are match functions to process\n\t// Use matchFuncsMutex to safely read matchFuncs\n\ttlc.matchFuncsMutex.Lock()\n\thasMatchFuncs := len(tlc.matchFuncs) > 0\n\t// Create a copy of matchFuncs to avoid holding the lock while processing\n\tmatchFuncsCopy := make([]*MatchFunc, len(tlc.matchFuncs))\n\tcopy(matchFuncsCopy, tlc.matchFuncs)\n\ttlc.matchFuncsMutex.Unlock()\n\n\tif hasMatchFuncs {\n\t\tgo func(lstr string) {\n\t\t\tfor _, matchFunc := range matchFuncsCopy {\n\t\t\t\tif matchFunc.F(lstr) {\n\t\t\t\t\tmatchFunc.matchesMu.Lock()\n\t\t\t\t\tmatchFunc.matches = append(matchFunc.matches, lstr)\n\t\t\t\t\tmatchFunc.matchesMu.Unlock()\n\t\t\t\t\tmatchFunc.done()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}(lstr)\n\t}\n\tif tlc.doPrint {\n\t\tfmt.Println(lstr)\n\t}\n\ttlc.l = append(tlc.l, fmt.Sprintf(format, v...))\n\ttlc.mu.Unlock()\n}\n\nfunc (tlc *TestLogCollector) WaitForLogContaining(searchString string, timeout time.Duration) bool {\n\ttimeoutCh := time.After(timeout)\n\tticker := time.NewTicker(100 * time.Millisecond)\n\tfor {\n\t\tselect {\n\t\tcase <-timeoutCh:\n\t\t\treturn false\n\t\tcase <-ticker.C:\n\t\t\tif tlc.Contains(searchString) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (tlc *TestLogCollector) MatchOrWaitForLogMatchFunc(mf func(string) bool, timeout time.Duration) (string, bool) {\n\tif DebugE2E() {\n\t\tfmt.Printf(\"[LOG-COLLECTOR] MatchOrWaitForLogMatchFunc: checking existing logs...\\n\")\n\t}\n\tif logs := tlc.GetLogsFiltered(mf); len(logs) > 0 {\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] MatchOrWaitForLogMatchFunc: found matching log in existing logs\\n\")\n\t\t}\n\t\treturn logs[0], true\n\t}\n\tif DebugE2E() {\n\t\tfmt.Printf(\"[LOG-COLLECTOR] MatchOrWaitForLogMatchFunc: no match in existing logs, waiting with timeout %v...\\n\", timeout)\n\t}\n\treturn tlc.WaitForLogMatchFunc(mf, timeout)\n}\n\nfunc (tlc *TestLogCollector) WaitForLogMatchFunc(mf func(string) bool, timeout time.Duration) (string, bool) {\n\tif DebugE2E() {\n\t\tfmt.Printf(\"[LOG-COLLECTOR] WaitForLogMatchFunc: starting with timeout %v\\n\", timeout)\n\t}\n\tmatchFunc := &MatchFunc{\n\t\tcompleted: atomic.Bool{},\n\t\tF:         mf,\n\t\tfound:     make(chan struct{}),\n\t\tmatches:   make([]string, 0),\n\t}\n\tmatchFunc.done = func() {\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] done(): starting\\n\")\n\t\t}\n\t\tif !matchFunc.completed.CompareAndSwap(false, true) {\n\t\t\tif DebugE2E() {\n\t\t\t\tfmt.Printf(\"[LOG-COLLECTOR] done(): already completed, returning\\n\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] done(): closing found channel\\n\")\n\t\t}\n\t\tclose(matchFunc.found)\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] done(): acquiring matchFuncsMutex...\\n\")\n\t\t}\n\t\ttlc.matchFuncsMutex.Lock()\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] done(): matchFuncsMutex acquired\\n\")\n\t\t}\n\t\tdefer tlc.matchFuncsMutex.Unlock()\n\t\tfor i, mf := range tlc.matchFuncs {\n\t\t\tif mf == matchFunc {\n\t\t\t\ttlc.matchFuncs = append(tlc.matchFuncs[:i], tlc.matchFuncs[i+1:]...)\n\t\t\t\tif DebugE2E() {\n\t\t\t\t\tfmt.Printf(\"[LOG-COLLECTOR] done(): removed matchFunc from list\\n\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] done(): matchFunc not found in list\\n\")\n\t\t}\n\t}\n\n\ttlc.matchFuncsMutex.Lock()\n\ttlc.matchFuncs = append(tlc.matchFuncs, matchFunc)\n\ttlc.matchFuncsMutex.Unlock()\n\tif DebugE2E() {\n\t\tfmt.Printf(\"[LOG-COLLECTOR] WaitForLogMatchFunc: registered match func, waiting...\\n\")\n\t}\n\n\tselect {\n\tcase <-matchFunc.found:\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] WaitForLogMatchFunc: match found! Acquiring matchesMu...\\n\")\n\t\t}\n\t\tmatchFunc.matchesMu.Lock()\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] WaitForLogMatchFunc: matchesMu acquired, matches len=%d\\n\", len(matchFunc.matches))\n\t\t}\n\t\tdefer matchFunc.matchesMu.Unlock()\n\t\tif len(matchFunc.matches) > 0 {\n\t\t\tif DebugE2E() {\n\t\t\t\tfmt.Printf(\"[LOG-COLLECTOR] WaitForLogMatchFunc: returning match\\n\")\n\t\t\t}\n\t\t\treturn matchFunc.matches[0], true\n\t\t}\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] WaitForLogMatchFunc: match found but no log message, this should not happen\\n\")\n\t\t}\n\t\treturn \"\", false\n\tcase <-time.After(timeout):\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[LOG-COLLECTOR] WaitForLogMatchFunc: TIMEOUT after %v\\n\", timeout)\n\t\t}\n\t\t// Clean up the matchFunc from the list on timeout\n\t\tmatchFunc.done()\n\t\treturn \"\", false\n\t}\n}\n\nfunc (tlc *TestLogCollector) GetLogs() logs {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\treturn tlc.l\n}\n\nfunc (tlc *TestLogCollector) DumpLogs() {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\tfmt.Println(\"Dumping logs:\")\n\tfmt.Println(\"===================================================\")\n\tfor _, log := range tlc.l {\n\t\tfmt.Println(log)\n\t}\n}\n\nfunc (tlc *TestLogCollector) ClearLogs() {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\ttlc.l = make([]string, 0)\n}\n\nfunc (tlc *TestLogCollector) Contains(searchString string) bool {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\treturn tlc.l.Contains(searchString)\n}\n\nfunc (tlc *TestLogCollector) MatchContainsAll(searchStrings []string) []string {\n\t// match a log that contains all\n\treturn tlc.GetLogsFiltered(func(log string) bool {\n\t\tfor _, searchString := range searchStrings {\n\t\t\tif !strings.Contains(log, searchString) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t})\n}\n\nfunc (tlc *TestLogCollector) GetLogCount() int {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\treturn tlc.l.GetCount()\n}\n\nfunc (tlc *TestLogCollector) GetLogCountThatContain(searchString string) int {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\treturn tlc.l.GetCountThatContain(searchString)\n}\n\nfunc (tlc *TestLogCollector) GetLogsFiltered(filter func(string) bool) logs {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\treturn tlc.l.GetLogsFiltered(filter)\n}\n\nfunc (tlc *TestLogCollector) GetTimedOutLogs() []string {\n\treturn tlc.GetLogsFiltered(isTimeout)\n}\n\nfunc (tlc *TestLogCollector) GetLogsPerConn(connID uint64) logs {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\treturn tlc.l.GetLogsPerConn(connID)\n}\n\nfunc (tlc *TestLogCollector) GetAnalysisForConn(connID uint64) *LogAnalisis {\n\treturn NewLogAnalysis(tlc.GetLogsPerConn(connID))\n}\n\nfunc NewTestLogCollector() *TestLogCollector {\n\treturn &TestLogCollector{\n\t\tl: make([]string, 0),\n\t}\n}\n\nfunc (tlc *TestLogCollector) GetAnalysis() *LogAnalisis {\n\treturn NewLogAnalysis(tlc.GetLogs())\n}\n\nfunc (tlc *TestLogCollector) Clear() {\n\ttlc.mu.Lock()\n\tdefer tlc.mu.Unlock()\n\ttlc.matchFuncs = make([]*MatchFunc, 0)\n\ttlc.l = make([]string, 0)\n}\n\n// LogAnalisis provides analysis of logs captured by TestLogCollector\ntype LogAnalisis struct {\n\tlogs                    []string\n\tTimeoutErrorsCount      int64\n\tRelaxedTimeoutCount     int64\n\tRelaxedPostHandoffCount int64\n\tUnrelaxedTimeoutCount   int64\n\tUnrelaxedAfterMoving    int64\n\tConnectionCount         int64\n\tconnLogs                map[uint64][]string\n\tconnIds                 map[uint64]bool\n\n\tTotalNotifications int64\n\tMovingCount        int64\n\tMigratingCount     int64\n\tMigratedCount      int64\n\tSMigratingCount    int64\n\tSMigratedCount     int64\n\tFailingOverCount   int64\n\tFailedOverCount    int64\n\tUnexpectedCount    int64\n\n\tTotalHandoffCount             int64\n\tFailedHandoffCount            int64\n\tSucceededHandoffCount         int64\n\tTotalHandoffRetries           int64\n\tTotalHandoffToCurrentEndpoint int64\n\n\t// Cluster state reload tracking\n\tClusterStateReloadCount int64\n}\n\nfunc NewLogAnalysis(logs []string) *LogAnalisis {\n\tla := &LogAnalisis{\n\t\tlogs:     logs,\n\t\tconnLogs: make(map[uint64][]string),\n\t\tconnIds:  make(map[uint64]bool),\n\t}\n\tla.Analyze()\n\treturn la\n}\n\nfunc (la *LogAnalisis) Analyze() {\n\thasMoving := false\n\tfor _, log := range la.logs {\n\t\tif isTimeout(log) {\n\t\t\tla.TimeoutErrorsCount++\n\t\t}\n\t\tif strings.Contains(log, \"MOVING\") {\n\t\t\thasMoving = true\n\t\t}\n\t\tif strings.Contains(log, logs2.RelaxedTimeoutDueToNotificationMessage) {\n\t\t\tla.RelaxedTimeoutCount++\n\t\t}\n\t\tif strings.Contains(log, logs2.ApplyingRelaxedTimeoutDueToPostHandoffMessage) {\n\t\t\tla.RelaxedTimeoutCount++\n\t\t\tla.RelaxedPostHandoffCount++\n\t\t}\n\t\tif strings.Contains(log, logs2.UnrelaxedTimeoutMessage) {\n\t\t\tla.UnrelaxedTimeoutCount++\n\t\t}\n\t\tif strings.Contains(log, logs2.UnrelaxedTimeoutAfterDeadlineMessage) {\n\t\t\tif hasMoving {\n\t\t\t\tla.UnrelaxedAfterMoving++\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Unrelaxed after deadline but no MOVING: %s\\n\", log)\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(log, logs2.ProcessingNotificationMessage) {\n\t\t\tla.TotalNotifications++\n\n\t\t\tswitch {\n\t\t\t// ordering here is important, since SMIGRATED contains MIGRATED and SMIGRATING contains MIGRATING\n\t\t\tcase notificationType(log, \"SMIGRATING\"):\n\t\t\t\tla.SMigratingCount++\n\t\t\tcase notificationType(log, \"SMIGRATED\"):\n\t\t\t\tla.SMigratedCount++\n\t\t\tcase notificationType(log, \"MOVING\"):\n\t\t\t\tla.MovingCount++\n\t\t\tcase notificationType(log, \"MIGRATING\"):\n\t\t\t\tla.MigratingCount++\n\t\t\tcase notificationType(log, \"MIGRATED\"):\n\t\t\t\tla.MigratedCount++\n\t\t\tcase notificationType(log, \"FAILING_OVER\"):\n\t\t\t\tla.FailingOverCount++\n\t\t\tcase notificationType(log, \"FAILED_OVER\"):\n\t\t\t\tla.FailedOverCount++\n\t\t\tdefault:\n\t\t\t\tfmt.Printf(\"[ERROR] Unexpected notification: %s\\n\", log)\n\t\t\t\tla.UnexpectedCount++\n\t\t\t}\n\t\t}\n\n\t\t// Track cluster state reloads (deduplicated, once per seqID)\n\t\t// Log format: \"triggering cluster state reload seqID=%d host:port=%s slots=%v\"\n\t\tif strings.Contains(log, logs2.TriggeringClusterStateReloadMessage) {\n\t\t\tla.ClusterStateReloadCount++\n\t\t}\n\n\t\tif strings.Contains(log, \"conn[\") {\n\t\t\tconnID := extractConnID(log)\n\t\t\tif _, ok := la.connIds[connID]; !ok {\n\t\t\t\tla.connIds[connID] = true\n\t\t\t\tla.ConnectionCount++\n\t\t\t}\n\t\t\tla.connLogs[connID] = append(la.connLogs[connID], log)\n\t\t}\n\n\t\tif strings.Contains(log, logs2.SchedulingHandoffToCurrentEndpointMessage) {\n\t\t\tla.TotalHandoffToCurrentEndpoint++\n\t\t}\n\n\t\tif strings.Contains(log, logs2.HandoffSuccessMessage) {\n\t\t\tla.SucceededHandoffCount++\n\t\t}\n\t\tif strings.Contains(log, logs2.HandoffFailedMessage) {\n\t\t\tla.FailedHandoffCount++\n\t\t}\n\t\tif strings.Contains(log, logs2.HandoffStartedMessage) {\n\t\t\tla.TotalHandoffCount++\n\t\t}\n\t\tif strings.Contains(log, logs2.HandoffRetryAttemptMessage) {\n\t\t\tla.TotalHandoffRetries++\n\t\t}\n\t}\n}\n\n// PrintSummary prints a compact summary without detailed information\nfunc (la *LogAnalisis) PrintSummary(t *testing.T) {\n\tt.Logf(\"Log Summary: %d logs, %d connections | Handoffs: %d (ok:%d fail:%d) | Notifications: %d | TimeoutErrors: %d\",\n\t\tlen(la.logs), len(la.connIds),\n\t\tla.TotalHandoffCount, la.SucceededHandoffCount, la.FailedHandoffCount,\n\t\tla.TotalNotifications, la.TimeoutErrorsCount)\n}\n\nfunc (la *LogAnalisis) Print(t *testing.T) {\n\tt.Logf(\"Log Analysis results for %d logs and %d connections:\", len(la.logs), len(la.connIds))\n\tt.Logf(\"Connection Count: %d\", la.ConnectionCount)\n\tt.Logf(\"-------------\")\n\tt.Logf(\"-Timeout Analysis-\")\n\tt.Logf(\"-------------\")\n\tt.Logf(\"Timeout Errors: %d\", la.TimeoutErrorsCount)\n\tt.Logf(\"Relaxed Timeout Count: %d\", la.RelaxedTimeoutCount)\n\tt.Logf(\" - Relaxed Timeout After Post-Handoff: %d\", la.RelaxedPostHandoffCount)\n\tt.Logf(\"Unrelaxed Timeout Count: %d\", la.UnrelaxedTimeoutCount)\n\tt.Logf(\" - Unrelaxed Timeout After Moving: %d\", la.UnrelaxedAfterMoving)\n\tt.Logf(\"-------------\")\n\tt.Logf(\"-Handoff Analysis-\")\n\tt.Logf(\"-------------\")\n\tt.Logf(\"Total Handoffs: %d\", la.TotalHandoffCount)\n\tt.Logf(\" - Succeeded: %d\", la.SucceededHandoffCount)\n\tt.Logf(\" - Failed: %d\", la.FailedHandoffCount)\n\tt.Logf(\" - Retries: %d\", la.TotalHandoffRetries)\n\tt.Logf(\" - Handoffs to current endpoint: %d\", la.TotalHandoffToCurrentEndpoint)\n\tt.Logf(\"-------------\")\n\tt.Logf(\"-Notification Analysis-\")\n\tt.Logf(\"-------------\")\n\tt.Logf(\"Total Notifications: %d\", la.TotalNotifications)\n\tt.Logf(\" - MOVING: %d\", la.MovingCount)\n\tt.Logf(\" - MIGRATING: %d\", la.MigratingCount)\n\tt.Logf(\" - MIGRATED: %d\", la.MigratedCount)\n\tt.Logf(\" - FAILING_OVER: %d\", la.FailingOverCount)\n\tt.Logf(\" - FAILED_OVER: %d\", la.FailedOverCount)\n\tt.Logf(\" - Unexpected: %d\", la.UnexpectedCount)\n\tt.Logf(\"-------------\")\n\tt.Logf(\"-Cluster-Specific Notification Analysis-\")\n\tt.Logf(\"-------------\")\n\tt.Logf(\" - SMIGRATING: %d\", la.SMigratingCount)\n\tt.Logf(\" - SMIGRATED: %d\", la.SMigratedCount)\n\tt.Logf(\" - Cluster state reloads: %d\", la.ClusterStateReloadCount)\n\tt.Logf(\"-------------\")\n\tt.Logf(\"Log Analysis completed successfully\")\n}\n\nfunc extractConnID(log string) uint64 {\n\tlogParts := strings.Split(log, \"conn[\")\n\tif len(logParts) < 2 {\n\t\treturn 0\n\t}\n\tconnIDStr := strings.Split(logParts[1], \"]\")[0]\n\tconnID, err := strconv.ParseUint(connIDStr, 10, 64)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn connID\n}\n\nfunc notificationType(log string, nt string) bool {\n\treturn strings.Contains(log, nt)\n}\n\n// notificationTypeWithSeqID matches logs that contain the notification type AND have seqID\n// This matches \"processing notification started\" logs which include seqID in the JSON\nfunc notificationTypeWithSeqID(log string, nt string) bool {\n\treturn strings.Contains(log, nt) && strings.Contains(log, \"seqID\")\n}\nfunc connID(log string, connID uint64) bool {\n\treturn strings.Contains(log, fmt.Sprintf(\"conn[%d]\", connID))\n}\nfunc seqID(log string, seqID int64) bool {\n\treturn strings.Contains(log, fmt.Sprintf(\"seqID[%d]\", seqID))\n}\n"
  },
  {
    "path": "maintnotifications/e2e/main_test.go",
    "content": "package e2e\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/logging\"\n)\n\n// Global log collector\nvar logCollector *TestLogCollector\n\nconst defaultTestTimeout = 30 * time.Minute\n\n// Global fault injector client\nvar faultInjector *FaultInjectorClient\nvar faultInjectorCleanup func()\n\nfunc TestMain(m *testing.M) {\n\tvar err error\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tlog.Println(\"Skipping scenario tests, E2E_SCENARIO_TESTS is not set\")\n\t\treturn\n\t}\n\n\t// Only create fault injector if we're not using the unified injector\n\t// The unified injector tests use NewNotificationInjector() which auto-detects the mode\n\t// and doesn't require the global faultInjector variable\n\t// We can detect unified injector mode by checking if REDIS_ENDPOINTS_CONFIG_PATH is NOT set\n\t// (which means we're using the proxy mock mode)\n\tif os.Getenv(\"REDIS_ENDPOINTS_CONFIG_PATH\") != \"\" {\n\t\t// Real fault injector mode - create the global fault injector\n\t\tfaultInjector, faultInjectorCleanup, err = CreateTestFaultInjectorWithCleanup()\n\t\tif err != nil {\n\t\t\tpanic(\"Failed to create fault injector: \" + err.Error())\n\t\t}\n\t\tdefer faultInjectorCleanup()\n\t} else {\n\t\tlog.Println(\"Using proxy mock mode - skipping global fault injector setup\")\n\t}\n\n\t// use log collector to capture logs from redis clients\n\tlogCollector = NewTestLogCollector()\n\tredis.SetLogger(logCollector)\n\tredis.SetLogLevel(logging.LogLevelDebug)\n\n\tlogCollector.Clear()\n\tdefer logCollector.Clear()\n\tlog.Println(\"Running scenario tests...\")\n\tstatus := m.Run()\n\tos.Exit(status)\n}\n"
  },
  {
    "path": "maintnotifications/e2e/notification_injector_test.go",
    "content": "package e2e\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n)\n\n// NotificationInjector is an interface that can inject maintenance notifications\n// It can be implemented by either a real fault injector or a proxy-based mock\ntype NotificationInjector interface {\n\t// InjectSMIGRATING injects an SMIGRATING notification\n\tInjectSMIGRATING(ctx context.Context, seqID int64, slots ...string) error\n\n\t// InjectSMIGRATED injects an SMIGRATED notification\n\tInjectSMIGRATED(ctx context.Context, seqID int64, hostPort string, slots ...string) error\n\n\t// InjectMOVING injects a MOVING notification (for standalone)\n\t// Format: [\"MOVING\", seqID, timeS, endpoint]\n\tInjectMOVING(ctx context.Context, seqID int64, timeS int64, endpoint string) error\n\n\t// InjectMIGRATING injects a MIGRATING notification (for standalone)\n\tInjectMIGRATING(ctx context.Context, seqID int64, slot int) error\n\n\t// InjectMIGRATED injects a MIGRATED notification (for standalone)\n\tInjectMIGRATED(ctx context.Context, seqID int64, slot int) error\n\n\t// InjectFAILING_OVER injects a FAILING_OVER notification\n\tInjectFAILING_OVER(ctx context.Context, seqID int64) error\n\n\t// InjectFAILED_OVER injects a FAILED_OVER notification\n\tInjectFAILED_OVER(ctx context.Context, seqID int64) error\n\n\t// Start starts the injector (if needed)\n\tStart() error\n\n\t// Stop stops the injector (if needed)\n\tStop() error\n\n\t// GetClusterAddrs returns the cluster addresses to connect to\n\tGetClusterAddrs() []string\n\n\t// IsReal returns true if this is a real fault injector (not a mock)\n\tIsReal() bool\n\n\t// GetTestModeConfig returns the test mode configuration for this injector\n\tGetTestModeConfig() *TestModeConfig\n}\n\n// NewNotificationInjector creates a notification injector based on environment\n// If FAULT_INJECTOR_URL is set, it uses the real fault injector\n// Otherwise, it uses the proxy-based mock\nfunc NewNotificationInjector() (NotificationInjector, error) {\n\tif faultInjectorURL := os.Getenv(\"FAULT_INJECTOR_URL\"); faultInjectorURL != \"\" {\n\t\t// Use real fault injector\n\t\treturn NewFaultInjectorNotificationInjector(faultInjectorURL), nil\n\t}\n\n\t// Use proxy-based mock\n\tapiPort := 18100 // Default port (updated to avoid macOS Control Center conflict)\n\tif portStr := os.Getenv(\"PROXY_API_PORT\"); portStr != \"\" {\n\t\t_, _ = fmt.Sscanf(portStr, \"%d\", &apiPort)\n\t}\n\n\treturn NewProxyNotificationInjector(apiPort), nil\n}\n\n// ProxyNotificationInjector implements NotificationInjector using cae-resp-proxy\ntype ProxyNotificationInjector struct {\n\tapiPort      int\n\tapiBaseURL   string\n\tcmd          *exec.Cmd\n\thttpClient   *http.Client\n\tnodes        []proxyNode\n\tvisibleNodes []int // Indices of nodes visible in CLUSTER SLOTS (for migration simulation)\n}\n\ntype proxyNode struct {\n\tlistenPort int\n\ttargetHost string\n\ttargetPort int\n\tproxyAddr  string\n\tnodeID     string\n}\n\n// NewProxyNotificationInjector creates a new proxy-based notification injector\nfunc NewProxyNotificationInjector(apiPort int) *ProxyNotificationInjector {\n\treturn &ProxyNotificationInjector{\n\t\tapiPort:    apiPort,\n\t\tapiBaseURL: fmt.Sprintf(\"http://localhost:%d\", apiPort),\n\t\thttpClient: &http.Client{\n\t\t\tTimeout: 5 * time.Second,\n\t\t},\n\t\tnodes: make([]proxyNode, 0),\n\t}\n}\n\nfunc (p *ProxyNotificationInjector) Start() error {\n\t// Get cluster configuration from environment\n\tclusterAddrs := os.Getenv(\"CLUSTER_ADDRS\")\n\tif clusterAddrs == \"\" {\n\t\t// Start with 4 nodes: 17000, 17001, 17002, 17003\n\t\t// Initially, CLUSTER SLOTS will only expose 17000, 17001, 17002\n\t\t// Node 17003 will be \"hidden\" until SMIGRATED swaps it in for 17002\n\t\tclusterAddrs = \"127.0.0.1:17000,127.0.0.1:17001,127.0.0.1:17002,127.0.0.1:17003\" // Use 127.0.0.1 to force IPv4\n\t}\n\n\ttargetHost := os.Getenv(\"REDIS_TARGET_HOST\")\n\tif targetHost == \"\" {\n\t\ttargetHost = \"127.0.0.1\" // Use 127.0.0.1 to force IPv4\n\t}\n\n\ttargetPort := 6379\n\tif portStr := os.Getenv(\"REDIS_TARGET_PORT\"); portStr != \"\" {\n\t\t_, _ = fmt.Sscanf(portStr, \"%d\", &targetPort)\n\t}\n\n\t// Parse cluster addresses\n\taddrs := strings.Split(clusterAddrs, \",\")\n\tif len(addrs) == 0 {\n\t\treturn fmt.Errorf(\"no cluster addresses specified\")\n\t}\n\n\t// Extract first port for initial node\n\tvar initialPort int\n\t_, _ = fmt.Sscanf(strings.Split(addrs[0], \":\")[1], \"%d\", &initialPort)\n\n\t// Check if proxy is already running (e.g., in Docker)\n\tproxyAlreadyRunning := false\n\tresp, err := p.httpClient.Get(p.apiBaseURL + \"/stats\")\n\tif err == nil && resp.StatusCode == 200 {\n\t\tresp.Body.Close()\n\t\tproxyAlreadyRunning = true\n\t\tfmt.Printf(\"✓ Detected existing proxy at %s (e.g., Docker container)\\n\", p.apiBaseURL)\n\t}\n\n\t// Only start proxy if not already running\n\tif !proxyAlreadyRunning {\n\t\t// Start proxy with initial node\n\t\tp.cmd = exec.Command(\"cae-resp-proxy\",\n\t\t\t\"--api-port\", fmt.Sprintf(\"%d\", p.apiPort),\n\t\t\t\"--listen-port\", fmt.Sprintf(\"%d\", initialPort),\n\t\t\t\"--target-host\", targetHost,\n\t\t\t\"--target-port\", fmt.Sprintf(\"%d\", targetPort),\n\t\t)\n\n\t\tif err := p.cmd.Start(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to start proxy: %w\", err)\n\t\t}\n\n\t\t// Wait for proxy to be ready\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}\n\n\tfor i := 0; i < 10; i++ {\n\t\tresp, err := p.httpClient.Get(p.apiBaseURL + \"/stats\")\n\t\tif err == nil {\n\t\t\tresp.Body.Close()\n\t\t\tif resp.StatusCode == 200 {\n\t\t\t\t// Add all nodes from the cluster addresses\n\t\t\t\t// For Docker proxy, all nodes are already running\n\t\t\t\t// For local proxy, we'll add them via API\n\t\t\t\tfor i, addr := range addrs {\n\t\t\t\t\tvar port int\n\t\t\t\t\t_, _ = fmt.Sscanf(strings.Split(addr, \":\")[1], \"%d\", &port)\n\n\t\t\t\t\tif i == 0 {\n\t\t\t\t\t\t// Add initial node directly\n\t\t\t\t\t\tp.nodes = append(p.nodes, proxyNode{\n\t\t\t\t\t\t\tlistenPort: port,\n\t\t\t\t\t\t\ttargetHost: targetHost,\n\t\t\t\t\t\t\ttargetPort: targetPort,\n\t\t\t\t\t\t\tproxyAddr:  fmt.Sprintf(\"127.0.0.1:%d\", port),\n\t\t\t\t\t\t\tnodeID:     fmt.Sprintf(\"127.0.0.1:%d:%d\", port, targetPort),\n\t\t\t\t\t\t})\n\t\t\t\t\t} else if !proxyAlreadyRunning {\n\t\t\t\t\t\t// Add remaining nodes via API (only if we started the proxy ourselves)\n\t\t\t\t\t\tif err := p.addNode(port, targetPort, targetHost); err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to add node %d: %w\", i, err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Docker proxy: nodes are already running, just add to our list\n\t\t\t\t\t\tp.nodes = append(p.nodes, proxyNode{\n\t\t\t\t\t\t\tlistenPort: port,\n\t\t\t\t\t\t\ttargetHost: targetHost,\n\t\t\t\t\t\t\ttargetPort: targetPort,\n\t\t\t\t\t\t\tproxyAddr:  fmt.Sprintf(\"127.0.0.1:%d\", port),\n\t\t\t\t\t\t\tnodeID:     fmt.Sprintf(\"127.0.0.1:%d:%d\", port, targetPort),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Initially, make only the first 3 nodes visible in CLUSTER SLOTS\n\t\t\t\t// The 4th node (index 3) will be hidden until SMIGRATED swaps it in\n\t\t\t\tif len(p.nodes) >= 4 {\n\t\t\t\t\tp.visibleNodes = []int{0, 1, 2} // Nodes 17000, 17001, 17002\n\t\t\t\t} else {\n\t\t\t\t\t// If we have fewer than 4 nodes, make all visible\n\t\t\t\t\tp.visibleNodes = make([]int, len(p.nodes))\n\t\t\t\t\tfor i := range p.visibleNodes {\n\t\t\t\t\t\tp.visibleNodes[i] = i\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Add cluster command interceptors to make standalone Redis appear as a cluster\n\t\t\t\tif err := p.setupClusterInterceptors(); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to setup cluster interceptors: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(200 * time.Millisecond)\n\t}\n\n\treturn fmt.Errorf(\"proxy did not become ready\")\n}\n\nfunc (p *ProxyNotificationInjector) addNode(listenPort, targetPort int, targetHost string) error {\n\tnodeConfig := map[string]interface{}{\n\t\t\"listenPort\": listenPort,\n\t\t\"targetHost\": targetHost,\n\t\t\"targetPort\": targetPort,\n\t}\n\n\tjsonData, err := json.Marshal(nodeConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal node config: %w\", err)\n\t}\n\n\tresp, err := p.httpClient.Post(\n\t\tp.apiBaseURL+\"/nodes\",\n\t\t\"application/json\",\n\t\tbytes.NewReader(jsonData),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add node: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"failed to add node, status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tp.nodes = append(p.nodes, proxyNode{\n\t\tlistenPort: listenPort,\n\t\ttargetHost: targetHost,\n\t\ttargetPort: targetPort,\n\t\tproxyAddr:  fmt.Sprintf(\"localhost:%d\", listenPort),\n\t\tnodeID:     fmt.Sprintf(\"localhost:%d:%d\", listenPort, targetPort),\n\t})\n\n\ttime.Sleep(200 * time.Millisecond)\n\treturn nil\n}\n\nfunc (p *ProxyNotificationInjector) buildClusterSlotsResponse() string {\n\t// Build CLUSTER SLOTS response dynamically based on VISIBLE nodes only\n\t// Format: Array of slot ranges, each containing:\n\t//   - start slot (integer)\n\t//   - end slot (integer)\n\t//   - master node array: [host, port]\n\t//   - replica arrays (optional)\n\n\t// For simplicity, divide slots equally among visible nodes\n\ttotalSlots := 16384\n\tvisibleCount := len(p.visibleNodes)\n\tif visibleCount == 0 {\n\t\tvisibleCount = len(p.nodes)\n\t}\n\tslotsPerNode := totalSlots / visibleCount\n\n\tresponse := fmt.Sprintf(\"*%d\\r\\n\", visibleCount) // Number of slot ranges\n\n\tfor i, nodeIdx := range p.visibleNodes {\n\t\tif nodeIdx >= len(p.nodes) {\n\t\t\tcontinue\n\t\t}\n\t\tnode := p.nodes[nodeIdx]\n\n\t\tstartSlot := i * slotsPerNode\n\t\tendSlot := startSlot + slotsPerNode - 1\n\t\tif i == visibleCount-1 {\n\t\t\tendSlot = 16383 // Last node gets remaining slots\n\t\t}\n\n\t\t// Extract host and port from proxyAddr\n\t\thost, portStr, _ := strings.Cut(node.proxyAddr, \":\")\n\n\t\tresponse += \"*3\\r\\n\" // 3 elements: start, end, master\n\t\tresponse += fmt.Sprintf(\":%d\\r\\n\", startSlot)\n\t\tresponse += fmt.Sprintf(\":%d\\r\\n\", endSlot)\n\t\tresponse += \"*2\\r\\n\" // master info: 2 elements (host, port)\n\t\tresponse += fmt.Sprintf(\"$%d\\r\\n%s\\r\\n\", len(host), host)\n\t\tresponse += fmt.Sprintf(\":%s\\r\\n\", portStr)\n\t}\n\n\treturn response\n}\n\nfunc (p *ProxyNotificationInjector) addClusterSlotsInterceptor() error {\n\tclusterSlotsResponse := p.buildClusterSlotsResponse()\n\n\tinterceptor := map[string]interface{}{\n\t\t\"name\":     \"cluster-slots\",\n\t\t\"match\":    \"*2\\r\\n$7\\r\\nCLUSTER\\r\\n$5\\r\\nSLOTS\\r\\n\",\n\t\t\"response\": clusterSlotsResponse,\n\t\t\"encoding\": \"raw\",\n\t}\n\n\tjsonData, err := json.Marshal(interceptor)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal interceptor: %w\", err)\n\t}\n\n\tresp, err := p.httpClient.Post(\n\t\tp.apiBaseURL+\"/interceptors\",\n\t\t\"application/json\",\n\t\tbytes.NewReader(jsonData),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add interceptor: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"failed to add interceptor, status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\treturn nil\n}\n\nfunc (p *ProxyNotificationInjector) updateClusterSlotsInterceptor() error {\n\t// The proxy doesn't support updating existing interceptors\n\t// As a workaround, we'll send the updated CLUSTER SLOTS response directly to all clients\n\t// This simulates what would happen when the client calls CLUSTER SLOTS after SMIGRATED\n\n\tclusterSlotsResponse := p.buildClusterSlotsResponse()\n\n\t// Send the updated CLUSTER SLOTS response to all connected clients\n\t// This uses the /send-to-all-clients endpoint\n\tpayload := map[string]interface{}{\n\t\t\"data\": clusterSlotsResponse,\n\t}\n\n\tjsonData, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal payload: %w\", err)\n\t}\n\n\tresp, err := p.httpClient.Post(\n\t\tp.apiBaseURL+\"/send-to-all-clients\",\n\t\t\"application/json\",\n\t\tbytes.NewReader(jsonData),\n\t)\n\tif err != nil {\n\t\t// If this fails, it's not critical - the client will get the updated\n\t\t// response when it calls CLUSTER SLOTS after receiving SMIGRATED\n\t\treturn nil\n\t}\n\tdefer resp.Body.Close()\n\n\treturn nil\n}\n\nfunc (p *ProxyNotificationInjector) setupClusterInterceptors() error {\n\t// Add CLUSTER SLOTS interceptor\n\tif err := p.addClusterSlotsInterceptor(); err != nil {\n\t\treturn err\n\t}\n\n\t// Interceptors to add\n\tinterceptors := []map[string]interface{}{\n\t\t{\n\t\t\t\"name\":     \"client-maint-notifications-on-with-endpoint\",\n\t\t\t\"match\":    \"*5\\r\\n$6\\r\\nclient\\r\\n$19\\r\\nmaint_notifications\\r\\n$2\\r\\non\\r\\n$21\\r\\nmoving-endpoint-type\\r\\n$4\\r\\nnone\\r\\n\",\n\t\t\t\"response\": \"+OK\\r\\n\",\n\t\t\t\"encoding\": \"raw\",\n\t\t},\n\t\t{\n\t\t\t\"name\":     \"client-maint-notifications-off\",\n\t\t\t\"match\":    \"*3\\r\\n$6\\r\\nclient\\r\\n$19\\r\\nmaint_notifications\\r\\n$3\\r\\noff\\r\\n\",\n\t\t\t\"response\": \"+OK\\r\\n\",\n\t\t\t\"encoding\": \"raw\",\n\t\t},\n\t}\n\n\t// Add all interceptors\n\tfor _, interceptor := range interceptors {\n\t\tjsonData, err := json.Marshal(interceptor)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal interceptor %s: %w\", interceptor[\"name\"], err)\n\t\t}\n\n\t\tresp, err := p.httpClient.Post(\n\t\t\tp.apiBaseURL+\"/interceptors\",\n\t\t\t\"application/json\",\n\t\t\tbytes.NewReader(jsonData),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to add interceptor %s: %w\", interceptor[\"name\"], err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\treturn fmt.Errorf(\"failed to add interceptor %s, status %d: %s\", interceptor[\"name\"], resp.StatusCode, string(body))\n\t\t}\n\n\t\tfmt.Printf(\"✓ Added %s interceptor to proxy\\n\", interceptor[\"name\"])\n\t}\n\n\treturn nil\n}\n\nfunc (p *ProxyNotificationInjector) Stop() error {\n\tif p.cmd != nil && p.cmd.Process != nil {\n\t\treturn p.cmd.Process.Kill()\n\t}\n\treturn nil\n}\n\nfunc (p *ProxyNotificationInjector) GetClusterAddrs() []string {\n\taddrs := make([]string, len(p.nodes))\n\tfor i, node := range p.nodes {\n\t\taddrs[i] = node.proxyAddr\n\t}\n\treturn addrs\n}\n\nfunc (p *ProxyNotificationInjector) IsReal() bool {\n\treturn false\n}\n\nfunc (p *ProxyNotificationInjector) GetTestModeConfig() *TestModeConfig {\n\treturn &TestModeConfig{\n\t\tMode:                     TestModeProxyMock,\n\t\tNotificationDelay:        1 * time.Second,\n\t\tActionWaitTimeout:        10 * time.Second,\n\t\tActionPollInterval:       500 * time.Millisecond,\n\t\tDatabaseReadyDelay:       1 * time.Second,\n\t\tConnectionEstablishDelay: 500 * time.Millisecond,\n\t\tMaxClients:               1,\n\t\tSkipMultiClientTests:     true,\n\t}\n}\n\nfunc (p *ProxyNotificationInjector) InjectSMIGRATING(ctx context.Context, seqID int64, slots ...string) error {\n\tnotification := formatSMigratingNotification(seqID, slots...)\n\treturn p.injectNotification(notification)\n}\n\nfunc (p *ProxyNotificationInjector) InjectSMIGRATED(ctx context.Context, seqID int64, hostPort string, slots ...string) error {\n\t// Simulate topology change by swapping visible nodes\n\t// If we have 4 nodes and currently showing [0,1,2], swap to [0,1,3]\n\t// This simulates node 2 (17002) being replaced by node 3 (17003)\n\tif len(p.nodes) >= 4 && len(p.visibleNodes) == 3 {\n\t\t// Check if the hostPort matches node 3 (the \"new\" node)\n\t\tif hostPort == p.nodes[3].proxyAddr {\n\t\t\t// Swap node 2 for node 3 in visible nodes\n\t\t\tp.visibleNodes = []int{0, 1, 3}\n\n\t\t\t// Update CLUSTER SLOTS interceptor to reflect new topology\n\t\t\tif err := p.updateClusterSlotsInterceptor(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to update CLUSTER SLOTS after migration: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Format endpoint as \"host:port slot1,slot2,range1-range2\"\n\tendpoint := fmt.Sprintf(\"%s %s\", hostPort, strings.Join(slots, \",\"))\n\tnotification := formatSMigratedNotification(seqID, endpoint)\n\treturn p.injectNotification(notification)\n}\n\nfunc (p *ProxyNotificationInjector) InjectMOVING(ctx context.Context, seqID int64, timeS int64, endpoint string) error {\n\tnotification := formatMovingNotification(seqID, timeS, endpoint)\n\treturn p.injectNotification(notification)\n}\n\nfunc (p *ProxyNotificationInjector) InjectMIGRATING(ctx context.Context, seqID int64, slot int) error {\n\tnotification := formatMigratingNotification(seqID, slot)\n\treturn p.injectNotification(notification)\n}\n\nfunc (p *ProxyNotificationInjector) InjectMIGRATED(ctx context.Context, seqID int64, slot int) error {\n\tnotification := formatMigratedNotification(seqID, slot)\n\treturn p.injectNotification(notification)\n}\n\nfunc (p *ProxyNotificationInjector) InjectFAILING_OVER(ctx context.Context, seqID int64) error {\n\tnotification := formatFailingOverNotification(seqID)\n\treturn p.injectNotification(notification)\n}\n\nfunc (p *ProxyNotificationInjector) InjectFAILED_OVER(ctx context.Context, seqID int64) error {\n\tnotification := formatFailedOverNotification(seqID)\n\treturn p.injectNotification(notification)\n}\n\nfunc (p *ProxyNotificationInjector) injectNotification(notification string) error {\n\turl := p.apiBaseURL + \"/send-to-all-clients?encoding=raw\"\n\tresp, err := p.httpClient.Post(url, \"application/octet-stream\", strings.NewReader(notification))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to inject notification: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"injection failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\treturn nil\n}\n\n// Helper functions to format notifications\n\nfunc formatMovingNotification(seqID int64, timeS int64, endpoint string) string {\n\t// Format: [\"MOVING\", seqID, timeS, endpoint]\n\tif endpoint == \"\" {\n\t\t// 3 elements: MOVING, seqID, timeS\n\t\treturn fmt.Sprintf(\">3\\r\\n$6\\r\\nMOVING\\r\\n:%d\\r\\n:%d\\r\\n\", seqID, timeS)\n\t}\n\t// 4 elements: MOVING, seqID, timeS, endpoint\n\treturn fmt.Sprintf(\">4\\r\\n$6\\r\\nMOVING\\r\\n:%d\\r\\n:%d\\r\\n$%d\\r\\n%s\\r\\n\", seqID, timeS, len(endpoint), endpoint)\n}\n\nfunc formatMigratingNotification(seqID int64, slot int) string {\n\tslotStr := fmt.Sprintf(\"%d\", slot)\n\treturn fmt.Sprintf(\">3\\r\\n$9\\r\\nMIGRATING\\r\\n:%d\\r\\n$%d\\r\\n%s\\r\\n\", seqID, len(slotStr), slotStr)\n}\n\nfunc formatMigratedNotification(seqID int64, slot int) string {\n\tslotStr := fmt.Sprintf(\"%d\", slot)\n\treturn fmt.Sprintf(\">3\\r\\n$8\\r\\nMIGRATED\\r\\n:%d\\r\\n$%d\\r\\n%s\\r\\n\", seqID, len(slotStr), slotStr)\n}\n\nfunc formatFailingOverNotification(seqID int64) string {\n\t// Format: [\"FAILING_OVER\", seqID]\n\treturn fmt.Sprintf(\">2\\r\\n$12\\r\\nFAILING_OVER\\r\\n:%d\\r\\n\", seqID)\n}\n\nfunc formatFailedOverNotification(seqID int64) string {\n\t// Format: [\"FAILED_OVER\", seqID]\n\treturn fmt.Sprintf(\">2\\r\\n$11\\r\\nFAILED_OVER\\r\\n:%d\\r\\n\", seqID)\n}\n\n// FaultInjectorNotificationInjector implements NotificationInjector using the real fault injector\ntype FaultInjectorNotificationInjector struct {\n\tclient       *FaultInjectorClient\n\tclusterAddrs []string\n\tbdbID        int\n}\n\n// NewFaultInjectorNotificationInjector creates a new fault injector-based notification injector\nfunc NewFaultInjectorNotificationInjector(baseURL string) *FaultInjectorNotificationInjector {\n\t// Get cluster addresses from environment\n\tclusterAddrs := os.Getenv(\"CLUSTER_ADDRS\")\n\tif clusterAddrs == \"\" {\n\t\tclusterAddrs = \"localhost:6379\"\n\t}\n\n\tbdbID := 1\n\tif bdbIDStr := os.Getenv(\"BDB_ID\"); bdbIDStr != \"\" {\n\t\t_, _ = fmt.Sscanf(bdbIDStr, \"%d\", &bdbID)\n\t}\n\n\treturn &FaultInjectorNotificationInjector{\n\t\tclient:       NewFaultInjectorClient(baseURL),\n\t\tclusterAddrs: strings.Split(clusterAddrs, \",\"),\n\t\tbdbID:        bdbID,\n\t}\n}\n\nfunc (f *FaultInjectorNotificationInjector) Start() error {\n\t// Fault injector is already running, nothing to start\n\treturn nil\n}\n\nfunc (f *FaultInjectorNotificationInjector) Stop() error {\n\t// Fault injector keeps running, nothing to stop\n\treturn nil\n}\n\nfunc (f *FaultInjectorNotificationInjector) GetClusterAddrs() []string {\n\treturn f.clusterAddrs\n}\n\nfunc (f *FaultInjectorNotificationInjector) IsReal() bool {\n\treturn true\n}\n\nfunc (f *FaultInjectorNotificationInjector) GetTestModeConfig() *TestModeConfig {\n\treturn &TestModeConfig{\n\t\tMode:                     TestModeRealFaultInjector,\n\t\tNotificationDelay:        30 * time.Second,\n\t\tActionWaitTimeout:        5 * time.Minute, // Real fault injector can take up to 5 minutes\n\t\tActionPollInterval:       500 * time.Millisecond,\n\t\tDatabaseReadyDelay:       10 * time.Second,\n\t\tConnectionEstablishDelay: 2 * time.Second,\n\t\tMaxClients:               3,\n\t\tSkipMultiClientTests:     false,\n\t}\n}\n\nfunc (f *FaultInjectorNotificationInjector) InjectSMIGRATING(ctx context.Context, seqID int64, slots ...string) error {\n\t// For real fault injector, we trigger actual slot migration which will generate SMIGRATING\n\t// Parse slot ranges\n\tvar startSlot, endSlot int\n\tif len(slots) > 0 {\n\t\tif strings.Contains(slots[0], \"-\") {\n\t\t\t_, _ = fmt.Sscanf(slots[0], \"%d-%d\", &startSlot, &endSlot)\n\t\t} else {\n\t\t\t_, _ = fmt.Sscanf(slots[0], \"%d\", &startSlot)\n\t\t\tendSlot = startSlot\n\t\t}\n\t}\n\n\t// Trigger slot migration (this will generate SMIGRATING notification)\n\tresp, err := f.client.TriggerSlotMigration(ctx, startSlot, endSlot, \"node-1\", \"node-2\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to trigger slot migration: %w\", err)\n\t}\n\n\t// Wait for action to start\n\t_, err = f.client.WaitForAction(ctx, resp.ActionID, WithMaxWaitTime(10*time.Second))\n\treturn err\n}\n\nfunc (f *FaultInjectorNotificationInjector) InjectSMIGRATED(ctx context.Context, seqID int64, hostPort string, slots ...string) error {\n\t// SMIGRATED is generated automatically when migration completes\n\t// We can't directly inject it with the real fault injector\n\t// This is a limitation of using the real fault injector\n\treturn fmt.Errorf(\"SMIGRATED cannot be directly injected with real fault injector - it's generated when migration completes\")\n}\n\nfunc (f *FaultInjectorNotificationInjector) InjectMOVING(ctx context.Context, seqID int64, timeS int64, endpoint string) error {\n\t// MOVING notifications are generated during bind action\n\treturn fmt.Errorf(\"MOVING cannot be directly injected with real fault injector - it's generated during bind action\")\n}\n\nfunc (f *FaultInjectorNotificationInjector) InjectMIGRATING(ctx context.Context, seqID int64, slot int) error {\n\t// Trigger slot migration for standalone\n\tresp, err := f.client.TriggerSlotMigration(ctx, slot, slot, \"node-1\", \"node-2\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to trigger slot migration: %w\", err)\n\t}\n\n\t_, err = f.client.WaitForAction(ctx, resp.ActionID, WithMaxWaitTime(10*time.Second))\n\treturn err\n}\n\nfunc (f *FaultInjectorNotificationInjector) InjectMIGRATED(ctx context.Context, seqID int64, slot int) error {\n\t// MIGRATED is generated automatically when migration completes\n\treturn fmt.Errorf(\"MIGRATED cannot be directly injected with real fault injector - it's generated when migration completes\")\n}\n\nfunc (f *FaultInjectorNotificationInjector) InjectFAILING_OVER(ctx context.Context, seqID int64) error {\n\t// FAILING_OVER is generated automatically when failover starts\n\treturn fmt.Errorf(\"FAILING_OVER cannot be directly injected with real fault injector - it's generated when failover starts\")\n}\n\nfunc (f *FaultInjectorNotificationInjector) InjectFAILED_OVER(ctx context.Context, seqID int64) error {\n\t// FAILED_OVER is generated automatically when failover completes\n\treturn fmt.Errorf(\"FAILED_OVER cannot be directly injected with real fault injector - it's generated when failover completes\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/notiftracker_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// DiagnosticsEvent represents a notification event\n// it may be a push notification or an error when processing\n// push notifications\ntype DiagnosticsEvent struct {\n\t// is this pre or post hook\n\tType   string `json:\"type\"`\n\tConnID uint64 `json:\"connID\"`\n\tSeqID  int64  `json:\"seqID\"`\n\n\t// ShardAddr is the address of the shard that received this notification\n\t// Only populated when using NewTrackingNotificationsHookWithShard\n\tShardAddr string `json:\"shardAddr,omitempty\"`\n\n\tError error `json:\"error\"`\n\n\tPre       bool                   `json:\"pre\"`\n\tTimestamp time.Time              `json:\"timestamp\"`\n\tDetails   map[string]interface{} `json:\"details\"`\n}\n\n// TrackingNotificationsHook is a notification hook that tracks notifications\ntype TrackingNotificationsHook struct {\n\t// unique connection count\n\tconnectionCount atomic.Int64\n\n\t// timeouts\n\trelaxedTimeoutCount   atomic.Int64\n\tunrelaxedTimeoutCount atomic.Int64\n\n\tnotificationProcessingErrors atomic.Int64\n\n\t// notification types\n\ttotalNotifications          atomic.Int64\n\tmigratingCount              atomic.Int64\n\tmigratedCount               atomic.Int64\n\tsMigratingCount             atomic.Int64\n\tsMigratedCount              atomic.Int64\n\tfailingOverCount            atomic.Int64\n\tfailedOverCount             atomic.Int64\n\tmovingCount                 atomic.Int64\n\tunexpectedNotificationCount atomic.Int64\n\n\t// cluster state reload tracking (actual reloads, not just SMIGRATED notifications)\n\tclusterStateReloadCount atomic.Int64\n\n\tdiagnosticsLog []DiagnosticsEvent\n\tconnIds        map[uint64]bool\n\tconnLogs       map[uint64][]DiagnosticsEvent\n\tmutex          sync.RWMutex\n\n\t// shard identifier\n\tshardAddr string\n\t// track last notification time for waiting\n\tlastNotificationTime atomic.Value // stores time.Time\n}\n\n// NewTrackingNotificationsHook creates a new notification hook with counters\nfunc NewTrackingNotificationsHook() *TrackingNotificationsHook {\n\thook := &TrackingNotificationsHook{\n\t\tdiagnosticsLog: make([]DiagnosticsEvent, 0),\n\t\tconnIds:        make(map[uint64]bool),\n\t\tconnLogs:       make(map[uint64][]DiagnosticsEvent),\n\t}\n\thook.lastNotificationTime.Store(time.Time{})\n\treturn hook\n}\n\n// NewTrackingNotificationsHookWithShard creates a hook with shard identifier\nfunc NewTrackingNotificationsHookWithShard(shardAddr string) *TrackingNotificationsHook {\n\thook := &TrackingNotificationsHook{\n\t\tdiagnosticsLog: make([]DiagnosticsEvent, 0),\n\t\tconnIds:        make(map[uint64]bool),\n\t\tconnLogs:       make(map[uint64][]DiagnosticsEvent),\n\t\tshardAddr:      shardAddr,\n\t}\n\thook.lastNotificationTime.Store(time.Time{})\n\treturn hook\n}\n\n// SetShardAddr sets the shard address\nfunc (tnh *TrackingNotificationsHook) SetShardAddr(addr string) {\n\ttnh.shardAddr = addr\n}\n\n// GetLastNotificationTime returns the time of the last notification received\nfunc (tnh *TrackingNotificationsHook) GetLastNotificationTime() time.Time {\n\treturn tnh.lastNotificationTime.Load().(time.Time)\n}\n\n// it is not reusable, but just to keep it consistent\n// with the log collector\nfunc (tnh *TrackingNotificationsHook) Clear() {\n\ttnh.mutex.Lock()\n\tdefer tnh.mutex.Unlock()\n\ttnh.diagnosticsLog = make([]DiagnosticsEvent, 0)\n\ttnh.connIds = make(map[uint64]bool)\n\ttnh.connLogs = make(map[uint64][]DiagnosticsEvent)\n\ttnh.relaxedTimeoutCount.Store(0)\n\ttnh.unrelaxedTimeoutCount.Store(0)\n\ttnh.notificationProcessingErrors.Store(0)\n\ttnh.totalNotifications.Store(0)\n\ttnh.migratingCount.Store(0)\n\ttnh.migratedCount.Store(0)\n\ttnh.sMigratingCount.Store(0)\n\ttnh.sMigratedCount.Store(0)\n\ttnh.failingOverCount.Store(0)\n\ttnh.failedOverCount.Store(0)\n\ttnh.movingCount.Store(0)\n\ttnh.unexpectedNotificationCount.Store(0)\n\ttnh.connectionCount.Store(0)\n\ttnh.clusterStateReloadCount.Store(0)\n}\n\n// wait for notification in prehook\nfunc (tnh *TrackingNotificationsHook) FindOrWaitForNotification(notificationType string, timeout time.Duration) (notification []interface{}, found bool) {\n\tif notification, found := tnh.FindNotification(notificationType); found {\n\t\treturn notification, true\n\t}\n\n\t// wait for notification\n\ttimeoutCh := time.After(timeout)\n\tticker := time.NewTicker(100 * time.Millisecond)\n\tfor {\n\t\tselect {\n\t\tcase <-timeoutCh:\n\t\t\treturn nil, false\n\t\tcase <-ticker.C:\n\t\t\tif notification, found := tnh.FindNotification(notificationType); found {\n\t\t\t\treturn notification, true\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (tnh *TrackingNotificationsHook) FindNotification(notificationType string) (notification []interface{}, found bool) {\n\ttnh.mutex.RLock()\n\tdefer tnh.mutex.RUnlock()\n\tfor _, event := range tnh.diagnosticsLog {\n\t\tif event.Type == notificationType {\n\t\t\treturn event.Details[\"notification\"].([]interface{}), true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// PreHook captures timeout-related events before processing\nfunc (tnh *TrackingNotificationsHook) PreHook(_ context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) {\n\t// Update last notification time\n\ttnh.lastNotificationTime.Store(time.Now())\n\n\ttnh.increaseNotificationCount(notificationType)\n\ttnh.storeDiagnosticsEvent(notificationType, notification, notificationCtx)\n\ttnh.increaseRelaxedTimeoutCount(notificationType)\n\treturn notification, true\n}\n\nfunc (tnh *TrackingNotificationsHook) getConnID(notificationCtx push.NotificationHandlerContext) uint64 {\n\tif conn, ok := notificationCtx.Conn.(*pool.Conn); ok {\n\t\treturn conn.GetID()\n\t}\n\treturn 0\n}\n\nfunc (tnh *TrackingNotificationsHook) getSeqID(notification []interface{}) int64 {\n\tseqID, ok := notification[1].(int64)\n\tif !ok {\n\t\treturn 0\n\t}\n\treturn seqID\n}\n\n// PostHook captures the result after processing push notification\nfunc (tnh *TrackingNotificationsHook) PostHook(_ context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, err error) {\n\tif err != nil {\n\t\tevent := DiagnosticsEvent{\n\t\t\tType:      notificationType + \"_ERROR\",\n\t\t\tConnID:    tnh.getConnID(notificationCtx),\n\t\t\tSeqID:     tnh.getSeqID(notification),\n\t\t\tError:     err,\n\t\t\tTimestamp: time.Now(),\n\t\t\tDetails: map[string]interface{}{\n\t\t\t\t\"notification\": notification,\n\t\t\t\t\"context\":      \"post-hook\",\n\t\t\t},\n\t\t}\n\n\t\ttnh.notificationProcessingErrors.Add(1)\n\t\ttnh.mutex.Lock()\n\t\ttnh.diagnosticsLog = append(tnh.diagnosticsLog, event)\n\t\ttnh.mutex.Unlock()\n\t}\n}\n\nfunc (tnh *TrackingNotificationsHook) storeDiagnosticsEvent(notificationType string, notification []interface{}, notificationCtx push.NotificationHandlerContext) {\n\tconnID := tnh.getConnID(notificationCtx)\n\n\t// Get shard address - prefer explicit shardAddr, fall back to connection's remote address\n\tshardAddr := tnh.shardAddr\n\tif shardAddr == \"\" {\n\t\tshardAddr = tnh.getShardAddrFromContext(notificationCtx)\n\t}\n\n\tevent := DiagnosticsEvent{\n\t\tType:      notificationType,\n\t\tConnID:    connID,\n\t\tSeqID:     tnh.getSeqID(notification),\n\t\tShardAddr: shardAddr,\n\t\tPre:       true,\n\t\tTimestamp: time.Now(),\n\t\tDetails: map[string]interface{}{\n\t\t\t\"notification\": notification,\n\t\t\t\"context\":      \"pre-hook\",\n\t\t},\n\t}\n\n\ttnh.mutex.Lock()\n\tif v, ok := tnh.connIds[connID]; !ok || !v {\n\t\ttnh.connIds[connID] = true\n\t\ttnh.connectionCount.Add(1)\n\t}\n\ttnh.connLogs[connID] = append(tnh.connLogs[connID], event)\n\ttnh.diagnosticsLog = append(tnh.diagnosticsLog, event)\n\ttnh.mutex.Unlock()\n}\n\n// getShardAddrFromContext extracts the shard address from the notification context\nfunc (tnh *TrackingNotificationsHook) getShardAddrFromContext(notificationCtx push.NotificationHandlerContext) string {\n\tif conn, ok := notificationCtx.Conn.(*pool.Conn); ok {\n\t\tif remoteAddr := conn.RemoteAddr(); remoteAddr != nil {\n\t\t\treturn remoteAddr.String()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// GetRelaxedTimeoutCount returns the count of relaxed timeout events\nfunc (tnh *TrackingNotificationsHook) GetRelaxedTimeoutCount() int64 {\n\treturn tnh.relaxedTimeoutCount.Load()\n}\n\n// GetUnrelaxedTimeoutCount returns the count of unrelaxed timeout events\nfunc (tnh *TrackingNotificationsHook) GetUnrelaxedTimeoutCount() int64 {\n\treturn tnh.unrelaxedTimeoutCount.Load()\n}\n\n// GetNotificationProcessingErrors returns the count of timeout errors\nfunc (tnh *TrackingNotificationsHook) GetNotificationProcessingErrors() int64 {\n\treturn tnh.notificationProcessingErrors.Load()\n}\n\n// GetTotalNotifications returns the total number of notifications processed\nfunc (tnh *TrackingNotificationsHook) GetTotalNotifications() int64 {\n\treturn tnh.totalNotifications.Load()\n}\n\n// GetConnectionCount returns the current connection count\nfunc (tnh *TrackingNotificationsHook) GetConnectionCount() int64 {\n\treturn tnh.connectionCount.Load()\n}\n\n// GetMovingCount returns the count of MOVING notifications\nfunc (tnh *TrackingNotificationsHook) GetMovingCount() int64 {\n\treturn tnh.movingCount.Load()\n}\n\n// GetDiagnosticsLog returns a copy of the diagnostics log\nfunc (tnh *TrackingNotificationsHook) GetDiagnosticsLog() []DiagnosticsEvent {\n\ttnh.mutex.RLock()\n\tdefer tnh.mutex.RUnlock()\n\n\tlogCopy := make([]DiagnosticsEvent, len(tnh.diagnosticsLog))\n\tcopy(logCopy, tnh.diagnosticsLog)\n\treturn logCopy\n}\n\nfunc (tnh *TrackingNotificationsHook) increaseNotificationCount(notificationType string) {\n\ttnh.totalNotifications.Add(1)\n\tswitch notificationType {\n\tcase \"MOVING\":\n\t\ttnh.movingCount.Add(1)\n\tcase \"MIGRATING\":\n\t\ttnh.migratingCount.Add(1)\n\tcase \"MIGRATED\":\n\t\ttnh.migratedCount.Add(1)\n\tcase \"SMIGRATING\":\n\t\ttnh.sMigratingCount.Add(1)\n\tcase \"SMIGRATED\":\n\t\ttnh.sMigratedCount.Add(1)\n\tcase \"FAILING_OVER\":\n\t\ttnh.failingOverCount.Add(1)\n\tcase \"FAILED_OVER\":\n\t\ttnh.failedOverCount.Add(1)\n\tdefault:\n\t\ttnh.unexpectedNotificationCount.Add(1)\n\t}\n}\n\nfunc (tnh *TrackingNotificationsHook) increaseRelaxedTimeoutCount(notificationType string) {\n\tswitch notificationType {\n\tcase \"MIGRATING\", \"SMIGRATING\", \"FAILING_OVER\":\n\t\ttnh.relaxedTimeoutCount.Add(1)\n\tcase \"MIGRATED\", \"SMIGRATED\", \"FAILED_OVER\":\n\t\ttnh.unrelaxedTimeoutCount.Add(1)\n\t}\n}\n\nfunc (tnh *TrackingNotificationsHook) GetAnalysis() *DiagnosticsAnalysis {\n\treturn NewDiagnosticsAnalysis(tnh.GetDiagnosticsLog())\n}\n\nfunc (tnh *TrackingNotificationsHook) GetDiagnosticsLogForConn(connID uint64) []DiagnosticsEvent {\n\ttnh.mutex.RLock()\n\tdefer tnh.mutex.RUnlock()\n\n\tvar connLogs []DiagnosticsEvent\n\tfor _, log := range tnh.diagnosticsLog {\n\t\tif log.ConnID == connID {\n\t\t\tconnLogs = append(connLogs, log)\n\t\t}\n\t}\n\treturn connLogs\n}\n\nfunc (tnh *TrackingNotificationsHook) GetAnalysisForConn(connID uint64) *DiagnosticsAnalysis {\n\treturn NewDiagnosticsAnalysis(tnh.GetDiagnosticsLogForConn(connID))\n}\n\ntype DiagnosticsAnalysis struct {\n\tRelaxedTimeoutCount          int64\n\tUnrelaxedTimeoutCount        int64\n\tNotificationProcessingErrors int64\n\tConnectionCount              int64\n\tMovingCount                  int64\n\tMigratingCount               int64\n\tMigratedCount                int64\n\tSMigratingCount              int64\n\tSMigratedCount               int64\n\tFailingOverCount             int64\n\tFailedOverCount              int64\n\tUnexpectedNotificationCount  int64\n\tTotalNotifications           int64\n\n\tdiagnosticsLog []DiagnosticsEvent\n\tconnLogs       map[uint64][]DiagnosticsEvent\n\tconnIds        map[uint64]bool\n}\n\nfunc NewDiagnosticsAnalysis(diagnosticsLog []DiagnosticsEvent) *DiagnosticsAnalysis {\n\tda := &DiagnosticsAnalysis{\n\t\tdiagnosticsLog: diagnosticsLog,\n\t\tconnLogs:       make(map[uint64][]DiagnosticsEvent),\n\t\tconnIds:        make(map[uint64]bool),\n\t}\n\n\tda.Analyze()\n\treturn da\n}\n\nfunc (a *DiagnosticsAnalysis) Analyze() {\n\tfor _, log := range a.diagnosticsLog {\n\t\ta.TotalNotifications++\n\t\tswitch log.Type {\n\t\tcase \"MOVING\":\n\t\t\ta.MovingCount++\n\t\tcase \"MIGRATING\":\n\t\t\ta.MigratingCount++\n\t\tcase \"MIGRATED\":\n\t\t\ta.MigratedCount++\n\t\tcase \"SMIGRATING\":\n\t\t\ta.SMigratingCount++\n\t\tcase \"SMIGRATED\":\n\t\t\ta.SMigratedCount++\n\t\tcase \"FAILING_OVER\":\n\t\t\ta.FailingOverCount++\n\t\tcase \"FAILED_OVER\":\n\t\t\ta.FailedOverCount++\n\t\tdefault:\n\t\t\ta.UnexpectedNotificationCount++\n\t\t}\n\t\tif log.Error != nil {\n\t\t\tfmt.Printf(\"[ERROR] Notification processing error: %v\\n\", log.Error)\n\t\t\tfmt.Printf(\"[ERROR] Notification: %v\\n\", log.Details[\"notification\"])\n\t\t\tfmt.Printf(\"[ERROR] Context: %v\\n\", log.Details[\"context\"])\n\t\t\ta.NotificationProcessingErrors++\n\t\t}\n\t\tswitch log.Type {\n\t\tcase \"MIGRATING\", \"SMIGRATING\", \"FAILING_OVER\":\n\t\t\ta.RelaxedTimeoutCount++\n\t\tcase \"MIGRATED\", \"SMIGRATED\", \"FAILED_OVER\":\n\t\t\ta.UnrelaxedTimeoutCount++\n\t\t}\n\t\tif log.ConnID != 0 {\n\t\t\tif v, ok := a.connIds[log.ConnID]; !ok || !v {\n\t\t\t\ta.connIds[log.ConnID] = true\n\t\t\t\ta.connLogs[log.ConnID] = make([]DiagnosticsEvent, 0)\n\t\t\t\ta.ConnectionCount++\n\t\t\t}\n\t\t\ta.connLogs[log.ConnID] = append(a.connLogs[log.ConnID], log)\n\t\t}\n\n\t}\n}\n\n// PrintSummary prints a compact summary without detailed per-event information\nfunc (a *DiagnosticsAnalysis) PrintSummary(t *testing.T) {\n\tt.Logf(\"Notification Summary: %d events, %d connections | MOVING:%d MIGRATING:%d MIGRATED:%d FAILING_OVER:%d FAILED_OVER:%d SMIGRATING:%d SMIGRATED:%d | Errors:%d\",\n\t\tlen(a.diagnosticsLog), len(a.connIds),\n\t\ta.MovingCount, a.MigratingCount, a.MigratedCount,\n\t\ta.FailingOverCount, a.FailedOverCount,\n\t\ta.SMigratingCount, a.SMigratedCount,\n\t\ta.NotificationProcessingErrors)\n}\n\nfunc (a *DiagnosticsAnalysis) Print(t *testing.T) {\n\tt.Logf(\"Notification Analysis results for %d events and %d connections:\", len(a.diagnosticsLog), len(a.connIds))\n\tt.Logf(\"-------------\")\n\tt.Logf(\"-Timeout Analysis based on type of notification-\")\n\tt.Logf(\"Note: MIGRATED and FAILED_OVER notifications are not tracked by the hook, so they are not included in the relaxed/unrelaxed count\")\n\tt.Logf(\"Note: The hook only tracks timeouts that occur after the notification is processed, so timeouts that occur during processing are not included\")\n\tt.Logf(\"-------------\")\n\tt.Logf(\" - Relaxed Timeout Count: %d\", a.RelaxedTimeoutCount)\n\tt.Logf(\" - Unrelaxed Timeout Count: %d\", a.UnrelaxedTimeoutCount)\n\tt.Logf(\"-------------\")\n\tt.Logf(\"-Notification Analysis-\")\n\tt.Logf(\"-------------\")\n\tt.Logf(\" - MOVING: %d\", a.MovingCount)\n\tt.Logf(\" - MIGRATING: %d\", a.MigratingCount)\n\tt.Logf(\" - MIGRATED: %d\", a.MigratedCount)\n\tt.Logf(\" - FAILING_OVER: %d\", a.FailingOverCount)\n\tt.Logf(\" - FAILED_OVER: %d\", a.FailedOverCount)\n\tt.Logf(\" - Unexpected: %d\", a.UnexpectedNotificationCount)\n\tt.Logf(\"-------------\")\n\tt.Logf(\"- CLUSTER-SPECIFIC Notification Analysis-\")\n\tt.Logf(\"-------------\")\n\tt.Logf(\" - SMIGRATING: %d\", a.SMigratingCount)\n\tt.Logf(\" - SMIGRATED: %d\", a.SMigratedCount)\n\tt.Logf(\"-------------\")\n\tt.Logf(\" - Total Notifications: %d\", a.TotalNotifications)\n\tt.Logf(\" - Notification Processing Errors: %d\", a.NotificationProcessingErrors)\n\tt.Logf(\" - Connection Count: %d\", a.ConnectionCount)\n\tt.Logf(\"-------------\")\n\n\t// Print detailed notification events grouped by seqID and type\n\tt.Logf(\"-Detailed Notification Events (grouped by seqID)-\")\n\tt.Logf(\"-------------\")\n\n\t// Group events by (seqID, type)\n\ttype eventKey struct {\n\t\tseqID int64\n\t\ttyp   string\n\t}\n\ttype shardEvent struct {\n\t\tshardAddr    string\n\t\tconnID       uint64\n\t\ttimestamp    time.Time\n\t\tnotification []interface{}\n\t}\n\tgroupedEvents := make(map[eventKey][]shardEvent)\n\tvar seqIDOrder []eventKey // Track order of first occurrence\n\n\tfor _, event := range a.diagnosticsLog {\n\t\tif !event.Pre {\n\t\t\tcontinue\n\t\t}\n\t\tkey := eventKey{seqID: event.SeqID, typ: event.Type}\n\t\tif _, exists := groupedEvents[key]; !exists {\n\t\t\tseqIDOrder = append(seqIDOrder, key)\n\t\t}\n\n\t\t// Extract notification from details\n\t\tvar notification []interface{}\n\t\tif event.Details != nil {\n\t\t\tif notif, ok := event.Details[\"notification\"].([]interface{}); ok {\n\t\t\t\tnotification = notif\n\t\t\t}\n\t\t}\n\n\t\tgroupedEvents[key] = append(groupedEvents[key], shardEvent{\n\t\t\tshardAddr:    event.ShardAddr,\n\t\t\tconnID:       event.ConnID,\n\t\t\ttimestamp:    event.Timestamp,\n\t\t\tnotification: notification,\n\t\t})\n\t}\n\n\t// Print grouped events\n\tfor _, key := range seqIDOrder {\n\t\tevents := groupedEvents[key]\n\t\tt.Logf(\"  seqID=%d type=%s (received on %d shard(s)):\", key.seqID, key.typ, len(events))\n\n\t\t// Print notification content once (should be same for all shards)\n\t\tif len(events) > 0 && events[0].notification != nil {\n\t\t\tt.Logf(\"    notification: %v\", events[0].notification)\n\t\t}\n\n\t\t// Print which shards received it\n\t\tfor _, se := range events {\n\t\t\tshardInfo := se.shardAddr\n\t\t\tif shardInfo == \"\" {\n\t\t\t\tshardInfo = \"unknown\"\n\t\t\t}\n\t\t\tt.Logf(\"    - shard=%s connID=%d time=%s\",\n\t\t\t\tshardInfo, se.connID, se.timestamp.Format(\"15:04:05.000\"))\n\t\t}\n\t}\n\tt.Logf(\"-------------\")\n\tt.Logf(\"Diagnostics Analysis completed successfully\")\n}\n\n// setupNotificationHook adds a notification hook to a cluster client\n//\n//nolint:unused // Used in test files\nfunc setupNotificationHook(client *redis.ClusterClient, hook maintnotifications.NotificationHook) {\n\t_ = client.ForEachShard(context.Background(), func(ctx context.Context, nodeClient *redis.Client) error {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.AddNotificationHook(hook)\n\t\t}\n\t\treturn nil\n\t})\n\n\t// Also add hook to new nodes\n\tclient.OnNewNode(func(nodeClient *redis.Client) {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.AddNotificationHook(hook)\n\t\t}\n\t})\n}\n\n// setupNotificationHooks adds multiple notification hooks to a regular client\n//\n//nolint:unused // Used in test files\nfunc setupNotificationHooks(client redis.UniversalClient, hooks ...maintnotifications.NotificationHook) {\n\t// Try to get manager from the client\n\tvar manager *maintnotifications.Manager\n\n\t// Check if it's a regular client\n\tif regularClient, ok := client.(*redis.Client); ok {\n\t\tmanager = regularClient.GetMaintNotificationsManager()\n\t}\n\n\t// Check if it's a cluster client\n\tif clusterClient, ok := client.(*redis.ClusterClient); ok {\n\t\t// For cluster clients, add hooks to all shards\n\t\t_ = clusterClient.ForEachShard(context.Background(), func(ctx context.Context, nodeClient *redis.Client) error {\n\t\t\tnodeManager := nodeClient.GetMaintNotificationsManager()\n\t\t\tif nodeManager != nil {\n\t\t\t\tfor _, hook := range hooks {\n\t\t\t\t\tnodeManager.AddNotificationHook(hook)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\n\t\t// Also add hooks to new nodes\n\t\tclusterClient.OnNewNode(func(nodeClient *redis.Client) {\n\t\t\tnodeManager := nodeClient.GetMaintNotificationsManager()\n\t\t\tif nodeManager != nil {\n\t\t\t\tfor _, hook := range hooks {\n\t\t\t\t\tnodeManager.AddNotificationHook(hook)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\treturn\n\t}\n\n\t// For regular clients, add hooks directly\n\tif manager != nil {\n\t\tfor _, hook := range hooks {\n\t\t\tmanager.AddNotificationHook(hook)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/e2e/proxy_fault_injector_server.go",
    "content": "package e2e\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// ProxyFaultInjectorServer mimics the fault injector server using cae-resp-proxy\n// This allows existing e2e tests to work unchanged - they just point to this server\n// instead of the real fault injector\ntype ProxyFaultInjectorServer struct {\n\t// HTTP server for fault injector API\n\thttpServer *http.Server\n\tlistenAddr string\n\n\t// Proxy management\n\tproxyCmd     *exec.Cmd\n\tproxyAPIPort int\n\tproxyAPIURL  string\n\tproxyHTTP    *http.Client\n\n\t// Cluster node tracking\n\tnodes      []proxyNodeInfo\n\tnodesMutex sync.RWMutex\n\n\t// Action tracking\n\tactions       map[string]*actionState\n\tactionsMutex  sync.RWMutex\n\tactionCounter atomic.Int64\n\tseqIDCounter  atomic.Int64 // Counter for generating sequence IDs (starts at 1001)\n\n\t// Track if this instance started the server\n\tstartedServer bool\n\n\t// Track active notifications for new connections\n\tactiveNotifications      map[string]string // map[notificationType]notification (RESP format)\n\tactiveNotificationsMutex sync.RWMutex\n\tknownConnections         map[string]bool // map[connectionID]bool\n\tknownConnectionsMutex    sync.RWMutex\n\tmonitoringActive         atomic.Bool\n}\n\ntype proxyNodeInfo struct {\n\tlistenPort int\n\ttargetHost string\n\ttargetPort int\n\tproxyAddr  string\n\tnodeID     string\n}\n\ntype actionState struct {\n\tID         string\n\tType       ActionType\n\tStatus     ActionStatus\n\tParameters map[string]interface{}\n\tStartTime  time.Time\n\tEndTime    time.Time\n\tError      error\n\tOutput     map[string]interface{}\n}\n\n// NewProxyFaultInjectorServer creates a new proxy-based fault injector server\nfunc NewProxyFaultInjectorServer(listenAddr string, proxyAPIPort int) *ProxyFaultInjectorServer {\n\ts := &ProxyFaultInjectorServer{\n\t\tlistenAddr:   listenAddr,\n\t\tproxyAPIPort: proxyAPIPort,\n\t\tproxyAPIURL:  fmt.Sprintf(\"http://localhost:%d\", proxyAPIPort),\n\t\tproxyHTTP: &http.Client{\n\t\t\tTimeout: 5 * time.Second,\n\t\t},\n\t\tnodes:               make([]proxyNodeInfo, 0),\n\t\tactions:             make(map[string]*actionState),\n\t\tactiveNotifications: make(map[string]string),\n\t\tknownConnections:    make(map[string]bool),\n\t}\n\ts.seqIDCounter.Store(1000) // Start at 1001 (will be incremented before use)\n\treturn s\n}\n\n// NewProxyFaultInjectorServerWithURL creates a new proxy-based fault injector server with a custom proxy API URL\nfunc NewProxyFaultInjectorServerWithURL(listenAddr string, proxyAPIURL string) *ProxyFaultInjectorServer {\n\ts := &ProxyFaultInjectorServer{\n\t\tlistenAddr:  listenAddr,\n\t\tproxyAPIURL: proxyAPIURL,\n\t\tproxyHTTP: &http.Client{\n\t\t\tTimeout: 5 * time.Second,\n\t\t},\n\t\tnodes:               make([]proxyNodeInfo, 0),\n\t\tactions:             make(map[string]*actionState),\n\t\tactiveNotifications: make(map[string]string),\n\t\tknownConnections:    make(map[string]bool),\n\t}\n\ts.seqIDCounter.Store(1000) // Start at 1001 (will be incremented before use)\n\treturn s\n}\n\n// Start starts both the proxy and the fault injector API server\nfunc (s *ProxyFaultInjectorServer) Start() error {\n\t// Get cluster configuration from environment\n\tclusterAddrs := os.Getenv(\"CLUSTER_ADDRS\")\n\tif clusterAddrs == \"\" {\n\t\tclusterAddrs = \"127.0.0.1:17000,127.0.0.1:17001,127.0.0.1:17002\" // Use 127.0.0.1 to force IPv4\n\t}\n\n\ttargetHost := os.Getenv(\"REDIS_TARGET_HOST\")\n\tif targetHost == \"\" {\n\t\ttargetHost = \"localhost\"\n\t}\n\n\ttargetPort := 6379\n\tif portStr := os.Getenv(\"REDIS_TARGET_PORT\"); portStr != \"\" {\n\t\tif _, err := fmt.Sscanf(portStr, \"%d\", &targetPort); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid REDIS_TARGET_PORT: %w\", err)\n\t\t}\n\t}\n\n\t// Parse cluster addresses\n\taddrs := strings.Split(clusterAddrs, \",\")\n\tif len(addrs) == 0 {\n\t\treturn fmt.Errorf(\"no cluster addresses specified\")\n\t}\n\n\t// Extract first port for initial node\n\tvar initialPort int\n\tif _, err := fmt.Sscanf(strings.Split(addrs[0], \":\")[1], \"%d\", &initialPort); err != nil {\n\t\treturn fmt.Errorf(\"invalid port in cluster address %s: %w\", addrs[0], err)\n\t}\n\n\t// Check if proxy is already running (e.g., in Docker)\n\tproxyAlreadyRunning := false\n\tresp, err := s.proxyHTTP.Get(s.proxyAPIURL + \"/stats\")\n\tif err == nil && resp.StatusCode == 200 {\n\t\tresp.Body.Close()\n\t\tproxyAlreadyRunning = true\n\t\tfmt.Printf(\"✓ Detected existing proxy at %s (e.g., Docker container)\\n\", s.proxyAPIURL)\n\t}\n\n\t// Only start proxy if not already running\n\tif !proxyAlreadyRunning {\n\t\t// Start cae-resp-proxy with initial node\n\t\ts.proxyCmd = exec.Command(\"cae-resp-proxy\",\n\t\t\t\"--api-port\", fmt.Sprintf(\"%d\", s.proxyAPIPort),\n\t\t\t\"--listen-port\", fmt.Sprintf(\"%d\", initialPort),\n\t\t\t\"--target-host\", targetHost,\n\t\t\t\"--target-port\", fmt.Sprintf(\"%d\", targetPort),\n\t\t)\n\n\t\tif err := s.proxyCmd.Start(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to start proxy: %w\", err)\n\t\t}\n\n\t\t// Wait for proxy to be ready\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}\n\n\t// Wait for proxy to be ready and configure nodes\n\tfor i := 0; i < 10; i++ {\n\t\tresp, err := s.proxyHTTP.Get(s.proxyAPIURL + \"/stats\")\n\t\tif err == nil {\n\t\t\tresp.Body.Close()\n\t\t\tif resp.StatusCode == 200 {\n\t\t\t\t// Add initial node\n\t\t\t\ts.nodesMutex.Lock()\n\t\t\t\ts.nodes = append(s.nodes, proxyNodeInfo{\n\t\t\t\t\tlistenPort: initialPort,\n\t\t\t\t\ttargetHost: targetHost,\n\t\t\t\t\ttargetPort: targetPort,\n\t\t\t\t\tproxyAddr:  fmt.Sprintf(\"localhost:%d\", initialPort),\n\t\t\t\t\tnodeID:     fmt.Sprintf(\"node-%d\", initialPort),\n\t\t\t\t})\n\t\t\t\ts.nodesMutex.Unlock()\n\n\t\t\t\t// Add remaining nodes (only if we started the proxy ourselves)\n\t\t\t\t// If using Docker proxy, it's already configured\n\t\t\t\tif !proxyAlreadyRunning {\n\t\t\t\t\tfor i := 1; i < len(addrs); i++ {\n\t\t\t\t\t\tvar port int\n\t\t\t\t\t\tif _, err := fmt.Sscanf(strings.Split(addrs[i], \":\")[1], \"%d\", &port); err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"invalid port in cluster address %s: %w\", addrs[i], err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err := s.addProxyNode(port, targetPort, targetHost); err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"failed to add node %d: %w\", i, err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(200 * time.Millisecond)\n\t}\n\n\t// Check if HTTP server is already running on this address\n\ttestClient := &http.Client{Timeout: 1 * time.Second}\n\ttestResp, err := testClient.Get(\"http://\" + s.listenAddr + \"/actions\")\n\tif err == nil {\n\t\ttestResp.Body.Close()\n\t\tif testResp.StatusCode == http.StatusOK {\n\t\t\tfmt.Printf(\"✓ Fault injector server already running at %s\\n\", s.listenAddr)\n\t\t\tfmt.Printf(\"[ProxyFI] Proxy API at %s\\n\", s.proxyAPIURL)\n\t\t\tfmt.Printf(\"[ProxyFI] Cluster nodes: %d\\n\", len(s.nodes))\n\t\t\ts.startedServer = false // This instance didn't start the server\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Start HTTP server for fault injector API\n\tmux := http.NewServeMux()\n\n\t// Add logging middleware\n\tloggingMux := http.NewServeMux()\n\tloggingMux.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Printf(\"[ProxyFI HTTP] %s %s from %s\\n\", r.Method, r.URL.Path, r.RemoteAddr)\n\t\tmux.ServeHTTP(w, r)\n\t})\n\n\t// IMPORTANT: The real fault injector uses /action (singular) for both listing and triggering\n\t// - GET /action -> list all actions\n\t// - POST /action -> trigger a new action\n\t// - GET /action/{action_id} -> get status of a specific action\n\tmux.HandleFunc(\"/action\", s.handleAction)\n\tmux.HandleFunc(\"/action/\", s.handleActionStatus)\n\tmux.HandleFunc(\"/slot-migrate\", s.handleSlotMigrate)\n\n\ts.httpServer = &http.Server{\n\t\tAddr:    s.listenAddr,\n\t\tHandler: loggingMux,\n\t}\n\n\tgo func() {\n\t\tfmt.Printf(\"[ProxyFI] HTTP server starting on %s\\n\", s.listenAddr)\n\t\tif err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n\t\t\tfmt.Printf(\"[ProxyFI] HTTP server error: %v\\n\", err)\n\t\t}\n\t}()\n\n\t// Wait for HTTP server to be ready\n\ttime.Sleep(200 * time.Millisecond)\n\n\t// Start monitoring for new connections\n\ts.startConnectionMonitoring()\n\n\tfmt.Printf(\"[ProxyFI] Started fault injector server at %s\\n\", s.listenAddr)\n\tfmt.Printf(\"[ProxyFI] Proxy API at %s\\n\", s.proxyAPIURL)\n\tfmt.Printf(\"[ProxyFI] Cluster nodes: %d\\n\", len(s.nodes))\n\n\ts.startedServer = true // This instance started the server\n\treturn nil\n}\n\n// Stop stops both the proxy and the HTTP server\n// Only stops the server if this instance started it\nfunc (s *ProxyFaultInjectorServer) Stop() error {\n\t// Only stop if this instance started the server\n\tif !s.startedServer {\n\t\treturn nil\n\t}\n\n\t// Stop connection monitoring\n\ts.stopConnectionMonitoring()\n\n\tif s.httpServer != nil {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\t\t_ = s.httpServer.Shutdown(ctx)\n\t}\n\n\tif s.proxyCmd != nil && s.proxyCmd.Process != nil {\n\t\treturn s.proxyCmd.Process.Kill()\n\t}\n\n\treturn nil\n}\n\nfunc (s *ProxyFaultInjectorServer) addProxyNode(listenPort, targetPort int, targetHost string) error {\n\tnodeConfig := map[string]interface{}{\n\t\t\"listenPort\": listenPort,\n\t\t\"targetHost\": targetHost,\n\t\t\"targetPort\": targetPort,\n\t}\n\n\tjsonData, err := json.Marshal(nodeConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal node config: %w\", err)\n\t}\n\n\tresp, err := s.proxyHTTP.Post(\n\t\ts.proxyAPIURL+\"/nodes\",\n\t\t\"application/json\",\n\t\tbytes.NewReader(jsonData),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add node: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"failed to add node, status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\ts.nodesMutex.Lock()\n\ts.nodes = append(s.nodes, proxyNodeInfo{\n\t\tlistenPort: listenPort,\n\t\ttargetHost: targetHost,\n\t\ttargetPort: targetPort,\n\t\tproxyAddr:  fmt.Sprintf(\"localhost:%d\", listenPort),\n\t\tnodeID:     fmt.Sprintf(\"node-%d\", listenPort),\n\t})\n\ts.nodesMutex.Unlock()\n\n\ttime.Sleep(200 * time.Millisecond)\n\treturn nil\n}\n\n// HTTP Handlers - mimic fault injector API\n\n// handleAction handles both GET (list actions) and POST (trigger action) requests to /action\n// This matches the real fault injector API which uses /action (singular) for both operations\nfunc (s *ProxyFaultInjectorServer) handleAction(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase http.MethodGet:\n\t\ts.handleListActions(w, r)\n\tcase http.MethodPost:\n\t\ts.handleTriggerAction(w, r)\n\tdefault:\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t}\n}\n\nfunc (s *ProxyFaultInjectorServer) handleListActions(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\t// Return list of actions in the same format as the real fault injector API\n\t// The real API returns an array of objects with job_id, action_type, status, and submitted_at\n\tactions := []ActionListItem{\n\t\t{\n\t\t\tJobID:       \"mock-job-1\",\n\t\t\tActionType:  string(ActionSlotMigration),\n\t\t\tStatus:      \"completed\",\n\t\t\tSubmittedAt: \"2026-01-26T00:00:00+00:00\",\n\t\t},\n\t\t{\n\t\t\tJobID:       \"mock-job-2\",\n\t\t\tActionType:  string(ActionClusterMigrate),\n\t\t\tStatus:      \"completed\",\n\t\t\tSubmittedAt: \"2026-01-26T00:00:00+00:00\",\n\t\t},\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t_ = json.NewEncoder(w).Encode(actions)\n}\n\nfunc (s *ProxyFaultInjectorServer) handleTriggerAction(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\tvar req ActionRequest\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Invalid request: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"[ProxyFI] handleTriggerAction received: Type='%s', Parameters=%+v\\n\", req.Type, req.Parameters)\n\n\t// Create action\n\tactionID := fmt.Sprintf(\"action-%d\", s.actionCounter.Add(1))\n\taction := &actionState{\n\t\tID:         actionID,\n\t\tType:       req.Type,\n\t\tStatus:     StatusRunning,\n\t\tParameters: req.Parameters,\n\t\tStartTime:  time.Now(),\n\t\tOutput:     make(map[string]interface{}),\n\t}\n\n\ts.actionsMutex.Lock()\n\ts.actions[actionID] = action\n\ts.actionsMutex.Unlock()\n\n\tfmt.Printf(\"[ProxyFI] Starting executeAction goroutine for action %s\\n\", actionID)\n\n\t// Execute action asynchronously\n\tgo s.executeAction(action)\n\n\t// Return response\n\tresp := ActionResponse{\n\t\tActionID: actionID,\n\t\tStatus:   string(StatusRunning),\n\t\tMessage:  fmt.Sprintf(\"Action %s started\", req.Type),\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t_ = json.NewEncoder(w).Encode(resp)\n}\n\nfunc (s *ProxyFaultInjectorServer) handleActionStatus(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\t// Extract action ID from path\n\tactionID := strings.TrimPrefix(r.URL.Path, \"/action/\")\n\n\ts.actionsMutex.RLock()\n\taction, exists := s.actions[actionID]\n\ts.actionsMutex.RUnlock()\n\n\tif !exists {\n\t\thttp.Error(w, \"Action not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tresp := ActionStatusResponse{\n\t\tActionID:  action.ID,\n\t\tStatus:    action.Status,\n\t\tStartTime: action.StartTime,\n\t\tEndTime:   action.EndTime,\n\t\tOutput:    action.Output,\n\t}\n\n\tif action.Error != nil {\n\t\tresp.Error = action.Error.Error()\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t_ = json.NewEncoder(w).Encode(resp)\n}\n\n// executeAction executes an action and injects appropriate notifications\nfunc (s *ProxyFaultInjectorServer) executeAction(action *actionState) {\n\tdefer func() {\n\t\taction.EndTime = time.Now()\n\t\tif action.Status == StatusRunning {\n\t\t\taction.Status = StatusSuccess\n\t\t}\n\t}()\n\n\tfmt.Printf(\"[ProxyFI] executeAction called with type: '%s' (ActionBind='%s')\\n\", action.Type, ActionBind)\n\n\tswitch action.Type {\n\tcase ActionSlotMigration:\n\t\ts.executeSlotMigration(action)\n\tcase ActionClusterReshard:\n\t\ts.executeClusterReshard(action)\n\tcase ActionClusterMigrate:\n\t\ts.executeClusterMigrate(action)\n\tcase ActionFailover:\n\t\ts.executeFailover(action)\n\tcase ActionMigrate:\n\t\ts.executeMigrate(action)\n\tcase ActionBind:\n\t\tfmt.Printf(\"[ProxyFI] Matched ActionBind case\\n\")\n\t\ts.executeBind(action)\n\tcase ActionCreateDatabase:\n\t\tfmt.Printf(\"[ProxyFI] Executing CreateDatabase\\n\")\n\t\ts.executeCreateDatabase(action)\n\tcase ActionDeleteDatabase:\n\t\tfmt.Printf(\"[ProxyFI] Executing DeleteDatabase\\n\")\n\t\ts.executeDeleteDatabase(action)\n\tdefault:\n\t\tfmt.Printf(\"[ProxyFI] No match, using default case\\n\")\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"unsupported action type: %s\", action.Type)\n\t}\n}\n\nfunc (s *ProxyFaultInjectorServer) executeSlotMigration(action *actionState) {\n\t// Extract parameters\n\tstartSlot, _ := action.Parameters[\"start_slot\"].(float64)\n\tendSlot, _ := action.Parameters[\"end_slot\"].(float64)\n\tsourceNode, _ := action.Parameters[\"source_node\"].(string)\n\ttargetNode, _ := action.Parameters[\"target_node\"].(string)\n\n\tfmt.Printf(\"[ProxyFI] Executing slot migration: slots %d-%d from %s to %s\\n\",\n\t\tint(startSlot), int(endSlot), sourceNode, targetNode)\n\n\t// Generate sequence ID using counter (starts at 1001)\n\tseqID := s.generateSeqID()\n\n\t// Step 1: Inject SMIGRATING notification\n\tslotRange := fmt.Sprintf(\"%d-%d\", int(startSlot), int(endSlot))\n\tnotification := formatSMigratingNotification(seqID, slotRange)\n\n\t// Track this as an active notification for new connections\n\ts.setActiveNotification(\"SMIGRATING\", notification)\n\n\tif err := s.injectNotification(notification); err != nil {\n\t\ts.clearActiveNotification(\"SMIGRATING\")\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"failed to inject SMIGRATING: %w\", err)\n\t\treturn\n\t}\n\n\taction.Output[\"smigrating_injected\"] = true\n\taction.Output[\"seqID\"] = seqID\n\n\t// Wait a bit to simulate migration in progress\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Step 2: Inject SMIGRATED notification\n\t// Get source and target addresses from nodes\n\ts.nodesMutex.RLock()\n\tsourceAddr := \"localhost:7000\" // Default source\n\ttargetAddr := \"localhost:7001\" // Default target\n\tif len(s.nodes) > 0 {\n\t\tsourceAddr = s.nodes[0].proxyAddr\n\t}\n\tif len(s.nodes) > 1 {\n\t\ttargetAddr = s.nodes[1].proxyAddr\n\t}\n\ts.nodesMutex.RUnlock()\n\n\t// Format as triplet: \"source target slots\"\n\ttriplet := fmt.Sprintf(\"%s %s %s\", sourceAddr, targetAddr, slotRange)\n\tmigratedNotif := formatSMigratedNotification(seqID+1, triplet)\n\n\t// Clear SMIGRATING from active notifications before sending SMIGRATED\n\ts.clearActiveNotification(\"SMIGRATING\")\n\n\tif err := s.injectNotification(migratedNotif); err != nil {\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"failed to inject SMIGRATED: %w\", err)\n\t\treturn\n\t}\n\n\taction.Output[\"smigrated_injected\"] = true\n\taction.Output[\"source_endpoint\"] = sourceAddr\n\taction.Output[\"target_endpoint\"] = targetAddr\n\n\tfmt.Printf(\"[ProxyFI] Slot migration completed: %s\\n\", slotRange)\n}\n\nfunc (s *ProxyFaultInjectorServer) executeClusterReshard(action *actionState) {\n\t// Similar to slot migration but for multiple slots\n\tslots, _ := action.Parameters[\"slots\"].([]interface{})\n\tsourceNode, _ := action.Parameters[\"source_node\"].(string)\n\ttargetNode, _ := action.Parameters[\"target_node\"].(string)\n\n\tfmt.Printf(\"[ProxyFI] Executing cluster reshard: %d slots from %s to %s\\n\",\n\t\tlen(slots), sourceNode, targetNode)\n\n\t// Generate sequence ID using counter (starts at 1001)\n\tseqID := s.generateSeqID()\n\n\t// Convert slots to string ranges\n\tslotStrs := make([]string, len(slots))\n\tfor i, slot := range slots {\n\t\tslotStrs[i] = fmt.Sprintf(\"%d\", int(slot.(float64)))\n\t}\n\n\t// Inject SMIGRATING\n\tnotification := formatSMigratingNotification(seqID, slotStrs...)\n\ts.setActiveNotification(\"SMIGRATING\", notification)\n\n\tif err := s.injectNotification(notification); err != nil {\n\t\ts.clearActiveNotification(\"SMIGRATING\")\n\t\taction.Status = StatusFailed\n\t\taction.Error = err\n\t\treturn\n\t}\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Inject SMIGRATED with triplet format: \"source target slots\"\n\ts.nodesMutex.RLock()\n\tsourceAddr := \"localhost:7000\" // Default source\n\ttargetAddr := \"localhost:7001\" // Default target\n\tif len(s.nodes) > 0 {\n\t\tsourceAddr = s.nodes[0].proxyAddr\n\t}\n\tif len(s.nodes) > 1 {\n\t\ttargetAddr = s.nodes[1].proxyAddr\n\t}\n\ts.nodesMutex.RUnlock()\n\n\t// Format as triplet: \"source target slots\"\n\ttriplet := fmt.Sprintf(\"%s %s %s\", sourceAddr, targetAddr, strings.Join(slotStrs, \",\"))\n\tmigratedNotif := formatSMigratedNotification(seqID+1, triplet)\n\n\t// Clear SMIGRATING from active notifications before sending SMIGRATED\n\ts.clearActiveNotification(\"SMIGRATING\")\n\n\tif err := s.injectNotification(migratedNotif); err != nil {\n\t\taction.Status = StatusFailed\n\t\taction.Error = err\n\t\treturn\n\t}\n\n\taction.Output[\"slots_migrated\"] = len(slots)\n}\n\nfunc (s *ProxyFaultInjectorServer) executeClusterMigrate(action *actionState) {\n\t// Similar to slot migration\n\ts.executeSlotMigration(action)\n}\n\nfunc (s *ProxyFaultInjectorServer) injectNotification(notification string) error {\n\turl := s.proxyAPIURL + \"/send-to-all-clients?encoding=raw\"\n\n\tfmt.Printf(\"[ProxyFI] Injecting notification to %s\\n\", url)\n\tfmt.Printf(\"[ProxyFI] Notification (first 100 chars): %s\\n\", notification[:min(100, len(notification))])\n\n\tresp, err := s.proxyHTTP.Post(url, \"application/octet-stream\", strings.NewReader(notification))\n\tif err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Failed to inject notification: %v\\n\", err)\n\t\treturn fmt.Errorf(\"failed to inject notification: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\tfmt.Printf(\"[ProxyFI] Injection failed with status %d: %s\\n\", resp.StatusCode, string(body))\n\t\treturn fmt.Errorf(\"injection failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tfmt.Printf(\"[ProxyFI] Notification injected successfully\\n\")\n\treturn nil\n}\n\n// Helper function for min\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc (s *ProxyFaultInjectorServer) executeFailover(action *actionState) {\n\tfmt.Printf(\"[ProxyFI] Executing failover\\n\")\n\n\t// Generate sequence ID using counter (starts at 1001)\n\tseqID := s.generateSeqID()\n\n\t// Step 1: Inject FAILING_OVER notification\n\t// Format: [\"FAILING_OVER\", SeqID]\n\tfailingOverNotif := fmt.Sprintf(\">2\\r\\n$12\\r\\nFAILING_OVER\\r\\n:%d\\r\\n\", seqID)\n\n\ts.setActiveNotification(\"FAILING_OVER\", failingOverNotif)\n\n\tif err := s.injectNotification(failingOverNotif); err != nil {\n\t\ts.clearActiveNotification(\"FAILING_OVER\")\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"failed to inject FAILING_OVER: %w\", err)\n\t\treturn\n\t}\n\n\taction.Output[\"failing_over_injected\"] = true\n\taction.Output[\"seqID\"] = seqID\n\n\t// Wait to simulate failover in progress\n\ttime.Sleep(1 * time.Second)\n\n\t// Step 2: Inject FAILED_OVER notification\n\t// Format: [\"FAILED_OVER\", SeqID]\n\tfailedOverNotif := fmt.Sprintf(\">2\\r\\n$11\\r\\nFAILED_OVER\\r\\n:%d\\r\\n\", seqID+1)\n\n\ts.clearActiveNotification(\"FAILING_OVER\")\n\n\tif err := s.injectNotification(failedOverNotif); err != nil {\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"failed to inject FAILED_OVER: %w\", err)\n\t\treturn\n\t}\n\n\taction.Output[\"failed_over_injected\"] = true\n\n\tfmt.Printf(\"[ProxyFI] Failover completed\\n\")\n}\n\nfunc (s *ProxyFaultInjectorServer) executeMigrate(action *actionState) {\n\tfmt.Printf(\"[ProxyFI] Executing migrate\\n\")\n\n\t// Generate sequence ID using counter (starts at 1001)\n\tseqID := s.generateSeqID()\n\tslot := 1000 // Default slot for migration\n\n\t// Step 1: Inject MIGRATING notification (no MOVING for migrate action)\n\t// Format: [\"MIGRATING\", seqID, slot]\n\tslotStr := fmt.Sprintf(\"%d\", slot)\n\tmigratingNotif := fmt.Sprintf(\">3\\r\\n$9\\r\\nMIGRATING\\r\\n:%d\\r\\n$%d\\r\\n%s\\r\\n\",\n\t\tseqID, len(slotStr), slotStr)\n\n\ts.setActiveNotification(\"MIGRATING\", migratingNotif)\n\n\tif err := s.injectNotification(migratingNotif); err != nil {\n\t\ts.clearActiveNotification(\"MIGRATING\")\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"failed to inject MIGRATING: %w\", err)\n\t\treturn\n\t}\n\n\taction.Output[\"migrating_injected\"] = true\n\taction.Output[\"seqID\"] = seqID\n\n\t// Wait to simulate migration in progress\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Step 2: Inject MIGRATED notification\n\t// Format: [\"MIGRATED\", seqID, slot]\n\tmigratedNotif := fmt.Sprintf(\">3\\r\\n$8\\r\\nMIGRATED\\r\\n:%d\\r\\n$%d\\r\\n%s\\r\\n\",\n\t\tseqID+1, len(slotStr), slotStr)\n\n\ts.clearActiveNotification(\"MIGRATING\")\n\n\tif err := s.injectNotification(migratedNotif); err != nil {\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"failed to inject MIGRATED: %w\", err)\n\t\treturn\n\t}\n\n\taction.Output[\"migrated_injected\"] = true\n\n\tfmt.Printf(\"[ProxyFI] Migrate completed\\n\")\n}\n\nfunc (s *ProxyFaultInjectorServer) executeBind(action *actionState) {\n\tfmt.Printf(\"[ProxyFI] Executing bind\\n\")\n\n\t// Generate sequence ID using counter (starts at 1001)\n\tseqID := s.generateSeqID()\n\ttimeS := int64(5) // Time in seconds for handoff\n\n\t// Get endpoint type from parameters (if provided)\n\tendpointType, _ := action.Parameters[\"endpoint_type\"].(string)\n\tfmt.Printf(\"[ProxyFI] Bind action - endpoint_type parameter: '%s'\\n\", endpointType)\n\n\t// Determine target endpoint based on endpoint type\n\tvar targetEndpoint string\n\ts.nodesMutex.RLock()\n\tdefaultAddr := \"localhost:7000\"\n\tif len(s.nodes) > 0 {\n\t\tdefaultAddr = s.nodes[0].proxyAddr\n\t}\n\ts.nodesMutex.RUnlock()\n\n\tswitch endpointType {\n\tcase \"external-ip\":\n\t\t// Return IP address (use 127.0.0.1 for localhost)\n\t\thost := strings.Split(defaultAddr, \":\")[0]\n\t\tport := strings.Split(defaultAddr, \":\")[1]\n\t\tif host == \"localhost\" {\n\t\t\thost = \"127.0.0.1\"\n\t\t}\n\t\ttargetEndpoint = fmt.Sprintf(\"%s:%s\", host, port)\n\t\tfmt.Printf(\"[ProxyFI] Using external-ip format: %s\\n\", targetEndpoint)\n\n\tcase \"external-fqdn\":\n\t\t// Return FQDN format (e.g., node-1.localhost:7000)\n\t\t// Extract host and port from defaultAddr\n\t\tparts := strings.Split(defaultAddr, \":\")\n\t\thost := parts[0]\n\t\tport := parts[1]\n\n\t\t// Create FQDN by prepending \"node-1.\" to the host\n\t\t// This ensures the domain suffix matches the endpointConfig.Host\n\t\ttargetEndpoint = fmt.Sprintf(\"node-1.%s:%s\", host, port)\n\t\tfmt.Printf(\"[ProxyFI] Using external-fqdn format: %s\\n\", targetEndpoint)\n\n\tcase \"none\":\n\t\t// Return null for \"none\" endpoint type\n\t\t// For RESP3, null is represented as \"_\\r\\n\"\n\t\t// But in array context, we use bulk string \"$-1\\r\\n\"\n\t\t// Actually, for \"none\" we should send the special null value\n\t\t// Let's use the internal.RedisNull constant which is \"-\"\n\t\ttargetEndpoint = \"-\"\n\t\tfmt.Printf(\"[ProxyFI] Using none format: null\\n\")\n\n\tcase \"\":\n\t\t// Empty endpoint type - use default\n\t\ttargetEndpoint = defaultAddr\n\t\tfmt.Printf(\"[ProxyFI] Empty endpoint_type, using default: %s\\n\", targetEndpoint)\n\n\tdefault:\n\t\t// Default to localhost address\n\t\ttargetEndpoint = defaultAddr\n\t\tfmt.Printf(\"[ProxyFI] Unknown endpoint_type '%s', using default: %s\\n\", endpointType, targetEndpoint)\n\t}\n\n\t// Inject MOVING notification\n\t// Format: [\"MOVING\", seqID, timeS, endpoint]\n\tvar movingNotif string\n\tif targetEndpoint == \"-\" {\n\t\t// Special case for null endpoint (EndpointTypeNone)\n\t\t// Use RESP3 null: \"_\\r\\n\" in array context\n\t\tmovingNotif = fmt.Sprintf(\">4\\r\\n$6\\r\\nMOVING\\r\\n:%d\\r\\n:%d\\r\\n_\\r\\n\",\n\t\t\tseqID, timeS)\n\t} else {\n\t\tmovingNotif = fmt.Sprintf(\">4\\r\\n$6\\r\\nMOVING\\r\\n:%d\\r\\n:%d\\r\\n$%d\\r\\n%s\\r\\n\",\n\t\t\tseqID, timeS, len(targetEndpoint), targetEndpoint)\n\t}\n\n\ts.setActiveNotification(\"MOVING\", movingNotif)\n\n\tif err := s.injectNotification(movingNotif); err != nil {\n\t\ts.clearActiveNotification(\"MOVING\")\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"failed to inject MOVING: %w\", err)\n\t\treturn\n\t}\n\n\taction.Output[\"moving_injected\"] = true\n\taction.Output[\"seqID\"] = seqID\n\taction.Output[\"target_endpoint\"] = targetEndpoint\n\taction.Output[\"endpoint_type\"] = endpointType\n\n\tfmt.Printf(\"[ProxyFI] Bind completed - MOVING notification sent (endpoint_type=%s, endpoint=%s)\\n\", endpointType, targetEndpoint)\n}\n\n// GetClusterAddrs returns the cluster addresses for connecting\nfunc (s *ProxyFaultInjectorServer) GetClusterAddrs() []string {\n\ts.nodesMutex.RLock()\n\tdefer s.nodesMutex.RUnlock()\n\n\taddrs := make([]string, len(s.nodes))\n\tfor i, node := range s.nodes {\n\t\taddrs[i] = node.proxyAddr\n\t}\n\treturn addrs\n}\n\n// generateSeqID generates a new sequence ID starting from 1001\nfunc (s *ProxyFaultInjectorServer) generateSeqID() int64 {\n\treturn s.seqIDCounter.Add(1)\n}\n\n// setActiveNotification stores a notification that should be sent to new connections\nfunc (s *ProxyFaultInjectorServer) setActiveNotification(notifType string, notification string) {\n\ts.activeNotificationsMutex.Lock()\n\tdefer s.activeNotificationsMutex.Unlock()\n\ts.activeNotifications[notifType] = notification\n\tfmt.Printf(\"[ProxyFI] Set active notification: %s\\n\", notifType)\n}\n\n// clearActiveNotification removes an active notification\nfunc (s *ProxyFaultInjectorServer) clearActiveNotification(notifType string) {\n\ts.activeNotificationsMutex.Lock()\n\tdefer s.activeNotificationsMutex.Unlock()\n\tdelete(s.activeNotifications, notifType)\n\tfmt.Printf(\"[ProxyFI] Cleared active notification: %s\\n\", notifType)\n}\n\n// getActiveNotifications returns a copy of all active notifications\nfunc (s *ProxyFaultInjectorServer) getActiveNotifications() map[string]string {\n\ts.activeNotificationsMutex.RLock()\n\tdefer s.activeNotificationsMutex.RUnlock()\n\n\tnotifications := make(map[string]string, len(s.activeNotifications))\n\tfor k, v := range s.activeNotifications {\n\t\tnotifications[k] = v\n\t}\n\treturn notifications\n}\n\n// startConnectionMonitoring starts monitoring for new connections\nfunc (s *ProxyFaultInjectorServer) startConnectionMonitoring() {\n\tif s.monitoringActive.Swap(true) {\n\t\treturn // Already monitoring\n\t}\n\n\tgo func() {\n\t\tticker := time.NewTicker(100 * time.Millisecond) // Poll every 100ms for faster detection\n\t\tdefer ticker.Stop()\n\n\t\tfor s.monitoringActive.Load() {\n\t\t\t<-ticker.C\n\t\t\ts.checkForNewConnections()\n\t\t}\n\t}()\n\tfmt.Printf(\"[ProxyFI] Started connection monitoring (polling every 100ms)\\n\")\n}\n\n// stopConnectionMonitoring stops monitoring for new connections\nfunc (s *ProxyFaultInjectorServer) stopConnectionMonitoring() {\n\ts.monitoringActive.Store(false)\n\tfmt.Printf(\"[ProxyFI] Stopped connection monitoring\\n\")\n}\n\n// checkForNewConnections checks for new connections and sends active notifications\nfunc (s *ProxyFaultInjectorServer) checkForNewConnections() {\n\t// Debug: Log that we're checking\n\tactiveNotifs := s.getActiveNotifications()\n\tfmt.Printf(\"[ProxyFI] Connection monitoring: checking (active notifications: %d)...\\n\", len(activeNotifs))\n\n\t// Get current connections from proxy stats endpoint\n\tresp, err := s.proxyHTTP.Get(s.proxyAPIURL + \"/stats\")\n\tif err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Connection monitoring: failed to get stats: %v\\n\", err)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tfmt.Printf(\"[ProxyFI] Connection monitoring: stats returned status %d\\n\", resp.StatusCode)\n\t\treturn\n\t}\n\n\t// Parse stats response - format is map[backend]stats\n\t// connections is an array of connection objects with \"id\" field\n\tvar stats map[string]struct {\n\t\tConnections []struct {\n\t\t\tID string `json:\"id\"`\n\t\t} `json:\"connections\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Connection monitoring: failed to decode stats: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Collect all connection IDs from all backends\n\tallConnIDs := make([]string, 0)\n\tfor backend, backendStats := range stats {\n\t\tfor _, conn := range backendStats.Connections {\n\t\t\tallConnIDs = append(allConnIDs, conn.ID)\n\t\t}\n\t\tfmt.Printf(\"[ProxyFI] Connection monitoring: backend %s has %d connections\\n\", backend, len(backendStats.Connections))\n\t}\n\n\t// Check for new connections\n\ts.knownConnectionsMutex.Lock()\n\tnewConnections := make([]string, 0)\n\tfor _, connID := range allConnIDs {\n\t\tif !s.knownConnections[connID] {\n\t\t\ts.knownConnections[connID] = true\n\t\t\tnewConnections = append(newConnections, connID)\n\t\t}\n\t}\n\ttotalKnown := len(s.knownConnections)\n\ts.knownConnectionsMutex.Unlock()\n\n\tfmt.Printf(\"[ProxyFI] Connection monitoring: total=%d, known=%d, new=%d\\n\", len(allConnIDs), totalKnown, len(newConnections))\n\n\t// Send active notifications to new connections\n\tif len(newConnections) > 0 {\n\t\tactiveNotifs := s.getActiveNotifications()\n\t\tfmt.Printf(\"[ProxyFI] Found %d new connection(s), have %d active notification(s)\\n\",\n\t\t\tlen(newConnections), len(activeNotifs))\n\n\t\tif len(activeNotifs) > 0 {\n\t\t\tfor _, connID := range newConnections {\n\t\t\t\tfor notifType, notification := range activeNotifs {\n\t\t\t\t\tif err := s.sendToConnection(connID, notification); err != nil {\n\t\t\t\t\t\tfmt.Printf(\"[ProxyFI] Failed to send %s to new connection %s: %v\\n\",\n\t\t\t\t\t\t\tnotifType, connID, err)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfmt.Printf(\"[ProxyFI] Sent %s to new connection %s\\n\", notifType, connID)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// sendToConnection sends a notification to a specific connection\nfunc (s *ProxyFaultInjectorServer) sendToConnection(connID string, notification string) error {\n\turl := fmt.Sprintf(\"%s/send-to-client/%s?encoding=raw\", s.proxyAPIURL, connID)\n\n\tresp, err := s.proxyHTTP.Post(url, \"application/octet-stream\", strings.NewReader(notification))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to send to connection: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"failed to send to connection, status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\treturn nil\n}\n\n// handleSlotMigrate handles the /slot-migrate endpoint\n// GET: Returns available triggers for a slot migration effect\n// POST: Triggers a slot migration action\nfunc (s *ProxyFaultInjectorServer) handleSlotMigrate(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase http.MethodGet:\n\t\ts.handleSlotMigrateGetTriggers(w, r)\n\tcase http.MethodPost:\n\t\ts.handleSlotMigrateTrigger(w, r)\n\tdefault:\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t}\n}\n\n// handleSlotMigrateGetTriggers returns available triggers for a slot migration effect\nfunc (s *ProxyFaultInjectorServer) handleSlotMigrateGetTriggers(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\teffect := SlotMigrateEffect(query.Get(\"effect\"))\n\tif effect == \"\" {\n\t\thttp.Error(w, \"Missing required parameter: effect\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// Parse cluster_index (optional, default: 0)\n\tclusterIndex := 0\n\tif clusterIndexStr := query.Get(\"cluster_index\"); clusterIndexStr != \"\" {\n\t\tif _, err := fmt.Sscanf(clusterIndexStr, \"%d\", &clusterIndex); err != nil {\n\t\t\thttp.Error(w, \"Invalid cluster_index parameter\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Build cluster field with proper structure: {index, nodes: [{host, port}, ...]}\n\ts.nodesMutex.RLock()\n\tnodesArray := make([]map[string]interface{}, len(s.nodes))\n\tfor i, node := range s.nodes {\n\t\t// Parse host and port from proxyAddr (format: \"localhost:7001\")\n\t\tparts := strings.Split(node.proxyAddr, \":\")\n\t\thost := parts[0]\n\t\tport := node.listenPort\n\t\tnodesArray[i] = map[string]interface{}{\n\t\t\t\"host\": host,\n\t\t\t\"port\": port,\n\t\t}\n\t}\n\ts.nodesMutex.RUnlock()\n\n\tclusterInfo := map[string]interface{}{\n\t\t\"index\": clusterIndex,\n\t\t\"nodes\": nodesArray,\n\t}\n\n\t// Return mock triggers based on effect\n\tvar triggers []SlotMigrateTrigger\n\n\t// Helper function to create requirements with dbconfig and cluster\n\tmakeRequirements := func(dbconfig map[string]interface{}, description string) []SlotMigrateTriggerRequirement {\n\t\treturn []SlotMigrateTriggerRequirement{\n\t\t\t{\n\t\t\t\tDBConfig:    dbconfig,\n\t\t\t\tCluster:     clusterInfo,\n\t\t\t\tDescription: description,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Default dbconfig for migrate (sharded cluster)\n\tdefaultDBConfig := map[string]interface{}{\n\t\t\"shards_count\":     3,\n\t\t\"shards_placement\": \"sparse\",\n\t}\n\n\t// Failover requirements (requires replication)\n\tfailoverDBConfig := map[string]interface{}{\n\t\t\"shards_count\":     3,\n\t\t\"shards_placement\": \"sparse\",\n\t\t\"replication\":      true,\n\t}\n\n\tswitch effect {\n\tcase SlotMigrateEffectRemoveAdd:\n\t\ttriggers = []SlotMigrateTrigger{\n\t\t\t{\n\t\t\t\tName:         \"migrate\",\n\t\t\t\tDescription:  \"Migrate all shards from source node to empty node\",\n\t\t\t\tRequirements: makeRequirements(defaultDBConfig, \"Requires sharded cluster\"),\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:         \"failover\",\n\t\t\t\tDescription:  \"Trigger failover (requires replication)\",\n\t\t\t\tRequirements: makeRequirements(failoverDBConfig, \"Requires replication enabled for failover\"),\n\t\t\t},\n\t\t}\n\tcase SlotMigrateEffectRemove:\n\t\ttriggers = []SlotMigrateTrigger{\n\t\t\t{\n\t\t\t\tName:         \"migrate\",\n\t\t\t\tDescription:  \"Migrate all shards from source node to existing node\",\n\t\t\t\tRequirements: makeRequirements(defaultDBConfig, \"Requires sharded cluster\"),\n\t\t\t},\n\t\t}\n\tcase SlotMigrateEffectAdd:\n\t\ttriggers = []SlotMigrateTrigger{\n\t\t\t{\n\t\t\t\tName:         \"migrate\",\n\t\t\t\tDescription:  \"Migrate one shard to empty node\",\n\t\t\t\tRequirements: makeRequirements(defaultDBConfig, \"Requires sharded cluster\"),\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:         \"failover\",\n\t\t\t\tDescription:  \"Trigger failover (requires replication)\",\n\t\t\t\tRequirements: makeRequirements(failoverDBConfig, \"Requires replication enabled for failover\"),\n\t\t\t},\n\t\t}\n\tcase SlotMigrateEffectSlotShuffle:\n\t\ttriggers = []SlotMigrateTrigger{\n\t\t\t{\n\t\t\t\tName:         \"migrate\",\n\t\t\t\tDescription:  \"Migrate one shard between existing nodes\",\n\t\t\t\tRequirements: makeRequirements(defaultDBConfig, \"Requires sharded cluster\"),\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:         \"failover\",\n\t\t\t\tDescription:  \"Trigger failover (requires replication)\",\n\t\t\t\tRequirements: makeRequirements(failoverDBConfig, \"Requires replication enabled for failover\"),\n\t\t\t},\n\t\t}\n\tdefault:\n\t\thttp.Error(w, fmt.Sprintf(\"Unknown effect: %s\", effect), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tresponse := SlotMigrateTriggersResponse{\n\t\tEffect:   effect,\n\t\tCluster:  clusterInfo,\n\t\tTriggers: triggers,\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t_ = json.NewEncoder(w).Encode(response)\n}\n\n// handleSlotMigrateTrigger triggers a slot migration action\nfunc (s *ProxyFaultInjectorServer) handleSlotMigrateTrigger(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\teffect := SlotMigrateEffect(query.Get(\"effect\"))\n\tbdbID := query.Get(\"bdb_id\")\n\t// IMPORTANT: The real fault injector API uses \"variant\" as the query parameter name (rest_api.py line 301)\n\t// We use \"trigger\" as the variable name internally, but must read from \"variant\" parameter\n\ttrigger := SlotMigrateVariant(query.Get(\"variant\"))\n\tsourceNode := query.Get(\"source_node\")\n\ttargetNode := query.Get(\"target_node\")\n\tclusterIndexStr := query.Get(\"cluster_index\")\n\n\t// Parse cluster_index with default of 0\n\tclusterIndex := 0\n\tif clusterIndexStr != \"\" {\n\t\tif idx, err := strconv.Atoi(clusterIndexStr); err == nil {\n\t\t\tclusterIndex = idx\n\t\t}\n\t}\n\n\tif effect == \"\" {\n\t\thttp.Error(w, \"Missing required parameter: effect\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif bdbID == \"\" {\n\t\thttp.Error(w, \"Missing required parameter: bdb_id\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// Default trigger is \"migrate\"\n\tif trigger == \"\" || trigger == SlotMigrateVariantDefault {\n\t\ttrigger = SlotMigrateVariantMigrate\n\t}\n\n\tfmt.Printf(\"[ProxyFI] Slot-migrate: effect=%s, bdb_id=%s, trigger=%s, cluster_index=%d, source_node=%s, target_node=%s\\n\",\n\t\teffect, bdbID, trigger, clusterIndex, sourceNode, targetNode)\n\n\t// Create action\n\tactionID := fmt.Sprintf(\"slot-migrate-%d\", s.actionCounter.Add(1))\n\taction := &actionState{\n\t\tID:     actionID,\n\t\tType:   ActionSlotMigrate,\n\t\tStatus: StatusRunning,\n\t\tParameters: map[string]interface{}{\n\t\t\t\"effect\":        string(effect),\n\t\t\t\"bdb_id\":        bdbID,\n\t\t\t\"variant\":       string(trigger),\n\t\t\t\"cluster_index\": clusterIndex,\n\t\t\t\"source_node\":   sourceNode,\n\t\t\t\"target_node\":   targetNode,\n\t\t},\n\t\tStartTime: time.Now(),\n\t\tOutput:    make(map[string]interface{}),\n\t}\n\n\ts.actionsMutex.Lock()\n\ts.actions[actionID] = action\n\ts.actionsMutex.Unlock()\n\n\t// Execute action asynchronously\n\tgo s.executeSlotMigrateAction(action, effect, trigger)\n\n\t// Return response per spec: only {\"action_id\": \"...\"}\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t_ = json.NewEncoder(w).Encode(map[string]string{\"action_id\": actionID})\n}\n\n// executeSlotMigrateAction executes a slot-migrate action with the specified effect and trigger\nfunc (s *ProxyFaultInjectorServer) executeSlotMigrateAction(action *actionState, effect SlotMigrateEffect, trigger SlotMigrateVariant) {\n\tdefer func() {\n\t\taction.EndTime = time.Now()\n\t\tif action.Status == StatusRunning {\n\t\t\taction.Status = StatusSuccess\n\t\t}\n\t}()\n\n\tfmt.Printf(\"[ProxyFI] Executing slot-migrate: effect=%s, trigger=%s\\n\", effect, trigger)\n\n\tswitch effect {\n\tcase SlotMigrateEffectRemoveAdd:\n\t\ts.executeSlotMigrateRemoveAdd(action, trigger)\n\tcase SlotMigrateEffectRemove:\n\t\ts.executeSlotMigrateRemove(action, trigger)\n\tcase SlotMigrateEffectAdd:\n\t\ts.executeSlotMigrateAdd(action, trigger)\n\tcase SlotMigrateEffectSlotShuffle:\n\t\ts.executeSlotMigrateSlotShuffle(action, trigger)\n\tdefault:\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"unsupported slot-migrate effect: %s\", effect)\n\t}\n}\n\n// executeSlotMigrateRemoveAdd simulates migrating all shards from one node to a new node\n// Effect: One endpoint removed, one endpoint added\nfunc (s *ProxyFaultInjectorServer) executeSlotMigrateRemoveAdd(action *actionState, trigger SlotMigrateVariant) {\n\ts.nodesMutex.RLock()\n\tif len(s.nodes) < 2 {\n\t\ts.nodesMutex.RUnlock()\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"need at least 2 nodes for remove-add effect\")\n\t\treturn\n\t}\n\n\t// Pick a source node (the one that will be \"removed\")\n\tsourceNode := s.nodes[0]\n\tsourceAddr := sourceNode.proxyAddr\n\tnumNodes := len(s.nodes)\n\ts.nodesMutex.RUnlock()\n\n\t// Calculate simulated slot range for this node (16384 slots total, distributed evenly)\n\tslotsPerNode := 16384 / numNodes\n\tslotStart := 0 // First node starts at slot 0\n\tslotEnd := slotsPerNode - 1\n\n\t// Generate a new endpoint address (simulating a new node)\n\tnewAddr := fmt.Sprintf(\"127.0.0.1:%d\", 7000+numNodes+1)\n\n\tfmt.Printf(\"[ProxyFI] remove-add: source=%s (slots %d-%d) -> new=%s\\n\", sourceAddr, slotStart, slotEnd, newAddr)\n\n\t// Generate sequence ID for notifications\n\tseqID := s.generateSeqID()\n\n\t// Send SMIGRATING notification for all slots on source node\n\tsmigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf(\"%d-%d\", slotStart, slotEnd))\n\tif err := s.injectNotification(smigratingMsg); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATING: %v\\n\", err)\n\t}\n\n\t// Brief delay to simulate migration in progress\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Send SMIGRATED notification with the new endpoint\n\tsmigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf(\"%s %d-%d\", newAddr, slotStart, slotEnd))\n\tif err := s.injectNotification(smigratedMsg); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATED: %v\\n\", err)\n\t}\n\n\taction.Output[\"source_addr\"] = sourceAddr\n\taction.Output[\"new_addr\"] = newAddr\n\taction.Output[\"slots\"] = fmt.Sprintf(\"%d-%d\", slotStart, slotEnd)\n\taction.Output[\"variant\"] = string(trigger)\n}\n\n// executeSlotMigrateRemove simulates migrating all shards from one node to an existing node\n// Effect: One endpoint removed (node count decreases by 1)\nfunc (s *ProxyFaultInjectorServer) executeSlotMigrateRemove(action *actionState, trigger SlotMigrateVariant) {\n\ts.nodesMutex.Lock()\n\tif len(s.nodes) < 2 {\n\t\ts.nodesMutex.Unlock()\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"need at least 2 nodes for remove effect\")\n\t\treturn\n\t}\n\n\t// Pick source and destination nodes\n\tsourceNode := s.nodes[0]\n\tdestNode := s.nodes[1]\n\tsourceAddr := sourceNode.proxyAddr\n\tdestAddr := destNode.proxyAddr\n\tnumNodes := len(s.nodes)\n\n\t// Remove the source node from the node list (topology change)\n\ts.nodes = s.nodes[1:]\n\ts.nodesMutex.Unlock()\n\n\t// Calculate simulated slot range for source node\n\tslotsPerNode := 16384 / numNodes\n\tslotStart := 0\n\tslotEnd := slotsPerNode - 1\n\n\tfmt.Printf(\"[ProxyFI] remove: source=%s (slots %d-%d) -> dest=%s (nodes: %d -> %d)\\n\", sourceAddr, slotStart, slotEnd, destAddr, numNodes, numNodes-1)\n\n\t// Generate sequence ID for notifications\n\tseqID := s.generateSeqID()\n\n\t// Send SMIGRATING notification\n\tsmigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf(\"%d-%d\", slotStart, slotEnd))\n\tif err := s.injectNotification(smigratingMsg); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATING: %v\\n\", err)\n\t}\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Send SMIGRATED notification with existing endpoint\n\tsmigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf(\"%s %d-%d\", destAddr, slotStart, slotEnd))\n\tif err := s.injectNotification(smigratedMsg); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATED: %v\\n\", err)\n\t}\n\n\taction.Output[\"source_addr\"] = sourceAddr\n\taction.Output[\"dest_addr\"] = destAddr\n\taction.Output[\"slots\"] = fmt.Sprintf(\"%d-%d\", slotStart, slotEnd)\n\taction.Output[\"variant\"] = string(trigger)\n\taction.Output[\"nodes_before\"] = numNodes\n\taction.Output[\"nodes_after\"] = numNodes - 1\n}\n\n// executeSlotMigrateAdd simulates migrating one shard from an existing node to a new node\n// Effect: One endpoint added (net +1 node)\nfunc (s *ProxyFaultInjectorServer) executeSlotMigrateAdd(action *actionState, trigger SlotMigrateVariant) {\n\ts.nodesMutex.Lock()\n\tif len(s.nodes) < 1 {\n\t\ts.nodesMutex.Unlock()\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"need at least 1 node for add effect\")\n\t\treturn\n\t}\n\n\t// Pick a source node (an existing node that will give up some slots)\n\tsourceNode := s.nodes[0]\n\tsourceAddr := sourceNode.proxyAddr\n\tnodesBefore := len(s.nodes)\n\n\t// Calculate partial slot range (one shard worth - roughly 1/3 of the node's slots)\n\tslotsPerNode := 16384 / nodesBefore\n\tslotStart := 0\n\tslotRange := slotsPerNode / 3\n\tslotEnd := slotStart + slotRange\n\n\t// Generate a new endpoint address (simulating a new node)\n\tnewAddr := fmt.Sprintf(\"127.0.0.1:%d\", 7000+nodesBefore+1)\n\n\t// Add the new node to the cluster\n\tnewNode := proxyNodeInfo{\n\t\tlistenPort: 7000 + nodesBefore + 1,\n\t\tproxyAddr:  newAddr,\n\t\tnodeID:     fmt.Sprintf(\"node-%d\", nodesBefore+1),\n\t}\n\ts.nodes = append(s.nodes, newNode)\n\tnodesAfter := len(s.nodes)\n\ts.nodesMutex.Unlock()\n\n\tfmt.Printf(\"[ProxyFI] add: source=%s (slots %d-%d) -> new=%s, nodes: %d -> %d\\n\",\n\t\tsourceAddr, slotStart, slotEnd, newAddr, nodesBefore, nodesAfter)\n\n\t// Generate sequence ID for notifications\n\tseqID := s.generateSeqID()\n\n\t// Send SMIGRATING notification for partial slot range\n\tsmigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf(\"%d-%d\", slotStart, slotEnd))\n\tif err := s.injectNotification(smigratingMsg); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATING: %v\\n\", err)\n\t}\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Send SMIGRATED notification with the new endpoint\n\tsmigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf(\"%s %d-%d\", newAddr, slotStart, slotEnd))\n\tif err := s.injectNotification(smigratedMsg); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATED: %v\\n\", err)\n\t}\n\n\taction.Output[\"source_addr\"] = sourceAddr\n\taction.Output[\"new_addr\"] = newAddr\n\taction.Output[\"slots\"] = fmt.Sprintf(\"%d-%d\", slotStart, slotEnd)\n\taction.Output[\"variant\"] = string(trigger)\n\taction.Output[\"nodes_before\"] = nodesBefore\n\taction.Output[\"nodes_after\"] = nodesAfter\n}\n\n// executeSlotMigrateSlotShuffle simulates moving slots between existing nodes\n// Effect: Slots move, no endpoint changes (same nodes, different slot distribution)\nfunc (s *ProxyFaultInjectorServer) executeSlotMigrateSlotShuffle(action *actionState, trigger SlotMigrateVariant) {\n\ts.nodesMutex.RLock()\n\tif len(s.nodes) < 2 {\n\t\ts.nodesMutex.RUnlock()\n\t\taction.Status = StatusFailed\n\t\taction.Error = fmt.Errorf(\"need at least 2 nodes for slot-shuffle effect\")\n\t\treturn\n\t}\n\n\t// Pick source and destination nodes (both existing)\n\tsourceNode := s.nodes[0]\n\tsourceAddr := sourceNode.proxyAddr\n\tdestNode := s.nodes[1]\n\tdestAddr := destNode.proxyAddr\n\tnumNodes := len(s.nodes)\n\n\t// Calculate partial slot range (one shard worth)\n\tslotsPerNode := 16384 / numNodes\n\tslotStart := 0\n\tslotRange := slotsPerNode / 3\n\tslotEnd := slotStart + slotRange\n\ts.nodesMutex.RUnlock()\n\n\tfmt.Printf(\"[ProxyFI] slot-shuffle: source=%s (slots %d-%d) -> dest=%s\\n\", sourceAddr, slotStart, slotEnd, destAddr)\n\n\t// Generate sequence ID for notifications\n\tseqID := s.generateSeqID()\n\n\t// Send SMIGRATING notification\n\tsmigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf(\"%d-%d\", slotStart, slotEnd))\n\n\t// Track this as an active notification for new connections\n\ts.setActiveNotification(\"SMIGRATING\", smigratingMsg)\n\n\tif err := s.injectNotification(smigratingMsg); err != nil {\n\t\ts.clearActiveNotification(\"SMIGRATING\")\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATING: %v\\n\", err)\n\t}\n\n\t// Wait 500ms to simulate migration in progress - allows tests to create new connections\n\t// that should receive the active SMIGRATING notification\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Clear SMIGRATING from active notifications before sending SMIGRATED\n\ts.clearActiveNotification(\"SMIGRATING\")\n\n\t// Send SMIGRATED notification with existing destination endpoint\n\tsmigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf(\"%s %d-%d\", destAddr, slotStart, slotEnd))\n\tif err := s.injectNotification(smigratedMsg); err != nil {\n\t\tfmt.Printf(\"[ProxyFI] Error sending SMIGRATED: %v\\n\", err)\n\t}\n\n\taction.Output[\"source_addr\"] = sourceAddr\n\taction.Output[\"dest_addr\"] = destAddr\n\taction.Output[\"slots\"] = fmt.Sprintf(\"%d-%d\", slotStart, slotEnd)\n\taction.Output[\"variant\"] = string(trigger)\n}\n\n// executeDeleteDatabase clears all nodes to simulate database deletion\nfunc (s *ProxyFaultInjectorServer) executeDeleteDatabase(action *actionState) {\n\ts.nodesMutex.Lock()\n\ts.nodes = make([]proxyNodeInfo, 0)\n\ts.nodesMutex.Unlock()\n\n\tfmt.Printf(\"[ProxyFI] DeleteDatabase: cleared all nodes\\n\")\n\taction.Status = StatusSuccess\n}\n\n// executeCreateDatabase resets nodes to initial state with 2 nodes\n// so that all slot-migrate effects can work (remove, remove-add, slot-shuffle need >= 2 nodes)\nfunc (s *ProxyFaultInjectorServer) executeCreateDatabase(action *actionState) {\n\ts.nodesMutex.Lock()\n\t// Get target host/port from the existing proxy or use defaults\n\ttargetHost := \"cae-resp-proxy\"\n\ttargetPort := 6379\n\n\t// Create 2 nodes so all effects can work\n\ts.nodes = []proxyNodeInfo{\n\t\t{listenPort: 7001, targetHost: targetHost, targetPort: targetPort, proxyAddr: \"localhost:7001\", nodeID: \"node-7001\"},\n\t\t{listenPort: 7002, targetHost: targetHost, targetPort: targetPort, proxyAddr: \"localhost:7002\", nodeID: \"node-7002\"},\n\t}\n\ts.nodesMutex.Unlock()\n\n\tfmt.Printf(\"[ProxyFI] CreateDatabase: initialized with 2 nodes\\n\")\n\taction.Status = StatusSuccess\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scenario_endpoint_types_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\tlogs2 \"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// TestEndpointTypesPushNotifications tests push notifications with different endpoint types\nfunc TestEndpointTypesPushNotifications(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)\n\tdefer cancel()\n\n\tvar dump = true\n\tvar errorsDetected = false\n\n\t// Test different endpoint types\n\tendpointTypes := []struct {\n\t\tname         string\n\t\tendpointType maintnotifications.EndpointType\n\t\tdescription  string\n\t}{\n\t\t{\n\t\t\tname:         \"ExternalIP\",\n\t\t\tendpointType: maintnotifications.EndpointTypeExternalIP,\n\t\t\tdescription:  \"External IP endpoint type for enterprise clusters\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ExternalFQDN\",\n\t\t\tendpointType: maintnotifications.EndpointTypeExternalFQDN,\n\t\t\tdescription:  \"External FQDN endpoint type for DNS-based routing\",\n\t\t},\n\t\t{\n\t\t\tname:         \"None\",\n\t\t\tendpointType: maintnotifications.EndpointTypeNone,\n\t\t\tdescription:  \"No endpoint type - reconnect with current config\",\n\t\t},\n\t}\n\n\tdefer func() {\n\t\tlogCollector.Clear()\n\t}()\n\n\t// Test each endpoint type with its own fresh database\n\tfor _, endpointTest := range endpointTypes {\n\t\tt.Run(endpointTest.name, func(t *testing.T) {\n\t\t\t// Setup: Create fresh database and client factory for THIS endpoint type test\n\t\t\tbdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, \"standalone\")\n\t\t\tdefer cleanup()\n\t\t\tt.Logf(\"[ENDPOINT-TYPES-%s] Created test database with bdb_id: %d (mode: %s)\", endpointTest.name, bdbID, testMode.Mode)\n\n\t\t\t// Skip this test if using proxy mock (requires real fault injector)\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\tt.Skip(\"Skipping endpoint type test - requires real fault injector\")\n\t\t\t}\n\n\t\t\t// Create fault injector with cleanup\n\t\t\tfaultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"[ERROR] Failed to create fault injector: %v\", err)\n\t\t\t}\n\t\t\tdefer fiCleanup()\n\n\t\t\t// Get endpoint config from factory (now connected to new database)\n\t\t\tendpointConfig := factory.GetConfig()\n\n\t\t\tdefer func() {\n\t\t\t\tif dump {\n\t\t\t\t\tfmt.Println(\"Pool stats:\")\n\t\t\t\t\tfactory.PrintPoolStats(t)\n\t\t\t\t}\n\t\t\t}()\n\t\t\t// Clear logs between endpoint type tests\n\t\t\tlogCollector.Clear()\n\t\t\t// reset errors detected flag\n\t\t\terrorsDetected = false\n\t\t\t// reset dump flag\n\t\t\tdump = true\n\t\t\t// redefine p and e for each test to get\n\t\t\t// proper test name in logs and proper test failures\n\t\t\tvar p = func(format string, args ...interface{}) {\n\t\t\t\tprintLog(\"ENDPOINT-TYPES\", false, format, args...)\n\t\t\t}\n\n\t\t\tvar e = func(format string, args ...interface{}) {\n\t\t\t\terrorsDetected = true\n\t\t\t\tprintLog(\"ENDPOINT-TYPES\", true, format, args...)\n\t\t\t}\n\n\t\t\tvar ef = func(format string, args ...interface{}) {\n\t\t\t\tprintLog(\"ENDPOINT-TYPES\", true, format, args...)\n\t\t\t\tt.FailNow()\n\t\t\t}\n\n\t\t\tp(\"Testing endpoint type: %s - %s\", endpointTest.name, endpointTest.description)\n\n\t\t\tminIdleConns := 3\n\t\t\tpoolSize := 8\n\t\t\tmaxConnections := 12\n\n\t\t\t// Create Redis client with specific endpoint type\n\t\t\tclient, err := factory.Create(fmt.Sprintf(\"endpoint-test-%s\", endpointTest.name), &CreateClientOptions{\n\t\t\t\tProtocol:       3, // RESP3 required for push notifications\n\t\t\t\tPoolSize:       poolSize,\n\t\t\t\tMinIdleConns:   minIdleConns,\n\t\t\t\tMaxActiveConns: maxConnections,\n\t\t\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\t\t\tHandoffTimeout:             30 * time.Second,\n\t\t\t\t\tRelaxedTimeout:             8 * time.Second,\n\t\t\t\t\tPostHandoffRelaxedDuration: 2 * time.Second,\n\t\t\t\t\tMaxWorkers:                 15,\n\t\t\t\t\tEndpointType:               endpointTest.endpointType, // Test specific endpoint type\n\t\t\t\t},\n\t\t\t\tClientName: fmt.Sprintf(\"endpoint-test-%s\", endpointTest.name),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to create client for %s: %v\", endpointTest.name, err)\n\t\t\t}\n\n\t\t\t// Create timeout tracker\n\t\t\ttracker := NewTrackingNotificationsHook()\n\t\t\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\t\t\tsetupNotificationHooks(client, tracker, logger)\n\t\t\tdefer func() {\n\t\t\t\ttracker.Clear()\n\t\t\t}()\n\n\t\t\t// Verify initial connectivity\n\t\t\terr = client.Ping(ctx).Err()\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to ping Redis with %s endpoint type: %v\", endpointTest.name, err)\n\t\t\t}\n\n\t\t\tp(\"Client connected successfully with %s endpoint type\", endpointTest.name)\n\n\t\t\tcommandsRunner, _ := NewCommandRunner(client)\n\t\t\tdefer func() {\n\t\t\t\tif dump {\n\t\t\t\t\tstats := commandsRunner.GetStats()\n\t\t\t\t\tp(\"%s endpoint stats: Operations: %d, Errors: %d, Timeout Errors: %d\",\n\t\t\t\t\t\tendpointTest.name, stats.Operations, stats.Errors, stats.TimeoutErrors)\n\t\t\t\t}\n\t\t\t\tcommandsRunner.Stop()\n\t\t\t}()\n\n\t\t\t// Test failover with this endpoint type\n\t\t\tp(\"Testing failover with %s endpoint type on database [bdb_id:%s]...\", endpointTest.name, endpointConfig.BdbID)\n\t\t\tfailoverResp, err := faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\tType: \"failover\",\n\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t},\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to trigger failover action for %s: %v\", endpointTest.name, err)\n\t\t\t}\n\n\t\t\t// Start command traffic\n\t\t\tgo func() {\n\t\t\t\tcommandsRunner.FireCommandsUntilStop(ctx)\n\t\t\t}()\n\n\t\t\t// Wait for failover to complete\n\t\t\tstatus, err := faultInjector.WaitForAction(ctx, failoverResp.ActionID,\n\t\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tef(\"[FI] Failover action failed for %s: %v\", endpointTest.name, err)\n\t\t\t}\n\t\t\tp(\"[FI] Failover action completed for %s: %s %s\", endpointTest.name, status.Status, actionOutputIfFailed(status))\n\n\t\t\t// Wait for FAILING_OVER notification\n\t\t\tmatch, found := logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"FAILING_OVER\")\n\t\t\t}, 3*time.Minute)\n\t\t\tif !found {\n\t\t\t\tef(\"FAILING_OVER notification was not received for %s endpoint type\", endpointTest.name)\n\t\t\t}\n\t\t\tfailingOverData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"FAILING_OVER notification received for %s. %v\", endpointTest.name, failingOverData)\n\n\t\t\t// Wait for FAILED_OVER notification\n\t\t\tseqIDToObserve := int64(failingOverData[\"seqID\"].(float64))\n\t\t\tconnIDToObserve := uint64(failingOverData[\"connID\"].(float64))\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn notificationType(s, \"FAILED_OVER\") && connID(s, connIDToObserve) && seqID(s, seqIDToObserve+1)\n\t\t\t}, 3*time.Minute)\n\t\t\tif !found {\n\t\t\t\tef(\"FAILED_OVER notification was not received for %s endpoint type\", endpointTest.name)\n\t\t\t}\n\t\t\tfailedOverData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"FAILED_OVER notification received for %s. %v\", endpointTest.name, failedOverData)\n\n\t\t\t// Test migration with this endpoint type\n\t\t\tp(\"Testing migration with %s endpoint type...\", endpointTest.name)\n\t\t\tmigrateResp, err := faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\tType: \"migrate\",\n\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t},\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to trigger migrate action for %s: %v\", endpointTest.name, err)\n\t\t\t}\n\n\t\t\t// Wait for migration to complete\n\t\t\tstatus, err = faultInjector.WaitForAction(ctx, migrateResp.ActionID,\n\t\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tef(\"[FI] Migrate action failed for %s: %v\", endpointTest.name, err)\n\t\t\t}\n\t\t\tp(\"[FI] Migrate action completed for %s: %s %s\", endpointTest.name, status.Status, actionOutputIfFailed(status))\n\n\t\t\t// Wait for MIGRATING notification\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && strings.Contains(s, \"MIGRATING\")\n\t\t\t}, 60*time.Second)\n\t\t\tif !found {\n\t\t\t\tef(\"MIGRATING notification was not received for %s endpoint type\", endpointTest.name)\n\t\t\t}\n\t\t\tmigrateData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"MIGRATING notification received for %s: %v\", endpointTest.name, migrateData)\n\n\t\t\t// Wait for MIGRATED notification\n\t\t\tseqIDToObserve = int64(migrateData[\"seqID\"].(float64))\n\t\t\tconnIDToObserve = uint64(migrateData[\"connID\"].(float64))\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn notificationType(s, \"MIGRATED\") && connID(s, connIDToObserve) && seqID(s, seqIDToObserve+1)\n\t\t\t}, 3*time.Minute)\n\t\t\tif !found {\n\t\t\t\tef(\"MIGRATED notification was not received for %s endpoint type\", endpointTest.name)\n\t\t\t}\n\t\t\tmigratedData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"MIGRATED notification received for %s. %v\", endpointTest.name, migratedData)\n\n\t\t\t// Complete migration with bind action\n\t\t\t// Pass endpoint_type to the bind action so it knows what format to use\n\t\t\tvar endpointTypeStr string\n\t\t\tswitch endpointTest.endpointType {\n\t\t\tcase maintnotifications.EndpointTypeExternalIP:\n\t\t\t\tendpointTypeStr = \"external-ip\"\n\t\t\tcase maintnotifications.EndpointTypeExternalFQDN:\n\t\t\t\tendpointTypeStr = \"external-fqdn\"\n\t\t\tcase maintnotifications.EndpointTypeNone:\n\t\t\t\tendpointTypeStr = \"none\"\n\t\t\t}\n\n\t\t\tbindResp, err := faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\tType: \"bind\",\n\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\"bdb_id\":        endpointConfig.BdbID,\n\t\t\t\t\t\"endpoint_type\": endpointTypeStr,\n\t\t\t\t},\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to trigger bind action for %s: %v\", endpointTest.name, err)\n\t\t\t}\n\n\t\t\t// Wait for MOVING notification\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"MOVING\")\n\t\t\t}, 3*time.Minute)\n\t\t\tif !found {\n\t\t\t\tef(\"MOVING notification was not received for %s endpoint type\", endpointTest.name)\n\t\t\t}\n\t\t\tmovingData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"MOVING notification received for %s. %v\", endpointTest.name, movingData)\n\n\t\t\tnotification, ok := movingData[\"notification\"].(string)\n\t\t\tif !ok {\n\t\t\t\te(\"invalid notification message\")\n\t\t\t}\n\n\t\t\tnotification = notification[:len(notification)-1]\n\t\t\tnotificationParts := strings.Split(notification, \" \")\n\t\t\taddress := notificationParts[len(notificationParts)-1]\n\n\t\t\tswitch endpointTest.endpointType {\n\t\t\tcase maintnotifications.EndpointTypeExternalFQDN:\n\t\t\t\taddress = strings.Split(address, \":\")[0]\n\t\t\t\taddressParts := strings.SplitN(address, \".\", 2)\n\t\t\t\tif len(addressParts) != 2 {\n\t\t\t\t\te(\"invalid address %s\", address)\n\t\t\t\t} else {\n\t\t\t\t\taddress = addressParts[1]\n\t\t\t\t}\n\n\t\t\t\tvar expectedAddress string\n\t\t\t\thostParts := strings.SplitN(endpointConfig.Host, \".\", 2)\n\t\t\t\tif len(hostParts) != 2 {\n\t\t\t\t\t// Docker proxy setup uses \"localhost\" without domain suffix\n\t\t\t\t\t// In this case, skip FQDN validation\n\t\t\t\t\tp(\"Skipping FQDN validation for Docker proxy setup (host=%s)\", endpointConfig.Host)\n\t\t\t\t} else {\n\t\t\t\t\texpectedAddress = hostParts[1]\n\t\t\t\t\tif address != expectedAddress {\n\t\t\t\t\t\te(\"invalid fqdn, expected: %s, got: %s\", expectedAddress, address)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase maintnotifications.EndpointTypeExternalIP:\n\t\t\t\taddress = strings.Split(address, \":\")[0]\n\t\t\t\tip := net.ParseIP(address)\n\t\t\t\tif ip == nil {\n\t\t\t\t\te(\"invalid message format, expected valid IP, got: %s\", address)\n\t\t\t\t}\n\t\t\tcase maintnotifications.EndpointTypeNone:\n\t\t\t\tif address != internal.RedisNull {\n\t\t\t\t\te(\"invalid endpoint type, expected: %s, got: %s\", internal.RedisNull, address)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Wait for bind to complete\n\t\t\tbindStatus, err := faultInjector.WaitForAction(ctx, bindResp.ActionID,\n\t\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\t\tWithPollInterval(2*time.Second))\n\t\t\tif err != nil {\n\t\t\t\tef(\"Bind action failed for %s: %v\", endpointTest.name, err)\n\t\t\t}\n\t\t\tp(\"Bind action completed for %s: %s %s\", endpointTest.name, bindStatus.Status, actionOutputIfFailed(bindStatus))\n\n\t\t\t// Continue traffic for analysis\n\t\t\ttime.Sleep(60 * time.Second)\n\t\t\tcommandsRunner.Stop()\n\n\t\t\t// Analyze results for this endpoint type\n\t\t\ttrackerAnalysis := tracker.GetAnalysis()\n\t\t\tif trackerAnalysis.NotificationProcessingErrors > 0 {\n\t\t\t\te(\"Notification processing errors with %s endpoint type: %d\", endpointTest.name, trackerAnalysis.NotificationProcessingErrors)\n\t\t\t}\n\n\t\t\tif trackerAnalysis.UnexpectedNotificationCount > 0 {\n\t\t\t\te(\"Unexpected notifications with %s endpoint type: %d\", endpointTest.name, trackerAnalysis.UnexpectedNotificationCount)\n\t\t\t}\n\n\t\t\t// Validate we received all expected notification types\n\t\t\tif trackerAnalysis.FailingOverCount == 0 {\n\t\t\t\te(\"Expected FAILING_OVER notifications with %s endpoint type, got none\", endpointTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.FailedOverCount == 0 {\n\t\t\t\te(\"Expected FAILED_OVER notifications with %s endpoint type, got none\", endpointTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.MigratingCount == 0 {\n\t\t\t\te(\"Expected MIGRATING notifications with %s endpoint type, got none\", endpointTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.MigratedCount == 0 {\n\t\t\t\te(\"Expected MIGRATED notifications with %s endpoint type, got none\", endpointTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.MovingCount == 0 {\n\t\t\t\te(\"Expected MOVING notifications with %s endpoint type, got none\", endpointTest.name)\n\t\t\t}\n\n\t\t\tlogAnalysis := logCollector.GetAnalysis()\n\t\t\tif logAnalysis.TotalHandoffCount == 0 {\n\t\t\t\te(\"Expected at least one handoff with %s endpoint type, got none\", endpointTest.name)\n\t\t\t}\n\t\t\tif logAnalysis.TotalHandoffCount != logAnalysis.SucceededHandoffCount {\n\t\t\t\te(\"Expected all handoffs to succeed with %s endpoint type, got %d failed\", endpointTest.name, logAnalysis.FailedHandoffCount)\n\t\t\t}\n\n\t\t\tif errorsDetected {\n\t\t\t\tlogCollector.DumpLogs()\n\t\t\t\ttrackerAnalysis.Print(t)\n\t\t\t\tlogCollector.Clear()\n\t\t\t\ttracker.Clear()\n\t\t\t\tef(\"[FAIL] Errors detected with %s endpoint type\", endpointTest.name)\n\t\t\t}\n\t\t\tp(\"Endpoint type %s test completed successfully\", endpointTest.name)\n\t\t\tlogCollector.GetAnalysis().Print(t)\n\t\t\ttrackerAnalysis.Print(t)\n\t\t\tlogCollector.Clear()\n\t\t\ttracker.Clear()\n\t\t})\n\t}\n\n\tt.Log(\"All endpoint types tested successfully\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scenario_push_notifications_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tlogs2 \"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// TestPushNotifications tests Redis Enterprise push notifications (MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER)\n// This test now works with BOTH the real fault injector and the proxy mock\nfunc TestPushNotifications(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)\n\tdefer cancel()\n\n\t// Setup: Create fresh database and client factory for this test\n\tbdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, \"standalone\")\n\tdefer cleanup()\n\tt.Logf(\"[PUSH-NOTIFICATIONS] Created test database with bdb_id: %d (mode: %s)\", bdbID, testMode.Mode)\n\n\t// Wait for database to be fully ready (mode-aware)\n\ttime.Sleep(testMode.DatabaseReadyDelay)\n\n\tvar dump = true\n\tvar seqIDToObserve int64\n\tvar connIDToObserve uint64\n\n\tvar match string\n\tvar found bool\n\n\tvar status *ActionStatusResponse\n\tvar bindStatus *ActionStatusResponse\n\tvar movingNotification []interface{}\n\tvar commandsRunner2, commandsRunner3 *CommandRunner\n\tvar errorsDetected = false\n\n\tvar p = func(format string, args ...interface{}) {\n\t\tprintLog(\"PUSH-NOTIFICATIONS\", false, format, args...)\n\t}\n\n\tvar e = func(format string, args ...interface{}) {\n\t\terrorsDetected = true\n\t\tprintLog(\"PUSH-NOTIFICATIONS\", true, format, args...)\n\t}\n\n\tvar ef = func(format string, args ...interface{}) {\n\t\tprintLog(\"PUSH-NOTIFICATIONS\", true, format, args...)\n\t\tt.FailNow()\n\t}\n\n\tlogCollector.ClearLogs()\n\tdefer func() {\n\t\tlogCollector.Clear()\n\t}()\n\n\t// Get endpoint config from factory (now connected to new database)\n\tendpointConfig := factory.GetConfig()\n\n\t// Create notification injector (works with both proxy mock and real FI)\n\tinjector, err := NewNotificationInjector()\n\tif err != nil {\n\t\tef(\"Failed to create notification injector: %v\", err)\n\t}\n\tdefer injector.Stop()\n\n\t// For real fault injector, we also need the FaultInjectorClient for actions\n\tvar faultInjector *FaultInjectorClient\n\tif !testMode.IsProxyMock() {\n\t\tfaultInjector, err = CreateTestFaultInjector()\n\t\tif err != nil {\n\t\t\tef(\"Failed to create fault injector: %v\", err)\n\t\t}\n\t}\n\n\tminIdleConns := 5\n\tpoolSize := 10\n\tmaxConnections := 15\n\t// Create Redis client with push notifications enabled\n\tclient, err := factory.Create(\"push-notification-client\", &CreateClientOptions{\n\t\tProtocol:       3, // RESP3 required for push notifications\n\t\tPoolSize:       poolSize,\n\t\tMinIdleConns:   minIdleConns,\n\t\tMaxActiveConns: maxConnections,\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\tHandoffTimeout:             40 * time.Second, // 30 seconds\n\t\t\tRelaxedTimeout:             10 * time.Second, // 10 seconds relaxed timeout\n\t\t\tPostHandoffRelaxedDuration: 2 * time.Second,  // 2 seconds post-handoff relaxed duration\n\t\t\tMaxWorkers:                 20,\n\t\t\tEndpointType:               maintnotifications.EndpointTypeExternalIP, // Use external IP for enterprise\n\t\t},\n\t\tClientName: \"push-notification-test-client\",\n\t})\n\tif err != nil {\n\t\tef(\"Failed to create client: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tfactory.DestroyAll()\n\t}()\n\n\t// Create timeout tracker\n\ttracker := NewTrackingNotificationsHook()\n\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\tsetupNotificationHooks(client, tracker, logger)\n\tdefer func() {\n\t\ttracker.Clear()\n\t}()\n\n\t// Verify initial connectivity\n\terr = client.Ping(ctx).Err()\n\tif err != nil {\n\t\tef(\"Failed to ping Redis: %v\", err)\n\t}\n\n\tp(\"Client connected successfully, starting push notification test\")\n\n\tcommandsRunner, _ := NewCommandRunner(client)\n\tdefer func() {\n\t\tif dump {\n\t\t\tp(\"Command runner stats:\")\n\t\t\tp(\"Operations: %d, Errors: %d, Timeout Errors: %d\",\n\t\t\t\tcommandsRunner.GetStats().Operations, commandsRunner.GetStats().Errors, commandsRunner.GetStats().TimeoutErrors)\n\t\t}\n\t\tp(\"Stopping command runner...\")\n\t\tcommandsRunner.Stop()\n\t}()\n\n\tp(\"Starting FAILING_OVER / FAILED_OVER notifications test...\")\n\n\t// Mode-aware: Proxy mock directly injects notifications, real FI triggers actions\n\tvar failoverResp *ActionResponse\n\tif testMode.IsProxyMock() {\n\t\t// Proxy mock: Directly inject FAILING_OVER notification\n\t\tp(\"Injecting FAILING_OVER notification (proxy mock mode)...\")\n\t\tif err := injector.InjectFAILING_OVER(ctx, 1000); err != nil {\n\t\t\tef(\"Failed to inject FAILING_OVER: %v\", err)\n\t\t}\n\t\ttime.Sleep(testMode.NotificationDelay)\n\t} else {\n\t\t// Real FI: Trigger failover action to generate FAILING_OVER, FAILED_OVER notifications\n\t\tp(\"Triggering failover action to generate push notifications...\")\n\t\tfailoverResp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\tType: \"failover\",\n\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tef(\"Failed to trigger failover action: %v\", err)\n\t\t}\n\t}\n\tgo func() {\n\t\tp(\"Waiting for FAILING_OVER notification\")\n\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"FAILING_OVER\")\n\t\t}, 3*time.Minute)\n\t\tcommandsRunner.Stop()\n\t}()\n\tcommandsRunner.FireCommandsUntilStop(ctx)\n\tif !found {\n\t\tef(\"FAILING_OVER notification was not received within 3 minutes\")\n\t}\n\tfailingOverData := logs2.ExtractDataFromLogMessage(match)\n\tp(\"FAILING_OVER notification received. %v\", failingOverData)\n\tseqIDToObserve = int64(failingOverData[\"seqID\"].(float64))\n\tconnIDToObserve = uint64(failingOverData[\"connID\"].(float64))\n\n\t// Inject FAILED_OVER in proxy mock mode\n\tif testMode.IsProxyMock() {\n\t\tp(\"Injecting FAILED_OVER notification (proxy mock mode)...\")\n\t\tif err := injector.InjectFAILED_OVER(ctx, 1001); err != nil {\n\t\t\tef(\"Failed to inject FAILED_OVER: %v\", err)\n\t\t}\n\t\ttime.Sleep(testMode.NotificationDelay)\n\t}\n\n\tgo func() {\n\t\t// Note: Redis Enterprise may not send FAILED_OVER with seqID = FAILING_OVER.seqID + 1\n\t\t// We wait for any FAILED_OVER on the same connection\n\t\tp(\"Waiting for FAILED_OVER notification on conn %d...\", connIDToObserve)\n\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"FAILED_OVER\") && connID(s, connIDToObserve)\n\t\t}, 3*time.Minute)\n\t\tcommandsRunner.Stop()\n\t}()\n\tcommandsRunner.FireCommandsUntilStop(ctx)\n\tif !found {\n\t\tef(\"FAILED_OVER notification was not received within 3 minutes\")\n\t}\n\tfailedOverData := logs2.ExtractDataFromLogMessage(match)\n\tp(\"FAILED_OVER notification received. %v\", failedOverData)\n\n\t// Wait for action to complete (real FI only)\n\tif !testMode.IsProxyMock() {\n\t\tstatus, err = faultInjector.WaitForAction(ctx, failoverResp.ActionID,\n\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\tWithPollInterval(2*time.Second),\n\t\t)\n\t\tif err != nil {\n\t\t\tef(\"[FI] Failover action failed: %v\", err)\n\t\t}\n\t\tp(\"Failover action completed: %s %s\", status.Status, actionOutputIfFailed(status))\n\t}\n\n\tp(\"FAILING_OVER / FAILED_OVER notifications test completed successfully\")\n\n\t// Test: Trigger migrate action to generate MOVING, MIGRATING, MIGRATED notifications\n\tvar migrateResp *ActionResponse\n\tif testMode.IsProxyMock() {\n\t\t// Proxy mock: Directly inject MIGRATING notification\n\t\tp(\"Injecting MIGRATING notification (proxy mock mode)...\")\n\t\tif err := injector.InjectMIGRATING(ctx, 2000, 5000); err != nil {\n\t\t\tef(\"Failed to inject MIGRATING: %v\", err)\n\t\t}\n\t\ttime.Sleep(testMode.NotificationDelay)\n\t} else {\n\t\t// Real FI: Trigger migrate action\n\t\tp(\"Triggering migrate action to generate push notifications...\")\n\t\tmigrateResp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\tType: \"migrate\",\n\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tef(\"Failed to trigger migrate action: %v\", err)\n\t\t}\n\t}\n\tgo func() {\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[DEBUG] Goroutine started: waiting for MIGRATING notification...\\n\")\n\t\t}\n\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"MIGRATING\")\n\t\t}, 60*time.Second)\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[DEBUG] Goroutine: MatchOrWaitForLogMatchFunc returned, found=%v, calling Stop()...\\n\", found)\n\t\t}\n\t\tcommandsRunner.Stop()\n\t\tif DebugE2E() {\n\t\t\tfmt.Printf(\"[DEBUG] Goroutine: Stop() returned\\n\")\n\t\t}\n\t}()\n\tif DebugE2E() {\n\t\tfmt.Printf(\"[DEBUG] Main thread: calling FireCommandsUntilStop...\\n\")\n\t}\n\tcommandsRunner.FireCommandsUntilStop(ctx)\n\tif DebugE2E() {\n\t\tfmt.Printf(\"[DEBUG] Main thread: FireCommandsUntilStop returned\\n\")\n\t}\n\tif !found {\n\t\tif !testMode.IsProxyMock() {\n\t\t\tstatus, err = faultInjector.WaitForAction(ctx, migrateResp.ActionID,\n\t\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tef(\"[FI] Migrate action failed: %v\", err)\n\t\t\t}\n\t\t\tp(\"[FI] Migrate action completed: %s %s\", status.Status, actionOutputIfFailed(status))\n\t\t}\n\t\tef(\"MIGRATING notification for migrate action was not received within 60 seconds\")\n\t}\n\tmigrateData := logs2.ExtractDataFromLogMessage(match)\n\tseqIDToObserve = int64(migrateData[\"seqID\"].(float64))\n\tconnIDToObserve = uint64(migrateData[\"connID\"].(float64))\n\tp(\"MIGRATING notification received: seqID: %d, connID: %d\", seqIDToObserve, connIDToObserve)\n\n\t// Wait for action to complete and inject MIGRATED (mode-aware)\n\tif testMode.IsProxyMock() {\n\t\t// Proxy mock: Directly inject MIGRATED notification\n\t\tp(\"Injecting MIGRATED notification (proxy mock mode)...\")\n\t\tif err := injector.InjectMIGRATED(ctx, 2001, 5000); err != nil {\n\t\t\tef(\"Failed to inject MIGRATED: %v\", err)\n\t\t}\n\t\ttime.Sleep(testMode.NotificationDelay)\n\t} else {\n\t\t// Real FI: Wait for action to complete (generates MIGRATED automatically)\n\t\tstatus, err = faultInjector.WaitForAction(ctx, migrateResp.ActionID,\n\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\tWithPollInterval(2*time.Second),\n\t\t)\n\t\tif err != nil {\n\t\t\tef(\"[FI] Migrate action failed: %v\", err)\n\t\t}\n\t\tp(\"[FI] Migrate action completed: %s %s\", status.Status, actionOutputIfFailed(status))\n\t}\n\n\tgo func() {\n\t\t// Note: During a real migration, the connection that received MIGRATING might be closed.\n\t\t// MIGRATED might arrive on a different connection.\n\t\t// We accept any MIGRATED notification rather than requiring same connection/seqID.\n\t\tp(\"Waiting for MIGRATED notification (preferring conn %d with seqID %d)...\", connIDToObserve, seqIDToObserve+1)\n\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"MIGRATED\")\n\t\t}, 3*time.Minute)\n\t\tcommandsRunner.Stop()\n\t}()\n\tcommandsRunner.FireCommandsUntilStop(ctx)\n\tif !found {\n\t\tef(\"MIGRATED notification was not received within 3 minutes\")\n\t}\n\tmigratedData := logs2.ExtractDataFromLogMessage(match)\n\tp(\"MIGRATED notification received. %v\", migratedData)\n\n\tp(\"MIGRATING / MIGRATED notifications test completed successfully\")\n\n\t// Trigger bind action to complete the migration process (or inject MOVING in proxy mock mode)\n\tvar bindResp *ActionResponse\n\tif testMode.IsProxyMock() {\n\t\t// Proxy mock: We need connections to receive the MOVING notification,\n\t\t// but we must stop traffic before handoffs start to avoid data race.\n\t\t// The handoff worker reinitializes connections, which races with CommandRunner.\n\t\tp(\"Starting commands before MOVING injection (proxy mock mode)...\")\n\t\tgo commandsRunner.FireCommandsUntilStop(ctx)\n\t\t// Give commands time to establish connections\n\t\ttime.Sleep(1 * time.Second)\n\n\t\t// Proxy mock: Directly inject MOVING notification\n\t\t// Format: [\"MOVING\", seqID, timeS, endpoint]\n\t\t// timeS is the time in seconds until the connection should be handed off\n\t\tp(\"Injecting MOVING notification (proxy mock mode)...\")\n\t\tif err := injector.InjectMOVING(ctx, 3000, 30, \"\"); err != nil {\n\t\t\tef(\"Failed to inject MOVING: %v\", err)\n\t\t}\n\t\ttime.Sleep(testMode.NotificationDelay)\n\n\t\t// Wait for MOVING notification to be received before stopping CommandRunner\n\t\tp(\"Waiting for MOVING notification to be received (proxy mock mode)...\")\n\t\tmatch, found := logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"MOVING\")\n\t\t}, 30*time.Second)\n\t\tif !found {\n\t\t\tef(\"MOVING notification was not received in proxy mock mode\")\n\t\t}\n\t\tmovingData := logs2.ExtractDataFromLogMessage(match)\n\t\tp(\"MOVING notification received (proxy mock mode). %v\", movingData)\n\n\t\t// Stop command runner before handoffs start to avoid data race\n\t\t// Handoffs are scheduled at timeS/2 = 15 seconds, so stop before that\n\t\tp(\"Stopping command runner before handoffs start (proxy mock mode)...\")\n\t\tcommandsRunner.Stop()\n\t} else {\n\t\t// Real FI: Trigger bind action\n\t\tp(\"Triggering bind action to complete migration...\")\n\t\tbindResp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\tType: \"bind\",\n\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tef(\"Failed to trigger bind action: %v\", err)\n\t\t}\n\t}\n\n\t// Multi-client tests - only run if not using proxy mock\n\tif !testMode.SkipMultiClientTests {\n\t\t// start a second client but don't execute any commands on it\n\t\tp(\"Starting a second client to observe notification during moving...\")\n\t\tclient2, err := factory.Create(\"push-notification-client-2\", &CreateClientOptions{\n\t\t\tProtocol:       3, // RESP3 required for push notifications\n\t\t\tPoolSize:       poolSize,\n\t\t\tMinIdleConns:   minIdleConns,\n\t\t\tMaxActiveConns: maxConnections,\n\t\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\t\tHandoffTimeout:             40 * time.Second, // 30 seconds\n\t\t\t\tRelaxedTimeout:             30 * time.Minute, // 30 minutes relaxed timeout for second client\n\t\t\t\tPostHandoffRelaxedDuration: 2 * time.Second,  // 2 seconds post-handoff relaxed duration\n\t\t\t\tMaxWorkers:                 20,\n\t\t\t\tEndpointType:               maintnotifications.EndpointTypeExternalIP, // Use external IP for enterprise\n\t\t\t},\n\t\t\tClientName: \"push-notification-test-client-2\",\n\t\t})\n\n\t\tif err != nil {\n\t\t\tef(\"failed to create client: %v\", err)\n\t\t}\n\t\t// setup tracking for second client\n\t\ttracker2 := NewTrackingNotificationsHook()\n\t\tlogger2 := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\t\tsetupNotificationHooks(client2, tracker2, logger2)\n\t\tcommandsRunner2, _ = NewCommandRunner(client2)\n\t\tp(\"Second client created\")\n\n\t\t// Use a channel to communicate errors from the goroutine\n\t\terrChan := make(chan error, 1)\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\terrChan <- fmt.Errorf(\"goroutine panic: %v\", r)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tp(\"Waiting for MOVING notification on first client\")\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"MOVING\")\n\t\t\t}, 3*time.Minute)\n\t\t\tcommandsRunner.Stop()\n\t\t\tif !found {\n\t\t\t\terrChan <- fmt.Errorf(\"MOVING notification was not received within 3 minutes ON A FIRST CLIENT\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// once moving is received, start a second client commands runner\n\t\t\tp(\"Starting commands on second client\")\n\t\t\tgo commandsRunner2.FireCommandsUntilStop(ctx)\n\n\t\t\tp(\"Waiting for MOVING notification on second client\")\n\t\t\tmatchNotif, fnd := tracker2.FindOrWaitForNotification(\"MOVING\", 3*time.Minute)\n\t\t\tif !fnd {\n\t\t\t\terrChan <- fmt.Errorf(\"MOVING notification was not received within 3 minutes ON A SECOND CLIENT\")\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tp(\"MOVING notification received on second client %v\", matchNotif)\n\t\t\t}\n\n\t\t\t// Signal success\n\t\t\terrChan <- nil\n\t\t}()\n\t\tcommandsRunner.FireCommandsUntilStop(ctx)\n\t\t// wait for moving on first client\n\t\t// once the commandRunner stops, it means a waiting\n\t\t// on the logCollector match has completed and we can proceed\n\t\tif !found {\n\t\t\tef(\"MOVING notification was not received within 3 minutes\")\n\t\t}\n\t\tmovingData := logs2.ExtractDataFromLogMessage(match)\n\t\tp(\"MOVING notification received. %v\", movingData)\n\t\tseqIDToObserve = int64(movingData[\"seqID\"].(float64))\n\t\tconnIDToObserve = uint64(movingData[\"connID\"].(float64))\n\n\t\ttime.Sleep(3 * time.Second)\n\t\t// start a third client but don't execute any commands on it\n\t\tp(\"Starting a third client to observe notification during moving...\")\n\t\tclient3, err := factory.Create(\"push-notification-test-client-3\", &CreateClientOptions{\n\t\t\tProtocol:       3, // RESP3 required for push notifications\n\t\t\tPoolSize:       poolSize,\n\t\t\tMinIdleConns:   minIdleConns,\n\t\t\tMaxActiveConns: maxConnections,\n\t\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\t\tHandoffTimeout:             40 * time.Second, // 30 seconds\n\t\t\t\tRelaxedTimeout:             30 * time.Minute, // 30 minutes relaxed timeout for second client\n\t\t\t\tPostHandoffRelaxedDuration: 2 * time.Second,  // 2 seconds post-handoff relaxed duration\n\t\t\t\tMaxWorkers:                 20,\n\t\t\t\tEndpointType:               maintnotifications.EndpointTypeExternalIP, // Use external IP for enterprise\n\t\t\t},\n\t\t\tClientName: \"push-notification-test-client-3\",\n\t\t})\n\n\t\tif err != nil {\n\t\t\tef(\"failed to create client: %v\", err)\n\t\t}\n\t\t// setup tracking for third client\n\t\ttracker3 := NewTrackingNotificationsHook()\n\t\tlogger3 := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\t\tsetupNotificationHooks(client3, tracker3, logger3)\n\t\tcommandsRunner3, _ = NewCommandRunner(client3)\n\t\tp(\"Third client created\")\n\t\tgo commandsRunner3.FireCommandsUntilStop(ctx)\n\t\t// wait for moving on third client\n\t\tmovingNotification, found = tracker3.FindOrWaitForNotification(\"MOVING\", 3*time.Minute)\n\t\tif !found {\n\t\t\tp(\"[NOTICE] MOVING notification was not received within 3 minutes ON A THIRD CLIENT\")\n\t\t} else {\n\t\t\tp(\"MOVING notification received on third client. %v\", movingNotification)\n\t\t\tif len(movingNotification) != 4 {\n\t\t\t\tp(\"[NOTICE] Invalid MOVING notification format: %s\", movingNotification)\n\t\t\t}\n\t\t\tmNotifTimeS, ok := movingNotification[2].(int64)\n\t\t\tif !ok {\n\t\t\t\tp(\"[NOTICE] Invalid timeS in MOVING notification: %s\", movingNotification)\n\t\t\t}\n\t\t\t// expect timeS to be less than 15\n\t\t\tif mNotifTimeS < 15 {\n\t\t\t\tp(\"[NOTICE] Expected timeS < 15, got %d\", mNotifTimeS)\n\t\t\t}\n\t\t}\n\t\tcommandsRunner3.Stop()\n\t\tcommandsRunner2.Stop() // Stop commandsRunner2 before the final section\n\t\t// Wait for the goroutine to complete and check for errors\n\t\tif err := <-errChan; err != nil {\n\t\t\tef(\"Second client goroutine error: %v\", err)\n\t\t}\n\n\t\t// Wait for bind action to complete\n\t\tbindStatus, err = faultInjector.WaitForAction(ctx, bindResp.ActionID,\n\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\tWithPollInterval(2*time.Second))\n\t\tif err != nil {\n\t\t\tef(\"Bind action failed: %v\", err)\n\t\t}\n\n\t\tp(\"Bind action completed: %s %s\", bindStatus.Status, actionOutputIfFailed(bindStatus))\n\n\t\tp(\"MOVING notification test completed successfully\")\n\t} else {\n\t\tp(\"Skipping multi-client MOVING test (proxy mock mode)\")\n\t}\n\n\tp(\"Executing commands and collecting logs for analysis... \")\n\n\t// Run commands based on mode\n\tif testMode.SkipMultiClientTests {\n\t\t// Single client mode (proxy mock)\n\t\t// CommandRunner was already stopped before handoffs started (to avoid data race)\n\t\t// Wait for handoffs to complete:\n\t\t// 1. MOVING notification was already processed\n\t\t// 2. Handoff is scheduled at timeS/2 = 15 seconds\n\t\t// 3. Handoff executes\n\t\t// Total: ~18 seconds for handoffs to complete\n\t\tp(\"Waiting for handoffs to complete (proxy mock mode)...\")\n\t\ttime.Sleep(18 * time.Second)\n\n\t\t// Restart command runner to observe post-handoff behavior\n\t\tp(\"Restarting command runner to observe post-handoff behavior...\")\n\t\tcommandsRunner, _ = NewCommandRunner(client)\n\t\tgo commandsRunner.FireCommandsUntilStop(ctx)\n\n\t\t// Run traffic for a bit to observe post-handoff behavior\n\t\ttime.Sleep(5 * time.Second)\n\t\tcommandsRunner.Stop()\n\t} else {\n\t\t// Multi-client mode (real FI)\n\t\tgo commandsRunner.FireCommandsUntilStop(ctx)\n\t\tgo commandsRunner2.FireCommandsUntilStop(ctx)\n\t\tgo commandsRunner3.FireCommandsUntilStop(ctx)\n\t\ttime.Sleep(2 * time.Minute)\n\t\tcommandsRunner.Stop()\n\t\tcommandsRunner2.Stop()\n\t\tcommandsRunner3.Stop()\n\t}\n\n\ttime.Sleep(2 * testMode.NotificationDelay)\n\tallLogsAnalysis := logCollector.GetAnalysis()\n\ttrackerAnalysis := tracker.GetAnalysis()\n\n\tif allLogsAnalysis.TimeoutErrorsCount > 0 {\n\t\te(\"Unexpected timeout errors: %d\", allLogsAnalysis.TimeoutErrorsCount)\n\t}\n\tif trackerAnalysis.UnexpectedNotificationCount > 0 {\n\t\te(\"Unexpected notifications: %d\", trackerAnalysis.UnexpectedNotificationCount)\n\t}\n\tif trackerAnalysis.NotificationProcessingErrors > 0 {\n\t\te(\"Notification processing errors: %d\", trackerAnalysis.NotificationProcessingErrors)\n\t}\n\tif allLogsAnalysis.RelaxedTimeoutCount == 0 {\n\t\te(\"Expected relaxed timeouts, got none\")\n\t}\n\tif allLogsAnalysis.UnrelaxedTimeoutCount == 0 {\n\t\te(\"Expected unrelaxed timeouts, got none\")\n\t}\n\tif allLogsAnalysis.UnrelaxedAfterMoving == 0 {\n\t\te(\"Expected unrelaxed timeouts after moving, got none\")\n\t}\n\tif allLogsAnalysis.RelaxedPostHandoffCount == 0 {\n\t\te(\"Expected relaxed timeouts after post-handoff, got none\")\n\t}\n\t// validate number of connections we do not exceed max connections\n\t// Adjust expected connections based on mode\n\t// During handoffs, new connections are created before old ones are closed,\n\t// so the connection count can temporarily exceed the pool size.\n\t// We use a multiplier to account for this.\n\texpectedMaxConns := int64(maxConnections)\n\tif !testMode.SkipMultiClientTests {\n\t\t// We started three clients, and during handoffs each connection can temporarily\n\t\t// have both old and new connections active. We allow 4x the base connections\n\t\t// to account for handoff overhead across all clients.\n\t\texpectedMaxConns = int64(maxConnections) * 4\n\t} else {\n\t\t// In proxy mock mode, we test with a standalone client,\n\t\t// During handoffs, new connections are created before old ones are closed,\n\t\texpectedMaxConns = int64(maxConnections) * 2\n\t}\n\n\tif allLogsAnalysis.ConnectionCount > expectedMaxConns {\n\t\te(\"Expected no more than %d connections, got %d\", expectedMaxConns, allLogsAnalysis.ConnectionCount)\n\t}\n\n\tif allLogsAnalysis.ConnectionCount < int64(minIdleConns) {\n\t\te(\"Expected at least %d connections, got %d\", minIdleConns, allLogsAnalysis.ConnectionCount)\n\t}\n\n\t// validate logs are present for all connections\n\tfor connID := range trackerAnalysis.connIds {\n\t\tif len(allLogsAnalysis.connLogs[connID]) == 0 {\n\t\t\te(\"No logs found for connection %d\", connID)\n\t\t}\n\t}\n\t// checks are tracker >= logs since the tracker only tracks client1\n\t// logs include all clients (and some of them start logging even before all hooks are setup)\n\t// for example for idle connections if they receive a notification before the hook is setup\n\t// the action (i.e. relaxing timeouts) will be logged, but the notification will not be tracked and maybe wont be logged\n\n\t// validate number of notifications in tracker matches number of notifications in logs\n\t// allow for more moving in the logs since we started a second client\n\tif trackerAnalysis.TotalNotifications > allLogsAnalysis.TotalNotifications {\n\t\te(\"Expected at least %d or more notifications, got %d\", trackerAnalysis.TotalNotifications, allLogsAnalysis.TotalNotifications)\n\t}\n\n\tif trackerAnalysis.MovingCount > allLogsAnalysis.MovingCount {\n\t\te(\"Expected at least %d or more MOVING notifications, got %d\", trackerAnalysis.MovingCount, allLogsAnalysis.MovingCount)\n\t}\n\n\tif trackerAnalysis.MigratingCount > allLogsAnalysis.MigratingCount {\n\t\te(\"Expected at least %d MIGRATING notifications, got %d\", trackerAnalysis.MigratingCount, allLogsAnalysis.MigratingCount)\n\t}\n\n\tif trackerAnalysis.MigratedCount > allLogsAnalysis.MigratedCount {\n\t\te(\"Expected at least %d MIGRATED notifications, got %d\", trackerAnalysis.MigratedCount, allLogsAnalysis.MigratedCount)\n\t}\n\n\tif trackerAnalysis.FailingOverCount > allLogsAnalysis.FailingOverCount {\n\t\te(\"Expected at least %d FAILING_OVER notifications, got %d\", trackerAnalysis.FailingOverCount, allLogsAnalysis.FailingOverCount)\n\t}\n\n\tif trackerAnalysis.FailedOverCount > allLogsAnalysis.FailedOverCount {\n\t\te(\"Expected at least %d FAILED_OVER notifications, got %d\", trackerAnalysis.FailedOverCount, allLogsAnalysis.FailedOverCount)\n\t}\n\n\tif trackerAnalysis.UnexpectedNotificationCount != allLogsAnalysis.UnexpectedCount {\n\t\te(\"Expected %d unexpected notifications, got %d\", trackerAnalysis.UnexpectedNotificationCount, allLogsAnalysis.UnexpectedCount)\n\t}\n\n\t// unrelaxed (and relaxed) after moving wont be tracked by the hook, so we have to exclude it\n\tif trackerAnalysis.UnrelaxedTimeoutCount > allLogsAnalysis.UnrelaxedTimeoutCount-allLogsAnalysis.UnrelaxedAfterMoving {\n\t\te(\"Expected at least %d unrelaxed timeouts, got %d\", trackerAnalysis.UnrelaxedTimeoutCount, allLogsAnalysis.UnrelaxedTimeoutCount-allLogsAnalysis.UnrelaxedAfterMoving)\n\t}\n\tif trackerAnalysis.RelaxedTimeoutCount > allLogsAnalysis.RelaxedTimeoutCount-allLogsAnalysis.RelaxedPostHandoffCount {\n\t\te(\"Expected at least %d relaxed timeouts, got %d\", trackerAnalysis.RelaxedTimeoutCount, allLogsAnalysis.RelaxedTimeoutCount-allLogsAnalysis.RelaxedPostHandoffCount)\n\t}\n\n\t// validate all handoffs succeeded\n\tif allLogsAnalysis.FailedHandoffCount > 0 {\n\t\te(\"Expected no failed handoffs, got %d\", allLogsAnalysis.FailedHandoffCount)\n\t}\n\tif allLogsAnalysis.SucceededHandoffCount == 0 {\n\t\te(\"Expected at least one successful handoff, got none\")\n\t}\n\tif allLogsAnalysis.TotalHandoffCount != allLogsAnalysis.SucceededHandoffCount {\n\t\te(\"Expected total handoffs to match successful handoffs, got %d != %d\", allLogsAnalysis.TotalHandoffCount, allLogsAnalysis.SucceededHandoffCount)\n\t}\n\n\t// no additional retries\n\tif allLogsAnalysis.TotalHandoffRetries != allLogsAnalysis.TotalHandoffCount {\n\t\te(\"Expected no additional handoff retries, got %d\", allLogsAnalysis.TotalHandoffRetries-allLogsAnalysis.TotalHandoffCount)\n\t}\n\n\tif errorsDetected {\n\t\tlogCollector.DumpLogs()\n\t\ttrackerAnalysis.Print(t)\n\t\tlogCollector.Clear()\n\t\ttracker.Clear()\n\t\tef(\"[FAIL] Errors detected in push notification test\")\n\t}\n\n\tp(\"Analysis complete, no errors found\")\n\tallLogsAnalysis.Print(t)\n\ttrackerAnalysis.Print(t)\n\tp(\"Command runner stats:\")\n\tp(\"Operations: %d, Errors: %d, Timeout Errors: %d\",\n\t\tcommandsRunner.GetStats().Operations, commandsRunner.GetStats().Errors, commandsRunner.GetStats().TimeoutErrors)\n\n\tp(\"Push notification test completed successfully\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scenario_stress_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// TestStressPushNotifications tests push notifications under extreme stress conditions\nfunc TestStressPushNotifications(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"[STRESS][SKIP] Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 40*time.Minute)\n\tdefer cancel()\n\n\t// Setup: Create fresh database and client factory for this test\n\tbdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, \"standalone\")\n\tdefer cleanup()\n\tt.Logf(\"[STRESS] Created test database with bdb_id: %d (mode: %s)\", bdbID, testMode.Mode)\n\n\t// Skip this test if using proxy mock (stress test requires real fault injector)\n\tif testMode.IsProxyMock() {\n\t\tt.Skip(\"Skipping stress test - requires real fault injector\")\n\t}\n\n\t// Wait for database to be fully ready (mode-aware)\n\ttime.Sleep(testMode.DatabaseReadyDelay)\n\n\tvar dump = true\n\tvar errorsDetected = false\n\n\tvar p = func(format string, args ...interface{}) {\n\t\tprintLog(\"STRESS\", false, format, args...)\n\t}\n\n\tvar e = func(format string, args ...interface{}) {\n\t\terrorsDetected = true\n\t\tprintLog(\"STRESS\", true, format, args...)\n\t}\n\n\tvar ef = func(format string, args ...interface{}) {\n\t\tprintLog(\"STRESS\", true, format, args...)\n\t\tt.FailNow()\n\t}\n\n\tlogCollector.ClearLogs()\n\tdefer func() {\n\t\tlogCollector.Clear()\n\t}()\n\n\t// Get endpoint config from factory (now connected to new database)\n\tendpointConfig := factory.GetConfig()\n\n\t// Create fault injector\n\tfaultInjector, err := CreateTestFaultInjector()\n\tif err != nil {\n\t\tef(\"Failed to create fault injector: %v\", err)\n\t}\n\n\t// Extreme stress configuration\n\tminIdleConns := 50\n\tpoolSize := 150\n\tmaxConnections := 200\n\tnumClients := 4\n\n\tvar clients []redis.UniversalClient\n\tvar trackers []*TrackingNotificationsHook\n\tvar commandRunners []*CommandRunner\n\n\t// Create multiple clients for extreme stress\n\tfor i := 0; i < numClients; i++ {\n\t\tclient, err := factory.Create(fmt.Sprintf(\"stress-client-%d\", i), &CreateClientOptions{\n\t\t\tProtocol:       3, // RESP3 required for push notifications\n\t\t\tPoolSize:       poolSize,\n\t\t\tMinIdleConns:   minIdleConns,\n\t\t\tMaxActiveConns: maxConnections,\n\t\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\t\tHandoffTimeout:             60 * time.Second, // Longer timeout for stress\n\t\t\t\tRelaxedTimeout:             20 * time.Second, // Longer relaxed timeout\n\t\t\t\tPostHandoffRelaxedDuration: 5 * time.Second,  // Longer post-handoff duration\n\t\t\t\tMaxWorkers:                 50,               // Maximum workers for stress\n\t\t\t\tHandoffQueueSize:           1000,             // Large queue for stress\n\t\t\t\tEndpointType:               maintnotifications.EndpointTypeExternalIP,\n\t\t\t},\n\t\t\tClientName: fmt.Sprintf(\"stress-test-client-%d\", i),\n\t\t})\n\t\tif err != nil {\n\t\t\tef(\"Failed to create stress client %d: %v\", i, err)\n\t\t}\n\t\tclients = append(clients, client)\n\n\t\t// Setup tracking for each client\n\t\ttracker := NewTrackingNotificationsHook()\n\t\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelWarn)) // Minimal logging for stress\n\t\tsetupNotificationHooks(client, tracker, logger)\n\t\ttrackers = append(trackers, tracker)\n\n\t\t// Create command runner for each client\n\t\tcommandRunner, _ := NewCommandRunner(client)\n\t\tcommandRunners = append(commandRunners, commandRunner)\n\t}\n\n\tdefer func() {\n\t\tif dump {\n\t\t\tp(\"Pool stats:\")\n\t\t\tfactory.PrintPoolStats(t)\n\t\t}\n\t\tfor _, runner := range commandRunners {\n\t\t\trunner.Stop()\n\t\t}\n\t\tfactory.DestroyAll()\n\t}()\n\n\t// Verify initial connectivity for all clients\n\tfor i, client := range clients {\n\t\terr = client.Ping(ctx).Err()\n\t\tif err != nil {\n\t\t\tef(\"Failed to ping Redis with stress client %d: %v\", i, err)\n\t\t}\n\t}\n\n\tp(\"All %d stress clients connected successfully\", numClients)\n\n\t// Start extreme traffic load on all clients\n\tvar trafficWg sync.WaitGroup\n\tfor i, runner := range commandRunners {\n\t\ttrafficWg.Add(1)\n\t\tgo func(clientID int, r *CommandRunner) {\n\t\t\tdefer trafficWg.Done()\n\t\t\tp(\"Starting extreme traffic load on stress client %d\", clientID)\n\t\t\tr.FireCommandsUntilStop(ctx)\n\t\t}(i, runner)\n\t}\n\n\t// Wait for traffic to stabilize\n\ttime.Sleep(10 * time.Second)\n\n\t// Trigger multiple concurrent fault injection actions\n\tvar actionWg sync.WaitGroup\n\tvar actionResults []string\n\tvar actionMutex sync.Mutex\n\n\tactions := []struct {\n\t\tname   string\n\t\taction string\n\t\tdelay  time.Duration\n\t}{\n\t\t{\"failover-1\", \"failover\", 0},\n\t\t{\"migrate-1\", \"migrate\", 5 * time.Second},\n\t\t{\"failover-2\", \"failover\", 10 * time.Second},\n\t}\n\n\tp(\"Starting %d concurrent fault injection actions under extreme stress...\", len(actions))\n\n\tfor _, action := range actions {\n\t\tactionWg.Add(1)\n\t\tgo func(actionName, actionType string, delay time.Duration) {\n\t\t\tdefer actionWg.Done()\n\n\t\t\tif delay > 0 {\n\t\t\t\ttime.Sleep(delay)\n\t\t\t}\n\n\t\t\tp(\"Triggering %s action under extreme stress...\", actionName)\n\t\t\tvar resp *ActionResponse\n\t\t\tvar err error\n\n\t\t\tswitch actionType {\n\t\t\tcase \"failover\":\n\t\t\t\tresp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\t\tType: \"failover\",\n\t\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\tcase \"migrate\":\n\t\t\t\tresp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\t\tType: \"migrate\",\n\t\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\te(\"Failed to trigger %s action: %v\", actionName, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Wait for action to complete\n\t\t\tstatus, err := faultInjector.WaitForAction(ctx, resp.ActionID,\n\t\t\t\tWithMaxWaitTime(360*time.Second), // Longer wait time for stress\n\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\te(\"[FI] %s action failed: %v\", actionName, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tactionMutex.Lock()\n\t\t\tactionResults = append(actionResults, fmt.Sprintf(\"%s: %s %s\", actionName, status.Status, actionOutputIfFailed(status)))\n\t\t\tactionMutex.Unlock()\n\n\t\t\tp(\"[FI] %s action completed: %s %s\", actionName, status.Status, actionOutputIfFailed(status))\n\t\t}(action.name, action.action, action.delay)\n\t}\n\n\t// Wait for all actions to complete\n\tactionWg.Wait()\n\n\t// Continue stress for a bit longer\n\tp(\"All fault injection actions completed, continuing stress for 2 more minutes...\")\n\ttime.Sleep(2 * time.Minute)\n\n\t// Stop all command runners\n\tfor _, runner := range commandRunners {\n\t\trunner.Stop()\n\t}\n\ttrafficWg.Wait()\n\n\t// Analyze stress test results\n\tallLogsAnalysis := logCollector.GetAnalysis()\n\ttotalOperations := int64(0)\n\ttotalErrors := int64(0)\n\ttotalTimeoutErrors := int64(0)\n\n\tfor i, runner := range commandRunners {\n\t\tstats := runner.GetStats()\n\t\tp(\"Stress client %d stats: Operations: %d, Errors: %d, Timeout Errors: %d\",\n\t\t\ti, stats.Operations, stats.Errors, stats.TimeoutErrors)\n\t\ttotalOperations += stats.Operations\n\t\ttotalErrors += stats.Errors\n\t\ttotalTimeoutErrors += stats.TimeoutErrors\n\t}\n\n\tp(\"STRESS TEST RESULTS:\")\n\tp(\"Total operations across all clients: %d\", totalOperations)\n\tp(\"Total errors: %d (%.2f%%)\", totalErrors, float64(totalErrors)/float64(totalOperations)*100)\n\tp(\"Total timeout errors: %d (%.2f%%)\", totalTimeoutErrors, float64(totalTimeoutErrors)/float64(totalOperations)*100)\n\tp(\"Total connections used: %d\", allLogsAnalysis.ConnectionCount)\n\n\t// Print action results\n\tactionMutex.Lock()\n\tp(\"Fault injection action results:\")\n\tfor _, result := range actionResults {\n\t\tp(\"  %s\", result)\n\t}\n\tactionMutex.Unlock()\n\n\t// Validate stress test results\n\tif totalOperations < 1000 {\n\t\te(\"Expected at least 1000 operations under stress, got %d\", totalOperations)\n\t}\n\n\t// Allow higher error rates under extreme stress (up to 20%)\n\terrorRate := float64(totalErrors) / float64(totalOperations) * 100\n\tif errorRate > 20.0 {\n\t\te(\"Error rate too high under stress: %.2f%% (max allowed: 20%%)\", errorRate)\n\t}\n\n\t// Validate connection limits weren't exceeded\n\texpectedMaxConnections := int64(numClients * maxConnections)\n\tif allLogsAnalysis.ConnectionCount > expectedMaxConnections {\n\t\te(\"Connection count exceeded limit: %d > %d\", allLogsAnalysis.ConnectionCount, expectedMaxConnections)\n\t}\n\n\t// Validate notifications were processed\n\ttotalTrackerNotifications := int64(0)\n\ttotalProcessingErrors := int64(0)\n\tfor _, tracker := range trackers {\n\t\tanalysis := tracker.GetAnalysis()\n\t\ttotalTrackerNotifications += analysis.TotalNotifications\n\t\ttotalProcessingErrors += analysis.NotificationProcessingErrors\n\t}\n\n\tif totalProcessingErrors > totalTrackerNotifications/10 { // Allow up to 10% processing errors under stress\n\t\te(\"Too many notification processing errors under stress: %d/%d\", totalProcessingErrors, totalTrackerNotifications)\n\t}\n\n\tif errorsDetected {\n\t\tef(\"Errors detected under stress\")\n\t\tlogCollector.DumpLogs()\n\t\tfor i, tracker := range trackers {\n\t\t\tp(\"=== Stress Client %d Analysis ===\", i)\n\t\t\ttracker.GetAnalysis().Print(t)\n\t\t}\n\t\tlogCollector.Clear()\n\t\tfor _, tracker := range trackers {\n\t\t\ttracker.Clear()\n\t\t}\n\t}\n\n\tdump = false\n\tp(\"[SUCCESS] Stress test completed successfully!\")\n\tp(\"Processed %d operations across %d clients with %d connections\",\n\t\ttotalOperations, numClients, allLogsAnalysis.ConnectionCount)\n\tp(\"Error rate: %.2f%%, Notification processing errors: %d/%d\",\n\t\terrorRate, totalProcessingErrors, totalTrackerNotifications)\n\n\t// Print final analysis (summary only for stress tests to reduce output)\n\tallLogsAnalysis.PrintSummary(t)\n\tfor i, tracker := range trackers {\n\t\tp(\"=== Stress Client %d ===\", i)\n\t\ttracker.GetAnalysis().PrintSummary(t)\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scenario_template.go.example",
    "content": "\n\npackage e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/hitless\"\n)\n\n// TestScenarioTemplate is a template for writing scenario tests\n// Copy this file and rename it to scenario_your_test_name.go\nfunc TestScenarioTemplate(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)\n\tdefer cancel()\n\n\t// Step 1: Create client factory from configuration\n\tfactory, err := CreateTestClientFactory(\"enterprise-cluster\") // or \"standalone0\"\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create client factory: %v\", err)\n\t}\n\tdefer factory.DestroyAll()\n\n\t// Step 2: Create fault injector\n\tfaultInjector, err := CreateTestFaultInjector()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create fault injector: %v\", err)\n\t}\n\n\t// Step 3: Create Redis client with hitless upgrades\n\tclient, err := factory.Create(\"scenario-client\", &CreateClientOptions{\n\t\tProtocol: 3,\n\t\tHitlessUpgradeConfig: &hitless.Config{\n\t\t\tMode:           hitless.MaintNotificationsEnabled,\n\t\t\tHandoffTimeout: 30000, // 30 seconds\n\t\t\tRelaxedTimeout: 10000, // 10 seconds\n\t\t\tMaxWorkers:     20,\n\t\t},\n\t\tClientName: \"scenario-test-client\",\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create client: %v\", err)\n\t}\n\n\t// Step 4: Verify initial connectivity\n\terr = client.Ping(ctx).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to ping Redis: %v\", err)\n\t}\n\n\tt.Log(\"Initial setup completed successfully\")\n\n\t// Step 5: Start background operations (optional)\n\tstopCh := make(chan struct{})\n\tdefer close(stopCh)\n\n\tgo func() {\n\t\tcounter := 0\n\t\tticker := time.NewTicker(100 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-stopCh:\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tkey := fmt.Sprintf(\"test-key-%d\", counter)\n\t\t\t\tvalue := fmt.Sprintf(\"test-value-%d\", counter)\n\n\t\t\t\terr := client.Set(ctx, key, value, time.Minute).Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"Background operation failed: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tcounter++\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Step 6: Wait for baseline operations\n\ttime.Sleep(5 * time.Second)\n\n\t// Step 7: Trigger fault injection scenario\n\tt.Log(\"Triggering fault injection scenario...\")\n\n\t// Example: Cluster failover\n\t// resp, err := faultInjector.TriggerClusterFailover(ctx, \"node-1\", false)\n\t// if err != nil {\n\t//     t.Fatalf(\"Failed to trigger failover: %v\", err)\n\t// }\n\n\t// Example: Network latency\n\t// nodes := []string{\"localhost:7001\", \"localhost:7002\"}\n\t// resp, err := faultInjector.SimulateNetworkLatency(ctx, nodes, 100*time.Millisecond, 20*time.Millisecond)\n\t// if err != nil {\n\t//     t.Fatalf(\"Failed to simulate latency: %v\", err)\n\t// }\n\n\t// Example: Complex sequence\n\t// sequence := []SequenceAction{\n\t//     {\n\t//         Type: ActionNetworkLatency,\n\t//         Parameters: map[string]interface{}{\n\t//             \"nodes\":   []string{\"localhost:7001\"},\n\t//             \"latency\": \"50ms\",\n\t//         },\n\t//     },\n\t//     {\n\t//         Type: ActionClusterFailover,\n\t//         Parameters: map[string]interface{}{\n\t//             \"node_id\": \"node-1\",\n\t//             \"force\":   false,\n\t//         },\n\t//         Delay: 10 * time.Second,\n\t//     },\n\t// }\n\t// resp, err := faultInjector.ExecuteSequence(ctx, sequence)\n\t// if err != nil {\n\t//     t.Fatalf(\"Failed to execute sequence: %v\", err)\n\t// }\n\n\t// Step 8: Wait for fault injection to complete\n\t// status, err := faultInjector.WaitForAction(ctx, resp.ActionID,\n\t//     WithMaxWaitTime(240*time.Second),\n\t//     WithPollInterval(2*time.Second))\n\t// if err != nil {\n\t//     t.Fatalf(\"Fault injection failed: %v\", err)\n\t// }\n\t// t.Logf(\"Fault injection completed: %s\", status.Status)\n\n\t// Step 9: Verify client remains operational during and after fault injection\n\ttime.Sleep(10 * time.Second)\n\n\terr = client.Ping(ctx).Err()\n\tif err != nil {\n\t\tt.Errorf(\"Client not responsive after fault injection: %v\", err)\n\t}\n\n\t// Step 10: Perform additional validation\n\ttestKey := \"validation-key\"\n\ttestValue := \"validation-value\"\n\n\terr = client.Set(ctx, testKey, testValue, time.Minute).Err()\n\tif err != nil {\n\t\tt.Errorf(\"Failed to set validation key: %v\", err)\n\t}\n\n\tretrievedValue, err := client.Get(ctx, testKey).Result()\n\tif err != nil {\n\t\tt.Errorf(\"Failed to get validation key: %v\", err)\n\t} else if retrievedValue != testValue {\n\t\tt.Errorf(\"Validation failed: expected %s, got %s\", testValue, retrievedValue)\n\t}\n\n\tt.Log(\"Scenario test completed successfully\")\n}\n\n// Helper functions for common scenario patterns\n\nfunc performContinuousOperations(ctx context.Context, client redis.UniversalClient, workerID int, stopCh <-chan struct{}, errorCh chan<- error) {\n\tticker := time.NewTicker(100 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tcounter := 0\n\tfor {\n\t\tselect {\n\t\tcase <-stopCh:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\tkey := fmt.Sprintf(\"worker_%d_key_%d\", workerID, counter)\n\t\t\tvalue := fmt.Sprintf(\"value_%d\", counter)\n\n\t\t\t// Perform operation with timeout\n\t\t\topCtx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\t\t\terr := client.Set(opCtx, key, value, time.Minute).Err()\n\t\t\tcancel()\n\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase errorCh <- err:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcounter++\n\t\t}\n\t}\n}\n\nfunc validateClusterHealth(ctx context.Context, client redis.UniversalClient) error {\n\t// Basic connectivity test\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\treturn fmt.Errorf(\"ping failed: %w\", err)\n\t}\n\n\t// Test basic operations\n\ttestKey := \"health-check-key\"\n\ttestValue := \"health-check-value\"\n\n\tif err := client.Set(ctx, testKey, testValue, time.Minute).Err(); err != nil {\n\t\treturn fmt.Errorf(\"set operation failed: %w\", err)\n\t}\n\n\tretrievedValue, err := client.Get(ctx, testKey).Result()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get operation failed: %w\", err)\n\t}\n\n\tif retrievedValue != testValue {\n\t\treturn fmt.Errorf(\"value mismatch: expected %s, got %s\", testValue, retrievedValue)\n\t}\n\n\t// Clean up\n\tclient.Del(ctx, testKey)\n\n\treturn nil\n}\n\nfunc waitForStableOperations(ctx context.Context, client redis.UniversalClient, duration time.Duration) error {\n\tdeadline := time.Now().Add(duration)\n\tticker := time.NewTicker(1 * time.Second)\n\tdefer ticker.Stop()\n\n\tfor time.Now().Before(deadline) {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase <-ticker.C:\n\t\t\tif err := validateClusterHealth(ctx, client); err != nil {\n\t\t\t\treturn fmt.Errorf(\"cluster health check failed: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scenario_timeout_configs_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tlogs2 \"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// TestTimeoutConfigurationsPushNotifications tests push notifications with different timeout configurations\n// This test now works with BOTH the real fault injector and the proxy mock\nfunc TestTimeoutConfigurationsPushNotifications(t *testing.T) {\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)\n\tdefer cancel()\n\n\tvar dump = true\n\n\tvar errorsDetected = false\n\tvar p = func(format string, args ...interface{}) {\n\t\tprintLog(\"TIMEOUT-CONFIGS\", false, format, args...)\n\t}\n\n\tvar e = func(format string, args ...interface{}) {\n\t\terrorsDetected = true\n\t\tprintLog(\"TIMEOUT-CONFIGS\", true, format, args...)\n\t}\n\n\t// Test different timeout configurations\n\ttimeoutConfigs := []struct {\n\t\tname                       string\n\t\thandoffTimeout             time.Duration\n\t\trelaxedTimeout             time.Duration\n\t\tpostHandoffRelaxedDuration time.Duration\n\t\tdescription                string\n\t\texpectedBehavior           string\n\t}{\n\t\t{\n\t\t\tname:                       \"Conservative\",\n\t\t\thandoffTimeout:             60 * time.Second,\n\t\t\trelaxedTimeout:             30 * time.Second,\n\t\t\tpostHandoffRelaxedDuration: 2 * time.Minute,\n\t\t\tdescription:                \"Conservative timeouts for stable environments\",\n\t\t\texpectedBehavior:           \"Longer timeouts, fewer timeout errors\",\n\t\t},\n\t\t{\n\t\t\tname:                       \"Aggressive\",\n\t\t\thandoffTimeout:             5 * time.Second,\n\t\t\trelaxedTimeout:             3 * time.Second,\n\t\t\tpostHandoffRelaxedDuration: 1 * time.Second,\n\t\t\tdescription:                \"Aggressive timeouts for fast failover\",\n\t\t\texpectedBehavior:           \"Shorter timeouts, faster recovery\",\n\t\t},\n\t\t{\n\t\t\tname:                       \"HighLatency\",\n\t\t\thandoffTimeout:             90 * time.Second,\n\t\t\trelaxedTimeout:             30 * time.Second,\n\t\t\tpostHandoffRelaxedDuration: 10 * time.Minute,\n\t\t\tdescription:                \"High latency environment timeouts\",\n\t\t\texpectedBehavior:           \"Very long timeouts for high latency networks\",\n\t\t},\n\t}\n\n\tlogCollector.ClearLogs()\n\tdefer func() {\n\t\tlogCollector.Clear()\n\t}()\n\n\t// Test each timeout configuration with its own fresh database\n\tfor _, timeoutTest := range timeoutConfigs {\n\t\tt.Run(timeoutTest.name, func(t *testing.T) {\n\t\t\t// Setup: Create fresh database and client factory for THIS timeout config test\n\t\t\tbdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, \"standalone\")\n\t\t\tdefer cleanup()\n\t\t\tt.Logf(\"[TIMEOUT-CONFIGS-%s] Created test database with bdb_id: %d (mode: %s)\", timeoutTest.name, bdbID, testMode.Mode)\n\n\t\t\t// Get endpoint config from factory (now connected to new database)\n\t\t\tendpointConfig := factory.GetConfig()\n\n\t\t\t// Create notification injector (works with both proxy mock and real FI)\n\t\t\tinjector, err := NewNotificationInjector()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"[ERROR] Failed to create notification injector: %v\", err)\n\t\t\t}\n\t\t\tdefer injector.Stop()\n\n\t\t\t// For real fault injector, we also need the FaultInjectorClient for actions\n\t\t\tvar faultInjector *FaultInjectorClient\n\t\t\tif !testMode.IsProxyMock() {\n\t\t\t\tfaultInjector, err = CreateTestFaultInjector()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"[ERROR] Failed to create fault injector: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tif dump {\n\t\t\t\t\tp(\"Pool stats:\")\n\t\t\t\t\tfactory.PrintPoolStats(t)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\terrorsDetected = false\n\t\t\tvar ef = func(format string, args ...interface{}) {\n\t\t\t\tprintLog(\"TIMEOUT-CONFIGS\", true, format, args...)\n\t\t\t\tt.FailNow()\n\t\t\t}\n\n\t\t\tp(\"Testing timeout configuration: %s - %s\", timeoutTest.name, timeoutTest.description)\n\t\t\tp(\"Expected behavior: %s\", timeoutTest.expectedBehavior)\n\t\t\tp(\"Handoff timeout: %v, Relaxed timeout: %v, Post-handoff duration: %v\",\n\t\t\t\ttimeoutTest.handoffTimeout, timeoutTest.relaxedTimeout, timeoutTest.postHandoffRelaxedDuration)\n\n\t\t\tminIdleConns := 4\n\t\t\tpoolSize := 10\n\t\t\tmaxConnections := 15\n\n\t\t\t// Create Redis client with specific timeout configuration\n\t\t\tclient, err := factory.Create(fmt.Sprintf(\"timeout-test-%s\", timeoutTest.name), &CreateClientOptions{\n\t\t\t\tProtocol:       3, // RESP3 required for push notifications\n\t\t\t\tPoolSize:       poolSize,\n\t\t\t\tMinIdleConns:   minIdleConns,\n\t\t\t\tMaxActiveConns: maxConnections,\n\t\t\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\t\t\tHandoffTimeout:             timeoutTest.handoffTimeout,\n\t\t\t\t\tRelaxedTimeout:             timeoutTest.relaxedTimeout,\n\t\t\t\t\tPostHandoffRelaxedDuration: timeoutTest.postHandoffRelaxedDuration,\n\t\t\t\t\tMaxWorkers:                 20,\n\t\t\t\t\tEndpointType:               maintnotifications.EndpointTypeExternalIP,\n\t\t\t\t},\n\t\t\t\tClientName: fmt.Sprintf(\"timeout-test-%s\", timeoutTest.name),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to create client for %s: %v\", timeoutTest.name, err)\n\t\t\t}\n\n\t\t\t// Create timeout tracker\n\t\t\ttracker := NewTrackingNotificationsHook()\n\t\t\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\t\t\tsetupNotificationHooks(client, tracker, logger)\n\t\t\tdefer func() {\n\t\t\t\ttracker.Clear()\n\t\t\t}()\n\n\t\t\t// Verify initial connectivity\n\t\t\terr = client.Ping(ctx).Err()\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to ping Redis with %s timeout config: %v\", timeoutTest.name, err)\n\t\t\t}\n\n\t\t\tp(\"Client connected successfully with %s timeout configuration\", timeoutTest.name)\n\n\t\t\tcommandsRunner, _ := NewCommandRunner(client)\n\t\t\tdefer func() {\n\t\t\t\tif dump {\n\t\t\t\t\tstats := commandsRunner.GetStats()\n\t\t\t\t\tp(\"%s timeout config stats: Operations: %d, Errors: %d, Timeout Errors: %d\",\n\t\t\t\t\t\ttimeoutTest.name, stats.Operations, stats.Errors, stats.TimeoutErrors)\n\t\t\t\t}\n\t\t\t\tcommandsRunner.Stop()\n\t\t\t}()\n\n\t\t\t// Start command traffic\n\t\t\tgo func() {\n\t\t\t\tcommandsRunner.FireCommandsUntilStop(ctx)\n\t\t\t}()\n\n\t\t\t// Record start time for timeout analysis\n\t\t\ttestStartTime := time.Now()\n\n\t\t\t// Test failover with this timeout configuration\n\t\t\tp(\"Testing failover with %s timeout configuration...\", timeoutTest.name)\n\t\t\tvar failoverResp *ActionResponse\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\t// Proxy mock: Directly inject FAILING_OVER notification\n\t\t\t\tp(\"Injecting FAILING_OVER notification (proxy mock mode)...\")\n\t\t\t\tif err := injector.InjectFAILING_OVER(ctx, 1000); err != nil {\n\t\t\t\t\tef(\"Failed to inject FAILING_OVER: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(testMode.NotificationDelay)\n\t\t\t} else {\n\t\t\t\t// Real FI: Trigger failover action\n\t\t\t\tfailoverResp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\t\tType: \"failover\",\n\t\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tef(\"Failed to trigger failover action for %s: %v\", timeoutTest.name, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Wait for FAILING_OVER notification\n\t\t\tmatch, found := logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"FAILING_OVER\")\n\t\t\t}, 3*time.Minute)\n\t\t\tif !found {\n\t\t\t\tef(\"FAILING_OVER notification was not received for %s timeout config\", timeoutTest.name)\n\t\t\t}\n\t\t\tfailingOverData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"FAILING_OVER notification received for %s. %v\", timeoutTest.name, failingOverData)\n\n\t\t\t// Inject FAILED_OVER in proxy mock mode\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\tp(\"Injecting FAILED_OVER notification (proxy mock mode)...\")\n\t\t\t\tif err := injector.InjectFAILED_OVER(ctx, 1001); err != nil {\n\t\t\t\t\tef(\"Failed to inject FAILED_OVER: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(testMode.NotificationDelay)\n\t\t\t}\n\n\t\t\t// Wait for FAILED_OVER notification\n\t\t\tseqIDToObserve := int64(failingOverData[\"seqID\"].(float64))\n\t\t\tconnIDToObserve := uint64(failingOverData[\"connID\"].(float64))\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn notificationType(s, \"FAILED_OVER\") && connID(s, connIDToObserve) && seqID(s, seqIDToObserve+1)\n\t\t\t}, 3*time.Minute)\n\t\t\tif !found {\n\t\t\t\tef(\"FAILED_OVER notification was not received for %s timeout config\", timeoutTest.name)\n\t\t\t}\n\t\t\tfailedOverData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"FAILED_OVER notification received for %s. %v\", timeoutTest.name, failedOverData)\n\n\t\t\t// Wait for failover to complete (real FI only)\n\t\t\tif !testMode.IsProxyMock() {\n\t\t\t\tstatus, err := faultInjector.WaitForAction(ctx, failoverResp.ActionID,\n\t\t\t\t\tWithMaxWaitTime(180*time.Second),\n\t\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tef(\"[FI] Failover action failed for %s: %v\", timeoutTest.name, err)\n\t\t\t\t}\n\t\t\t\tp(\"[FI] Failover action completed for %s: %s %s\", timeoutTest.name, status.Status, actionOutputIfFailed(status))\n\t\t\t}\n\n\t\t\t// Continue traffic to observe timeout behavior\n\t\t\t// In proxy mock mode, use shorter sleep since notifications are immediate\n\t\t\t// but still need enough time to observe timeout behavior\n\t\t\ttrafficObservationDuration := timeoutTest.relaxedTimeout * 2\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\t// Use 11 seconds for proxy mock - enough to observe timeout behavior\n\t\t\t\ttrafficObservationDuration = 11 * time.Second\n\t\t\t}\n\t\t\tp(\"Continuing traffic for %v to observe timeout behavior...\", trafficObservationDuration)\n\t\t\ttime.Sleep(trafficObservationDuration)\n\n\t\t\t// Test migration to trigger more timeout scenarios\n\t\t\tp(\"Testing migration with %s timeout configuration...\", timeoutTest.name)\n\t\t\tvar migrateResp *ActionResponse\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\t// Proxy mock: Directly inject MIGRATING notification\n\t\t\t\tp(\"Injecting MIGRATING notification (proxy mock mode)...\")\n\t\t\t\tif err := injector.InjectMIGRATING(ctx, 2000, 5000); err != nil {\n\t\t\t\t\tef(\"Failed to inject MIGRATING: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(testMode.NotificationDelay)\n\t\t\t} else {\n\t\t\t\t// Real FI: Trigger migrate action\n\t\t\t\tmigrateResp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\t\tType: \"migrate\",\n\t\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tef(\"Failed to trigger migrate action for %s: %v\", timeoutTest.name, err)\n\t\t\t\t}\n\n\t\t\t\t// Wait for migration to complete\n\t\t\t\tstatus, err := faultInjector.WaitForAction(ctx, migrateResp.ActionID,\n\t\t\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tef(\"[FI] Migrate action failed for %s: %v\", timeoutTest.name, err)\n\t\t\t\t}\n\n\t\t\t\tp(\"[FI] Migrate action completed for %s: %s %s\", timeoutTest.name, status.Status, actionOutputIfFailed(status))\n\t\t\t}\n\n\t\t\t// Wait for MIGRATING notification\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && strings.Contains(s, \"MIGRATING\")\n\t\t\t}, 60*time.Second)\n\t\t\tif !found {\n\t\t\t\tef(\"MIGRATING notification was not received for %s timeout config\", timeoutTest.name)\n\t\t\t}\n\t\t\tmigrateData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"MIGRATING notification received for %s: %v\", timeoutTest.name, migrateData)\n\n\t\t\t// Inject MIGRATED in proxy mock mode\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\tp(\"Injecting MIGRATED notification (proxy mock mode)...\")\n\t\t\t\tif err := injector.InjectMIGRATED(ctx, 2001, 5000); err != nil {\n\t\t\t\t\tef(\"Failed to inject MIGRATED: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(testMode.NotificationDelay)\n\t\t\t}\n\n\t\t\t// do a bind action (or inject MOVING in proxy mock mode)\n\t\t\tvar bindResp *ActionResponse\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\t// Proxy mock: Directly inject MOVING notification\n\t\t\t\t// CommandRunner is still running so connections can receive the notification\n\t\t\t\tp(\"Injecting MOVING notification (proxy mock mode)...\")\n\t\t\t\tif err := injector.InjectMOVING(ctx, 3000, 30, \"\"); err != nil {\n\t\t\t\t\tef(\"Failed to inject MOVING: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(testMode.NotificationDelay)\n\t\t\t} else {\n\t\t\t\t// Real FI: Trigger bind action\n\t\t\t\tbindResp, err = faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\t\tType: \"bind\",\n\t\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tef(\"Failed to trigger bind action for %s: %v\", timeoutTest.name, err)\n\t\t\t\t}\n\t\t\t\tstatus, err := faultInjector.WaitForAction(ctx, bindResp.ActionID,\n\t\t\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tef(\"[FI] Bind action failed for %s: %v\", timeoutTest.name, err)\n\t\t\t\t}\n\t\t\t\tp(\"[FI] Bind action completed for %s: %s %s\", timeoutTest.name, status.Status, actionOutputIfFailed(status))\n\t\t\t}\n\n\t\t\t// waiting for moving notification\n\t\t\tmatch, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, \"MOVING\")\n\t\t\t}, 3*time.Minute)\n\t\t\tif !found {\n\t\t\t\tef(\"MOVING notification was not received for %s timeout config\", timeoutTest.name)\n\t\t\t}\n\n\t\t\tmovingData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"MOVING notification received for %s. %v\", timeoutTest.name, movingData)\n\n\t\t\t// Continue traffic for post-handoff timeout observation\n\t\t\t// In proxy mock mode, use shorter sleep since notifications are immediate\n\t\t\t// but still need enough time to observe post-handoff timeout behavior\n\t\t\tpostHandoffDuration := 1 * time.Minute\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\t// In proxy mock mode, stop the CommandRunner after MOVING is received\n\t\t\t\t// but before handoffs execute to avoid data race.\n\t\t\t\t// The handoff worker reinitializes connections, which races with CommandRunner\n\t\t\t\t// using the same connections.\n\t\t\t\t// Handoffs are scheduled at timeS/2 = 15 seconds.\n\t\t\t\tp(\"Stopping command runner before handoffs execute (proxy mock mode)...\")\n\t\t\t\tcommandsRunner.Stop()\n\n\t\t\t\t// Wait for handoffs to complete\n\t\t\t\tp(\"Waiting for handoffs to complete...\")\n\t\t\t\ttime.Sleep(18 * time.Second) // Wait for handoffs (scheduled at 15s)\n\n\t\t\t\t// Restart command runner to observe post-handoff behavior\n\t\t\t\tp(\"Restarting command runner to observe post-handoff behavior...\")\n\t\t\t\tcommandsRunner, _ = NewCommandRunner(client)\n\t\t\t\tgo commandsRunner.FireCommandsUntilStop(ctx)\n\n\t\t\t\t// Use shorter observation time since we already waited for handoffs\n\t\t\t\tpostHandoffDuration = 5 * time.Second\n\t\t\t}\n\t\t\tp(\"Continuing traffic for %v to observe post-handoff timeout behavior...\", postHandoffDuration)\n\t\t\ttime.Sleep(postHandoffDuration)\n\n\t\t\tcommandsRunner.Stop()\n\t\t\ttestDuration := time.Since(testStartTime)\n\n\t\t\t// Analyze timeout behavior\n\t\t\ttrackerAnalysis := tracker.GetAnalysis()\n\t\t\tlogAnalysis := logCollector.GetAnalysis()\n\t\t\tif trackerAnalysis.NotificationProcessingErrors > 0 {\n\t\t\t\te(\"Notification processing errors with %s timeout config: %d\", timeoutTest.name, trackerAnalysis.NotificationProcessingErrors)\n\t\t\t}\n\n\t\t\t// Validate timeout-specific behavior\n\t\t\tswitch timeoutTest.name {\n\t\t\tcase \"Conservative\":\n\t\t\t\t// In proxy mock mode, the timing is more predictable and we may not see\n\t\t\t\t// the same ratio of relaxed to unrelaxed timeouts as with real FI\n\t\t\t\tif !testMode.IsProxyMock() && trackerAnalysis.UnrelaxedTimeoutCount > trackerAnalysis.RelaxedTimeoutCount {\n\t\t\t\t\te(\"Conservative config should have more relaxed than unrelaxed timeouts, got relaxed=%d, unrelaxed=%d\",\n\t\t\t\t\t\ttrackerAnalysis.RelaxedTimeoutCount, trackerAnalysis.UnrelaxedTimeoutCount)\n\t\t\t\t}\n\t\t\tcase \"Aggressive\":\n\t\t\t\t// Aggressive timeouts should complete faster\n\t\t\t\t// Proxy mock is much faster than real FI, so adjust expectations\n\t\t\t\tmaxDuration := 5 * time.Minute\n\t\t\t\tif testMode.IsProxyMock() {\n\t\t\t\t\tmaxDuration = 2 * time.Minute\n\t\t\t\t}\n\t\t\t\tif testDuration > maxDuration {\n\t\t\t\t\te(\"Aggressive config took too long: %v (max: %v)\", testDuration, maxDuration)\n\t\t\t\t}\n\t\t\t\tif !testMode.IsProxyMock() && logAnalysis.TotalHandoffRetries > logAnalysis.TotalHandoffCount {\n\t\t\t\t\te(\"Expect handoff retries since aggressive timeouts are shorter, got %d retries for %d handoffs\",\n\t\t\t\t\t\tlogAnalysis.TotalHandoffRetries, logAnalysis.TotalHandoffCount)\n\t\t\t\t}\n\t\t\tcase \"HighLatency\":\n\t\t\t\t// High latency config should have very few unrelaxed after moving\n\t\t\t\tif logAnalysis.UnrelaxedAfterMoving > 2 {\n\t\t\t\t\te(\"High latency config should have minimal unrelaxed timeouts after moving, got %d\", logAnalysis.UnrelaxedAfterMoving)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Validate we received expected notifications\n\t\t\tif trackerAnalysis.FailingOverCount == 0 {\n\t\t\t\te(\"Expected FAILING_OVER notifications with %s timeout config, got none\", timeoutTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.FailedOverCount == 0 {\n\t\t\t\te(\"Expected FAILED_OVER notifications with %s timeout config, got none\", timeoutTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.MigratingCount == 0 {\n\t\t\t\te(\"Expected MIGRATING notifications with %s timeout config, got none\", timeoutTest.name)\n\t\t\t}\n\n\t\t\t// Validate timeout counts are reasonable\n\t\t\tif trackerAnalysis.RelaxedTimeoutCount == 0 {\n\t\t\t\te(\"Expected relaxed timeouts with %s config, got none\", timeoutTest.name)\n\t\t\t}\n\n\t\t\tif logAnalysis.SucceededHandoffCount == 0 {\n\t\t\t\te(\"Expected successful handoffs with %s config, got none\", timeoutTest.name)\n\t\t\t}\n\n\t\t\tif errorsDetected {\n\t\t\t\tlogCollector.DumpLogs()\n\t\t\t\ttrackerAnalysis.Print(t)\n\t\t\t\tlogCollector.Clear()\n\t\t\t\ttracker.Clear()\n\t\t\t\tef(\"[FAIL] Errors detected with %s timeout config\", timeoutTest.name)\n\t\t\t}\n\t\t\tp(\"Timeout configuration %s test completed successfully in %v\", timeoutTest.name, testDuration)\n\t\t\tp(\"Command runner stats:\")\n\t\t\tp(\"Operations: %d, Errors: %d, Timeout Errors: %d\",\n\t\t\t\tcommandsRunner.GetStats().Operations, commandsRunner.GetStats().Errors, commandsRunner.GetStats().TimeoutErrors)\n\t\t\tp(\"Relaxed timeouts: %d, Unrelaxed timeouts: %d\", trackerAnalysis.RelaxedTimeoutCount, trackerAnalysis.UnrelaxedTimeoutCount)\n\t\t})\n\n\t\t// Clear logs between timeout configuration tests\n\t\tlogCollector.ClearLogs()\n\t}\n\n\tp(\"All timeout configurations tested successfully\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scenario_tls_configs_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tlogs2 \"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/logging\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// TestTLSConfigurationsPushNotifications tests push notifications with different TLS configurations\nfunc TestTLSConfigurationsPushNotifications(t *testing.T) {\n\tt.Skip(\"Test disabled due to tls environment missing in test environment\")\n\tif os.Getenv(\"E2E_SCENARIO_TESTS\") != \"true\" {\n\t\tt.Skip(\"Scenario tests require E2E_SCENARIO_TESTS=true\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)\n\tdefer cancel()\n\n\tvar dump = true\n\tvar errorsDetected = false\n\tvar p = func(format string, args ...interface{}) {\n\t\tprintLog(\"TLS-CONFIGS\", false, format, args...)\n\t}\n\n\tvar e = func(format string, args ...interface{}) {\n\t\terrorsDetected = true\n\t\tprintLog(\"TLS-CONFIGS\", true, format, args...)\n\t}\n\n\t// Test different TLS configurations\n\t// Note: TLS configuration is typically handled at the Redis connection config level\n\t// This scenario demonstrates the testing pattern for different TLS setups\n\ttlsConfigs := []struct {\n\t\tname        string\n\t\tdescription string\n\t\tskipReason  string\n\t}{\n\t\t{\n\t\t\tname:        \"NoTLS\",\n\t\t\tdescription: \"No TLS encryption (plain text)\",\n\t\t},\n\t\t{\n\t\t\tname:        \"TLSInsecure\",\n\t\t\tdescription: \"TLS with insecure skip verify (testing only)\",\n\t\t},\n\t\t{\n\t\t\tname:        \"TLSSecure\",\n\t\t\tdescription: \"Secure TLS with certificate verification\",\n\t\t\tskipReason:  \"Requires valid certificates in test environment\",\n\t\t},\n\t\t{\n\t\t\tname:        \"TLSMinimal\",\n\t\t\tdescription: \"TLS with minimal version requirements\",\n\t\t},\n\t\t{\n\t\t\tname:        \"TLSStrict\",\n\t\t\tdescription: \"Strict TLS with TLS 1.3 and specific cipher suites\",\n\t\t},\n\t}\n\n\tlogCollector.ClearLogs()\n\tdefer func() {\n\t\tlogCollector.Clear()\n\t}()\n\n\t// Test each TLS configuration with its own fresh database\n\tfor _, tlsTest := range tlsConfigs {\n\t\tt.Run(tlsTest.name, func(t *testing.T) {\n\t\t\t// Setup: Create fresh database and client factory for THIS TLS config test\n\t\t\tbdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, \"standalone\")\n\t\t\tdefer cleanup()\n\t\t\tt.Logf(\"[TLS-CONFIGS-%s] Created test database with bdb_id: %d (mode: %s)\", tlsTest.name, bdbID, testMode.Mode)\n\n\t\t\t// Skip this test if using proxy mock (requires real fault injector)\n\t\t\tif testMode.IsProxyMock() {\n\t\t\t\tt.Skip(\"Skipping TLS config test - requires real fault injector\")\n\t\t\t}\n\n\t\t\t// Get endpoint config from factory (now connected to new database)\n\t\t\tendpointConfig := factory.GetConfig()\n\n\t\t\t// Create fault injector\n\t\t\tfaultInjector, err := CreateTestFaultInjector()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"[ERROR] Failed to create fault injector: %v\", err)\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tif dump {\n\t\t\t\t\tp(\"Pool stats:\")\n\t\t\t\t\tfactory.PrintPoolStats(t)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\terrorsDetected = false\n\t\t\tvar ef = func(format string, args ...interface{}) {\n\t\t\t\tprintLog(\"TLS-CONFIGS\", true, format, args...)\n\t\t\t\tt.FailNow()\n\t\t\t}\n\n\t\t\tif tlsTest.skipReason != \"\" {\n\t\t\t\tt.Skipf(\"Skipping %s: %s\", tlsTest.name, tlsTest.skipReason)\n\t\t\t}\n\n\t\t\tp(\"Testing TLS configuration: %s - %s\", tlsTest.name, tlsTest.description)\n\n\t\t\tminIdleConns := 3\n\t\t\tpoolSize := 8\n\t\t\tmaxConnections := 12\n\n\t\t\t// Create Redis client with specific TLS configuration\n\t\t\t// Note: TLS configuration is handled at the factory/connection level\n\t\t\tclient, err := factory.Create(fmt.Sprintf(\"tls-test-%s\", tlsTest.name), &CreateClientOptions{\n\t\t\t\tProtocol:       3, // RESP3 required for push notifications\n\t\t\t\tPoolSize:       poolSize,\n\t\t\t\tMinIdleConns:   minIdleConns,\n\t\t\t\tMaxActiveConns: maxConnections,\n\t\t\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\t\t\tMode:                       maintnotifications.ModeEnabled,\n\t\t\t\t\tHandoffTimeout:             30 * time.Second,\n\t\t\t\t\tRelaxedTimeout:             10 * time.Second,\n\t\t\t\t\tPostHandoffRelaxedDuration: 2 * time.Second,\n\t\t\t\t\tMaxWorkers:                 15,\n\t\t\t\t\tEndpointType:               maintnotifications.EndpointTypeExternalIP,\n\t\t\t\t},\n\t\t\t\tClientName: fmt.Sprintf(\"tls-test-%s\", tlsTest.name),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\t// Some TLS configurations might fail in test environments\n\t\t\t\tif tlsTest.name == \"TLSSecure\" || tlsTest.name == \"TLSStrict\" {\n\t\t\t\t\tt.Skipf(\"TLS configuration %s failed (expected in test environment): %v\", tlsTest.name, err)\n\t\t\t\t}\n\t\t\t\tef(\"Failed to create client for %s: %v\", tlsTest.name, err)\n\t\t\t}\n\n\t\t\t// Create timeout tracker\n\t\t\ttracker := NewTrackingNotificationsHook()\n\t\t\tlogger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))\n\t\t\tsetupNotificationHooks(client, tracker, logger)\n\t\t\tdefer func() {\n\t\t\t\ttracker.Clear()\n\t\t\t}()\n\n\t\t\t// Verify initial connectivity\n\t\t\terr = client.Ping(ctx).Err()\n\t\t\tif err != nil {\n\t\t\t\tif tlsTest.name == \"TLSSecure\" || tlsTest.name == \"TLSStrict\" {\n\t\t\t\t\tt.Skipf(\"TLS configuration %s ping failed (expected in test environment): %v\", tlsTest.name, err)\n\t\t\t\t}\n\t\t\t\tef(\"Failed to ping Redis with %s TLS config: %v\", tlsTest.name, err)\n\t\t\t}\n\n\t\t\tp(\"Client connected successfully with %s TLS configuration\", tlsTest.name)\n\n\t\t\tcommandsRunner, _ := NewCommandRunner(client)\n\t\t\tdefer func() {\n\t\t\t\tif dump {\n\t\t\t\t\tstats := commandsRunner.GetStats()\n\t\t\t\t\tp(\"%s TLS config stats: Operations: %d, Errors: %d, Timeout Errors: %d\",\n\t\t\t\t\t\ttlsTest.name, stats.Operations, stats.Errors, stats.TimeoutErrors)\n\t\t\t\t}\n\t\t\t\tcommandsRunner.Stop()\n\t\t\t}()\n\n\t\t\t// Start command traffic\n\t\t\tgo func() {\n\t\t\t\tcommandsRunner.FireCommandsUntilStop(ctx)\n\t\t\t}()\n\n\t\t\t// Test migration with this TLS configuration\n\t\t\tp(\"Testing migration with %s TLS configuration...\", tlsTest.name)\n\t\t\tmigrateResp, err := faultInjector.TriggerAction(ctx, ActionRequest{\n\t\t\t\tType: \"migrate\",\n\t\t\t\tParameters: map[string]interface{}{\n\t\t\t\t\t\"bdb_id\": endpointConfig.BdbID,\n\t\t\t\t},\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tef(\"Failed to trigger migrate action for %s: %v\", tlsTest.name, err)\n\t\t\t}\n\n\t\t\t// Wait for MIGRATING notification\n\t\t\tmatch, found := logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {\n\t\t\t\treturn strings.Contains(s, logs2.ProcessingNotificationMessage) && strings.Contains(s, \"MIGRATING\")\n\t\t\t}, 60*time.Second)\n\t\t\tif !found {\n\t\t\t\tef(\"MIGRATING notification was not received for %s TLS config\", tlsTest.name)\n\t\t\t}\n\t\t\tmigrateData := logs2.ExtractDataFromLogMessage(match)\n\t\t\tp(\"MIGRATING notification received for %s: %v\", tlsTest.name, migrateData)\n\n\t\t\t// Wait for migration to complete\n\t\t\tstatus, err := faultInjector.WaitForAction(ctx, migrateResp.ActionID,\n\t\t\t\tWithMaxWaitTime(240*time.Second),\n\t\t\t\tWithPollInterval(2*time.Second),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tef(\"[FI] Migrate action failed for %s: %v\", tlsTest.name, err)\n\t\t\t}\n\t\t\tp(\"[FI] Migrate action completed for %s: %s %s\", tlsTest.name, status.Status, actionOutputIfFailed(status))\n\n\t\t\t// Continue traffic for a bit to observe TLS behavior\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\tcommandsRunner.Stop()\n\n\t\t\t// Analyze results for this TLS configuration\n\t\t\ttrackerAnalysis := tracker.GetAnalysis()\n\t\t\tif trackerAnalysis.NotificationProcessingErrors > 0 {\n\t\t\t\te(\"Notification processing errors with %s TLS config: %d\", tlsTest.name, trackerAnalysis.NotificationProcessingErrors)\n\t\t\t}\n\n\t\t\tif trackerAnalysis.UnexpectedNotificationCount > 0 {\n\t\t\t\te(\"Unexpected notifications with %s TLS config: %d\", tlsTest.name, trackerAnalysis.UnexpectedNotificationCount)\n\t\t\t}\n\n\t\t\t// Validate we received expected notifications\n\t\t\tif trackerAnalysis.FailingOverCount == 0 {\n\t\t\t\te(\"Expected FAILING_OVER notifications with %s TLS config, got none\", tlsTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.FailedOverCount == 0 {\n\t\t\t\te(\"Expected FAILED_OVER notifications with %s TLS config, got none\", tlsTest.name)\n\t\t\t}\n\t\t\tif trackerAnalysis.MigratingCount == 0 {\n\t\t\t\te(\"Expected MIGRATING notifications with %s TLS config, got none\", tlsTest.name)\n\t\t\t}\n\n\t\t\tif errorsDetected {\n\t\t\t\tlogCollector.DumpLogs()\n\t\t\t\ttrackerAnalysis.Print(t)\n\t\t\t\tlogCollector.Clear()\n\t\t\t\ttracker.Clear()\n\t\t\t\tef(\"[FAIL] Errors detected with %s TLS config\", tlsTest.name)\n\t\t\t}\n\t\t\t// TLS-specific validations\n\t\t\tstats := commandsRunner.GetStats()\n\t\t\tswitch tlsTest.name {\n\t\t\tcase \"NoTLS\":\n\t\t\t\t// Plain text should work fine\n\t\t\t\tp(\"Plain text connection processed %d operations\", stats.Operations)\n\t\t\tcase \"TLSInsecure\", \"TLSMinimal\":\n\t\t\t\t// Insecure TLS should work in test environments\n\t\t\t\tp(\"Insecure TLS connection processed %d operations\", stats.Operations)\n\t\t\t\tif stats.Operations == 0 {\n\t\t\t\t\te(\"Expected operations with %s TLS config, got none\", tlsTest.name)\n\t\t\t\t}\n\t\t\tcase \"TLSStrict\":\n\t\t\t\t// Strict TLS might have different performance characteristics\n\t\t\t\tp(\"Strict TLS connection processed %d operations\", stats.Operations)\n\t\t\t}\n\n\t\t\tp(\"TLS configuration %s test completed successfully\", tlsTest.name)\n\t\t})\n\n\t\t// Clear logs between TLS configuration tests\n\t\tlogCollector.ClearLogs()\n\t}\n\n\tp(\"All TLS configurations tested successfully\")\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scenario_unified_injector_test.go",
    "content": "package e2e\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// TestUnifiedInjector_SMIGRATING demonstrates using the unified notification injector\n// This test requires the proxy-based mock to directly inject notifications\nfunc TestUnifiedInjector_SMIGRATING(t *testing.T) {\n\t// Skip if using real fault injector (these tests require proxy mock for direct notification injection)\n\tif os.Getenv(\"FAULT_INJECTOR_URL\") != \"\" {\n\t\tt.Skip(\"Skipping unified injector test - requires proxy mock, not real fault injector\")\n\t}\n\n\tctx := context.Background()\n\n\t// Create notification injector (automatically chooses based on environment)\n\tinjector, err := NewNotificationInjector()\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create notification injector: %v\", err)\n\t}\n\n\t// Start the injector - skip if proxy not available\n\tif err := injector.Start(); err != nil {\n\t\tt.Skipf(\"Skipping test - proxy not available: %v\", err)\n\t}\n\tdefer injector.Stop()\n\n\t// Get test mode configuration\n\ttestMode := injector.GetTestModeConfig()\n\tt.Logf(\"Using %s mode\", testMode.Mode)\n\tt.Logf(\"Cluster addresses: %v\", injector.GetClusterAddrs())\n\n\t// Create cluster client with maintnotifications enabled\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    injector.GetClusterAddrs(),\n\t\tProtocol: 3, // RESP3 required for push notifications\n\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode:           maintnotifications.ModeEnabled,\n\t\t\tRelaxedTimeout: 30 * time.Second,\n\t\t},\n\t})\n\tdefer client.Close()\n\n\t// Set up notification tracking\n\ttracker := NewTrackingNotificationsHook()\n\tsetupNotificationHook(client, tracker)\n\n\t// Verify connection\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect to cluster: %v\", err)\n\t}\n\n\t// Perform some operations to establish connections\n\tfor i := 0; i < 10; i++ {\n\t\tif err := client.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err(); err != nil {\n\t\t\tt.Logf(\"Warning: Failed to set key: %v\", err)\n\t\t}\n\t}\n\n\t// Start a blocking operation in background to keep connection active\n\t// This ensures the proxy has an active connection to send notifications to\n\tblockingDone := make(chan error, 1)\n\tgo func() {\n\t\t// BLPOP with 10 second timeout - keeps connection active\n\t\t_, err := client.BLPop(ctx, 10*time.Second, \"notification-test-list\").Result()\n\t\tblockingDone <- err\n\t}()\n\n\t// Wait for blocking command to start (mode-aware)\n\ttime.Sleep(testMode.ConnectionEstablishDelay)\n\n\t// Inject SMIGRATING notification while connection is active\n\tt.Log(\"Injecting SMIGRATING notification...\")\n\tif err := injector.InjectSMIGRATING(ctx, 12345, \"1000-2000\", \"3000\"); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to inject SMIGRATING: %v\", err)\n\t}\n\n\t// Wait for notification processing (mode-aware)\n\ttime.Sleep(testMode.NotificationDelay)\n\n\t// Verify notification was received\n\tanalysis := tracker.GetAnalysis()\n\tif analysis.SMigratingCount == 0 {\n\t\tt.Errorf(\"[ERROR] Expected to receive SMIGRATING notification, got 0\")\n\t} else {\n\t\tt.Logf(\"✓ Received %d SMIGRATING notification(s)\", analysis.SMigratingCount)\n\t}\n\n\t// Verify operations still work (timeouts should be relaxed)\n\tif err := client.Set(ctx, \"test-key-during-migration\", \"value\", 0).Err(); err != nil {\n\t\tt.Errorf(\"[ERROR] Expected operations to work during migration, got error: %v\", err)\n\t}\n\n\t// Print analysis\n\tanalysis.Print(t)\n\n\tt.Log(\"✓ SMIGRATING test passed\")\n}\n\n// TestUnifiedInjector_SMIGRATED demonstrates SMIGRATED notification handling\n// This test requires the proxy-based mock to directly inject notifications\nfunc TestUnifiedInjector_SMIGRATED(t *testing.T) {\n\t// Skip if using real fault injector (these tests require proxy mock for direct notification injection)\n\tif os.Getenv(\"FAULT_INJECTOR_URL\") != \"\" {\n\t\tt.Skip(\"Skipping unified injector test - requires proxy mock, not real fault injector\")\n\t}\n\n\tctx := context.Background()\n\n\tinjector, err := NewNotificationInjector()\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create notification injector: %v\", err)\n\t}\n\n\t// Start the injector - skip if proxy not available\n\tif err := injector.Start(); err != nil {\n\t\tt.Skipf(\"Skipping test - proxy not available: %v\", err)\n\t}\n\tdefer injector.Stop()\n\n\t// Get test mode configuration\n\ttestMode := injector.GetTestModeConfig()\n\tt.Logf(\"Using %s mode\", testMode.Mode)\n\n\t// Track cluster state reloads\n\tvar reloadCount atomic.Int32\n\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    injector.GetClusterAddrs(),\n\t\tProtocol: 3,\n\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode:           maintnotifications.ModeEnabled,\n\t\t\tRelaxedTimeout: 30 * time.Second,\n\t\t},\n\t})\n\tdefer client.Close()\n\n\t// Set up notification tracking\n\ttracker := NewTrackingNotificationsHook()\n\tsetupNotificationHook(client, tracker)\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect: %v\", err)\n\t}\n\n\t// Set up reload callback on existing nodes\n\tclient.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\treloadCount.Add(1)\n\t\t\t\tt.Logf(\"Cluster state reload triggered for %s, slots: %v\", hostPort, slotRanges)\n\t\t\t})\n\t\t}\n\t\treturn nil\n\t})\n\n\t// Set up reload callback for new nodes\n\tclient.OnNewNode(func(nodeClient *redis.Client) {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\treloadCount.Add(1)\n\t\t\t\tt.Logf(\"Cluster state reload triggered for %s, slots: %v\", hostPort, slotRanges)\n\t\t\t})\n\t\t}\n\t})\n\n\t// Perform some operations to establish connections\n\tfor i := 0; i < 10; i++ {\n\t\tif err := client.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err(); err != nil {\n\t\t\tt.Logf(\"Warning: Failed to set key: %v\", err)\n\t\t}\n\t}\n\n\tinitialReloads := reloadCount.Load()\n\n\t// Start a blocking operation in background to keep connection active\n\tblockingDone := make(chan error, 1)\n\tgo func() {\n\t\t// BLPOP with 10 second timeout - keeps connection active\n\t\t_, err := client.BLPop(ctx, 10*time.Second, \"notification-test-list\").Result()\n\t\tblockingDone <- err\n\t}()\n\n\t// Wait for blocking command to start (mode-aware)\n\ttime.Sleep(testMode.ConnectionEstablishDelay)\n\n\t// Get all node addresses - needed for both modes\n\taddrs := injector.GetClusterAddrs()\n\tvar newNodeAddr string\n\tif len(addrs) >= 4 {\n\t\tnewNodeAddr = addrs[3] // Node 3: 127.0.0.1:17003\n\t} else {\n\t\t// Fallback to first node if we don't have 4 nodes\n\t\tnewNodeAddr = addrs[0]\n\t}\n\n\t// Mode-specific behavior for triggering SMIGRATED\n\tif testMode.IsProxyMock() {\n\t\t// Proxy mock: Directly inject SMIGRATED notification\n\t\tt.Log(\"Injecting SMIGRATED notification to swap node 2 for node 3...\")\n\t\tt.Logf(\"Using new node address: %s\", newNodeAddr)\n\n\t\tif err := injector.InjectSMIGRATED(ctx, 12346, newNodeAddr, \"1000-2000\", \"3000\"); err != nil {\n\t\t\tt.Fatalf(\"[ERROR] Failed to inject SMIGRATED: %v\", err)\n\t\t}\n\t} else {\n\t\t// Real fault injector: Trigger slot migration which generates both SMIGRATING and SMIGRATED\n\t\tt.Log(\"Triggering slot migration (will generate SMIGRATING and SMIGRATED)...\")\n\n\t\t// First inject SMIGRATING (this triggers the actual migration)\n\t\tif err := injector.InjectSMIGRATING(ctx, 12345, \"1000-2000\", \"3000\"); err != nil {\n\t\t\tt.Fatalf(\"[ERROR] Failed to trigger slot migration: %v\", err)\n\t\t}\n\n\t\t// Wait for migration to complete (real FI takes longer)\n\t\tt.Log(\"Waiting for migration to complete...\")\n\t}\n\n\t// Wait for notification processing (mode-aware)\n\ttime.Sleep(testMode.NotificationDelay)\n\n\t// Wait for blocking operation to complete\n\t<-blockingDone\n\n\t// Verify notification was received\n\tanalysis := tracker.GetAnalysis()\n\n\tif testMode.IsProxyMock() {\n\t\t// Proxy mock: SMIGRATED notifications may not always be received\n\t\t// because they're sent to all connections, but the client might not be actively\n\t\t// listening on all of them. This is expected behavior.\n\t\tif analysis.MigratedCount > 0 {\n\t\t\tt.Logf(\"✓ Received %d SMIGRATED notification(s)\", analysis.MigratedCount)\n\t\t} else {\n\t\t\tt.Logf(\"Note: No SMIGRATED notifications received (expected in proxy mock mode)\")\n\t\t}\n\t} else {\n\t\t// Real FI: Should receive both SMIGRATING and SMIGRATED\n\t\tif analysis.MigratingCount == 0 {\n\t\t\tt.Errorf(\"[ERROR] Expected to receive SMIGRATING notification with real FI, got 0\")\n\t\t} else {\n\t\t\tt.Logf(\"✓ Received %d SMIGRATING notification(s)\", analysis.MigratingCount)\n\t\t}\n\n\t\tif analysis.MigratedCount > 0 {\n\t\t\tt.Logf(\"✓ Received %d SMIGRATED notification(s)\", analysis.MigratedCount)\n\t\t} else {\n\t\t\tt.Logf(\"Note: SMIGRATED notification not yet received (migration may still be in progress)\")\n\t\t}\n\t}\n\n\t// Verify cluster state reload callback\n\t// Note: SMIGRATED notifications trigger cluster reload via the endpoint information\n\t// However, in proxy mock mode, the callback might not be triggered because\n\t// the notification is sent to all connections, not just the active one\n\tfinalReloads := reloadCount.Load()\n\tif finalReloads > initialReloads {\n\t\tt.Logf(\"✓ Cluster state reloaded %d time(s)\", finalReloads-initialReloads)\n\t} else {\n\t\tt.Logf(\"Note: Cluster state reload callback not triggered (expected in proxy mock mode)\")\n\t}\n\n\t// Verify the client discovered the new node topology\n\t// After SMIGRATED, the client should reload cluster slots\n\t// Give it a moment to process\n\ttime.Sleep(2 * time.Second)\n\n\t// Count how many nodes the client knows about\n\tvar nodeAddrsMu sync.Mutex\n\tnodeAddrs := make(map[string]bool)\n\tclient.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {\n\t\taddr := nodeClient.Options().Addr\n\t\tnodeAddrsMu.Lock()\n\t\tnodeAddrs[addr] = true\n\t\tnodeAddrsMu.Unlock()\n\t\tt.Logf(\"Client knows about node: %s\", addr)\n\t\treturn nil\n\t})\n\n\t// We should have 3 nodes (0, 1, 3) after the swap\n\tif len(nodeAddrs) < 3 {\n\t\tt.Logf(\"Warning: Expected client to discover 3 nodes after SMIGRATED, got %d\", len(nodeAddrs))\n\t} else {\n\t\tt.Logf(\"✓ Client discovered %d node(s) after SMIGRATED\", len(nodeAddrs))\n\t}\n\n\t// Verify the new node (17003) is in the list\n\tif len(addrs) >= 4 && !nodeAddrs[newNodeAddr] {\n\t\tt.Logf(\"Warning: Client did not discover new node %s\", newNodeAddr)\n\t} else if len(addrs) >= 4 {\n\t\tt.Logf(\"✓ Client discovered new node %s\", newNodeAddr)\n\t}\n\n\t// Verify we can still perform operations after SMIGRATED\n\t// The client should have reloaded cluster topology\n\tsuccessCount := 0\n\tfor i := 0; i < 10; i++ {\n\t\tkey := fmt.Sprintf(\"post-migration-key-%d\", i)\n\t\tif err := client.Set(ctx, key, \"value\", 0).Err(); err == nil {\n\t\t\tsuccessCount++\n\t\t} else {\n\t\t\tt.Logf(\"Warning: Failed to set key after SMIGRATED: %v\", err)\n\t\t}\n\t}\n\n\tif successCount < 8 {\n\t\tt.Errorf(\"[ERROR] Expected most operations to succeed after SMIGRATED, got %d/10\", successCount)\n\t} else {\n\t\tt.Logf(\"✓ Successfully performed %d/10 operations after SMIGRATED\", successCount)\n\t}\n\n\t// Print analysis\n\tanalysis.Print(t)\n\n\tt.Log(\"✓ SMIGRATED test passed\")\n}\n\n// TestUnifiedInjector_ComplexScenario demonstrates a complex migration scenario\n// This test requires the proxy-based mock to directly inject notifications\nfunc TestUnifiedInjector_ComplexScenario(t *testing.T) {\n\t// Skip if using real fault injector (these tests require proxy mock for direct notification injection)\n\tif os.Getenv(\"FAULT_INJECTOR_URL\") != \"\" {\n\t\tt.Skip(\"Skipping unified injector test - requires proxy mock, not real fault injector\")\n\t}\n\n\tctx := context.Background()\n\n\tinjector, err := NewNotificationInjector()\n\tif err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to create notification injector: %v\", err)\n\t}\n\n\t// Start the injector - skip if proxy not available\n\tif err := injector.Start(); err != nil {\n\t\tt.Skipf(\"Skipping test - proxy not available: %v\", err)\n\t}\n\tdefer injector.Stop()\n\n\t// Get test mode configuration\n\ttestMode := injector.GetTestModeConfig()\n\tt.Logf(\"Using %s mode\", testMode.Mode)\n\n\tvar reloadCount atomic.Int32\n\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    injector.GetClusterAddrs(),\n\t\tProtocol: 3,\n\t})\n\tdefer client.Close()\n\n\ttracker := NewTrackingNotificationsHook()\n\tsetupNotificationHook(client, tracker)\n\n\tclient.OnNewNode(func(nodeClient *redis.Client) {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\treloadCount.Add(1)\n\t\t\t})\n\t\t}\n\t})\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to connect: %v\", err)\n\t}\n\n\t// Perform operations\n\tfor i := 0; i < 20; i++ {\n\t\tclient.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0)\n\t}\n\n\t// Wait for connections to establish (mode-aware)\n\ttime.Sleep(testMode.ConnectionEstablishDelay)\n\n\t// Simulate a multi-step migration scenario\n\tt.Log(\"Step 1: Injecting SMIGRATING for slots 0-5000...\")\n\tif err := injector.InjectSMIGRATING(ctx, 10001, \"0-5000\"); err != nil {\n\t\tt.Fatalf(\"[ERROR] Failed to inject SMIGRATING: %v\", err)\n\t}\n\n\t// Wait for notification processing (mode-aware)\n\ttime.Sleep(testMode.NotificationDelay)\n\n\t// Verify operations work during migration\n\tfor i := 0; i < 5; i++ {\n\t\tif err := client.Set(ctx, fmt.Sprintf(\"migration-key%d\", i), \"value\", 0).Err(); err != nil {\n\t\t\tt.Logf(\"Warning: Operation failed during migration: %v\", err)\n\t\t}\n\t}\n\n\tif testMode.IsProxyMock() {\n\t\t// Only inject SMIGRATED with mock injector\n\t\tt.Log(\"Step 2: Injecting SMIGRATED for completed migration...\")\n\t\taddrs := injector.GetClusterAddrs()\n\t\thostPort := addrs[0]\n\n\t\tif err := injector.InjectSMIGRATED(ctx, 10002, hostPort, \"0-5000\"); err != nil {\n\t\t\tt.Fatalf(\"[ERROR] Failed to inject SMIGRATED: %v\", err)\n\t\t}\n\n\t\t// Wait for notification processing (mode-aware)\n\t\ttime.Sleep(testMode.NotificationDelay)\n\t}\n\n\t// Verify operations still work\n\tfor i := 0; i < 5; i++ {\n\t\tif err := client.Set(ctx, fmt.Sprintf(\"post-migration-key%d\", i), \"value\", 0).Err(); err != nil {\n\t\t\tt.Errorf(\"[ERROR] Operations failed after migration: %v\", err)\n\t\t}\n\t}\n\n\t// Print final analysis\n\tanalysis := tracker.GetAnalysis()\n\tanalysis.Print(t)\n\n\tt.Logf(\"✓ Complex scenario test passed\")\n\tt.Logf(\"  - SMIGRATING notifications: %d\", analysis.MigratingCount)\n\tt.Logf(\"  - SMIGRATED notifications: %d\", analysis.MigratedCount)\n\tt.Logf(\"  - Cluster state reloads: %d\", reloadCount.Load())\n}\n"
  },
  {
    "path": "maintnotifications/e2e/scripts/run-e2e-tests.sh",
    "content": "#!/bin/bash\n\n# Maintenance Notifications E2E Tests Runner\n# This script sets up the environment and runs the maintnotifications upgrade E2E tests\n\nset -euo pipefail\n\n# Script directory and repository root\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"${SCRIPT_DIR}/../../..\" && pwd)\"\nE2E_DIR=\"${REPO_ROOT}/maintnotifications/e2e\"\n\n# Configuration\nFAULT_INJECTOR_URL=\"http://127.0.0.1:20324\"\nCONFIG_PATH=\"${REPO_ROOT}/maintnotifications/e2e/infra/cae-client-testing/config/endpoints.json\"\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# Logging functions\nlog_info() {\n    echo -e \"${BLUE}[INFO]${NC} $1\" >&2\n}\n\nlog_success() {\n    echo -e \"${GREEN}[SUCCESS]${NC} $1\" >&2\n}\n\nlog_warning() {\n    echo -e \"${YELLOW}[WARNING]${NC} $1\" >&2\n}\n\nlog_error() {\n    echo -e \"${RED}[ERROR]${NC} $1\" >&2\n}\n\n# Help function\nshow_help() {\n    cat << EOF\nMaintenance Notifications E2E Tests Runner\n\nUsage: $0 [OPTIONS]\n\nOPTIONS:\n    -h, --help              Show this help message\n    -v, --verbose           Enable verbose test output (human-readable)\n    -d, --debug             Enable debug logging (shows detailed fault injector responses)\n    -t, --timeout DURATION  Test timeout (default: 90m)\n    -r, --run PATTERN       Run only tests matching pattern\n    --json                  Enable JSON output (default)\n    --no-json               Disable JSON output, use verbose human-readable format\n    --cluster               Run only cluster tests (tests with 'Cluster' in name)\n    --single                Run only non-cluster tests (exclude tests with 'Cluster' in name)\n    --dry-run               Show what would be executed without running\n    --list                  List available tests\n    --config PATH           Override config path (default: infra/cae-client-testing/endpoints.json)\n    --fault-injector URL    Override fault injector URL (default: http://127.0.0.1:20324)\n\nEXAMPLES:\n    $0                                    # Run all E2E tests with JSON output\n    $0 --no-json                         # Run all tests with verbose human-readable output\n    $0 --debug                           # Run with debug logging enabled\n    $0 --cluster                         # Run only cluster tests (OSS Cluster API)\n    $0 --single                          # Run only non-cluster tests\n    $0 -r TestPushNotifications          # Run only tests matching pattern\n    $0 -t 45m                            # Run with 45 minute timeout\n    $0 --dry-run                         # Show what would be executed\n    $0 --list                            # List available tests\n\nENVIRONMENT:\n    The script automatically sets up the required environment variables:\n    - REDIS_ENDPOINTS_CONFIG_PATH: Path to Redis endpoints configuration\n    - FAULT_INJECTION_API_URL: URL of the fault injector server\n    - E2E_SCENARIO_TESTS: Enables scenario tests\n    - E2E_DEBUG: Enables debug logging (set by --debug flag)\n\nEOF\n}\n\n# Parse command line arguments\nTIMEOUT=\"90m\"\nRUN_PATTERN=\"\"\nSKIP_PATTERN=\"\"\nDRY_RUN=false\nLIST_TESTS=false\nJSON_OUTPUT=true\nCLUSTER_ONLY=false\nSINGLE_ONLY=false\nDEBUG_MODE=false\n\nwhile [[ $# -gt 0 ]]; do\n    case $1 in\n        -h|--help)\n            show_help\n            exit 0\n            ;;\n        -v|--verbose)\n            # Verbose is now the same as --no-json for backward compatibility\n            JSON_OUTPUT=false\n            shift\n            ;;\n        -d|--debug)\n            DEBUG_MODE=true\n            shift\n            ;;\n        -t|--timeout)\n            TIMEOUT=\"$2\"\n            shift 2\n            ;;\n        -r|--run)\n            RUN_PATTERN=\"$2\"\n            shift 2\n            ;;\n        --json)\n            JSON_OUTPUT=true\n            shift\n            ;;\n        --no-json)\n            JSON_OUTPUT=false\n            shift\n            ;;\n        --cluster)\n            CLUSTER_ONLY=true\n            shift\n            ;;\n        --single)\n            SINGLE_ONLY=true\n            shift\n            ;;\n        --dry-run)\n            DRY_RUN=true\n            shift\n            ;;\n        --list)\n            LIST_TESTS=true\n            shift\n            ;;\n        --config)\n            CONFIG_PATH=\"$2\"\n            shift 2\n            ;;\n        --fault-injector)\n            FAULT_INJECTOR_URL=\"$2\"\n            shift 2\n            ;;\n        *)\n            log_error \"Unknown option: $1\"\n            show_help\n            exit 1\n            ;;\n    esac\ndone\n\n# Validate mutually exclusive options\nif [[ \"$CLUSTER_ONLY\" == true && \"$SINGLE_ONLY\" == true ]]; then\n    log_error \"Cannot use --cluster and --single together\"\n    exit 1\nfi\n\n# Set run pattern based on cluster/single options\nif [[ \"$CLUSTER_ONLY\" == true ]]; then\n    if [[ -n \"$RUN_PATTERN\" ]]; then\n        log_warning \"Overriding -r pattern with --cluster\"\n    fi\n    RUN_PATTERN=\"Cluster\"\nelif [[ \"$SINGLE_ONLY\" == true ]]; then\n    if [[ -n \"$RUN_PATTERN\" ]]; then\n        log_warning \"Overriding -r pattern with --single\"\n    fi\n    # Use -skip to exclude Cluster tests (requires Go 1.21+)\n    SKIP_PATTERN=\"Cluster\"\nfi\n\n# Validate configuration file exists\nif [[ ! -f \"$CONFIG_PATH\" ]]; then\n    log_error \"Configuration file not found: $CONFIG_PATH\"\n    log_info \"Please ensure the endpoints.json file exists at the specified path\"\n    exit 1\nfi\n\n# Set up environment variables\nexport REDIS_ENDPOINTS_CONFIG_PATH=\"$CONFIG_PATH\"\nexport FAULT_INJECTION_API_URL=\"$FAULT_INJECTOR_URL\"\nexport E2E_SCENARIO_TESTS=\"true\"\nif [[ \"$DEBUG_MODE\" == true ]]; then\n    export E2E_DEBUG=\"true\"\nfi\n\n# Build test command\nTEST_CMD=\"go test -tags=e2e\"\n\n# Add JSON or verbose output\nif [[ \"$JSON_OUTPUT\" == true ]]; then\n    TEST_CMD=\"$TEST_CMD -json\"\nelse\n    TEST_CMD=\"$TEST_CMD -v\"\nfi\n\nif [[ -n \"$TIMEOUT\" ]]; then\n    TEST_CMD=\"$TEST_CMD -timeout=$TIMEOUT\"\nfi\n\nif [[ -n \"$RUN_PATTERN\" ]]; then\n    TEST_CMD=\"$TEST_CMD -run '$RUN_PATTERN'\"\nfi\n\nif [[ -n \"$SKIP_PATTERN\" ]]; then\n    TEST_CMD=\"$TEST_CMD -skip '$SKIP_PATTERN'\"\nfi\n\nTEST_CMD=\"$TEST_CMD ./maintnotifications/e2e/\"\n\n# List tests if requested\nif [[ \"$LIST_TESTS\" == true ]]; then\n    log_info \"Available E2E tests:\"\n    cd \"$REPO_ROOT\"\n    go test -tags=e2e ./maintnotifications/e2e/ -list=. | grep -E \"^Test\" | sort\n    exit 0\nfi\n\n# Show configuration\nlog_info \"Maintenance notifications E2E Tests Configuration:\"\necho \"  Repository Root: $REPO_ROOT\" >&2\necho \"  E2E Directory: $E2E_DIR\" >&2\necho \"  Config Path: $CONFIG_PATH\" >&2\necho \"  Fault Injector URL: $FAULT_INJECTOR_URL\" >&2\necho \"  Test Timeout: $TIMEOUT\" >&2\necho \"  JSON Output: $JSON_OUTPUT\" >&2\necho \"  Debug Mode: $DEBUG_MODE\" >&2\nif [[ -n \"$RUN_PATTERN\" ]]; then\n    echo \"  Test Pattern: $RUN_PATTERN\" >&2\nfi\nif [[ -n \"$SKIP_PATTERN\" ]]; then\n    echo \"  Skip Pattern: $SKIP_PATTERN\" >&2\nfi\necho \"\" >&2\n\n# Validate fault injector connectivity\nlog_info \"Checking fault injector connectivity...\"\nif command -v curl >/dev/null 2>&1; then\n    if curl -s --connect-timeout 5 \"$FAULT_INJECTOR_URL/health\" >/dev/null 2>&1; then\n        log_success \"Fault injector is accessible at $FAULT_INJECTOR_URL\"\n    else\n        log_warning \"Cannot connect to fault injector at $FAULT_INJECTOR_URL\"\n        log_warning \"Tests may fail if fault injection is required\"\n    fi\nelse\n    log_warning \"curl not available, skipping fault injector connectivity check\"\nfi\n\n# Show what would be executed in dry-run mode\nif [[ \"$DRY_RUN\" == true ]]; then\n    log_info \"Dry run mode - would execute:\"\n    echo \"  cd $REPO_ROOT\" >&2\n    echo \"  export REDIS_ENDPOINTS_CONFIG_PATH=\\\"$CONFIG_PATH\\\"\" >&2\n    echo \"  export FAULT_INJECTION_API_URL=\\\"$FAULT_INJECTOR_URL\\\"\" >&2\n    echo \"  export E2E_SCENARIO_TESTS=\\\"true\\\"\" >&2\n    if [[ \"$DEBUG_MODE\" == true ]]; then\n        echo \"  export E2E_DEBUG=\\\"true\\\"\" >&2\n    fi\n    echo \"  $TEST_CMD\" >&2\n    exit 0\nfi\n\n# Change to repository root\ncd \"$REPO_ROOT\"\n\n# Run the tests\nlog_info \"Starting E2E tests...\"\nlog_info \"Command: $TEST_CMD\"\necho \"\" >&2\n\nif eval \"$TEST_CMD\"; then\n    echo \"\" >&2\n    log_success \"All E2E tests completed successfully!\"\n    exit 0\nelse\n    echo \"\" >&2\n    log_error \"E2E tests failed!\"\n    log_info \"Check the test output above for details\"\n    exit 1\nfi\n"
  },
  {
    "path": "maintnotifications/e2e/slot_keys_test.go",
    "content": "package e2e\n\nimport \"strings\"\n\n// extractSlotsFromEvent extracts slot ranges from a notification event\nfunc extractSlotsFromEvent(event DiagnosticsEvent) string {\n\tif event.Details == nil {\n\t\treturn \"\"\n\t}\n\tnotification, ok := event.Details[\"notification\"].([]interface{})\n\tif !ok || len(notification) < 3 {\n\t\treturn \"\"\n\t}\n\n\tswitch event.Type {\n\tcase \"SMIGRATING\":\n\t\t// SMIGRATING format: [type, seqID, slot1, slot2, ...]\n\t\tvar slots []string\n\t\tfor i := 2; i < len(notification); i++ {\n\t\t\tif slot, ok := notification[i].(string); ok {\n\t\t\t\tslots = append(slots, slot)\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(slots, \",\")\n\n\tcase \"SMIGRATED\":\n\t\t// SMIGRATED format: [type, seqID, [[source, target, slots], ...]]\n\t\ttriplets, ok := notification[2].([]interface{})\n\t\tif !ok {\n\t\t\treturn \"\"\n\t\t}\n\t\tvar allSlots []string\n\t\tfor _, t := range triplets {\n\t\t\ttriplet, ok := t.([]interface{})\n\t\t\tif !ok || len(triplet) < 3 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif slots, ok := triplet[2].(string); ok {\n\t\t\t\tallSlots = append(allSlots, slots)\n\t\t\t}\n\t\t}\n\t\treturn strings.Join(allSlots, \";\")\n\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// slotKeys is a precomputed lookup table mapping each slot (0-16383) to a key that hashes to it.\n// These keys were precomputed from maintnotifications/e2e/infra/all_slots.txt.\nvar slotKeys = [16384]string{\n\t\"{3560}\", \"{22179}\", \"{48756}\", \"{2977}\", \"{4569}\", \"{460}\", \"{4384}\", \"{41432}\", \"{46920}\", \"{9073}\",\n\t\"{2281}\", \"{15129}\", \"{5465}\", \"{18928}\", \"{5288}\", \"{4872}\", \"{4883}\", \"{5279}\", \"{40322}\", \"{5494}\",\n\t\"{23669}\", \"{2270}\", \"{6915}\", \"{49046}\", \"{50482}\", \"{1407}\", \"{491}\", \"{4598}\", \"{2986}\", \"{42942}\",\n\t\"{7819}\", \"{3591}\", \"{13280}\", \"{4128}\", \"{41073}\", \"{18072}\", \"{14289}\", \"{3121}\", \"{11316}\", \"{9929}\",\n\t\"{40792}\", \"{5024}\", \"{12561}\", \"{104856}\", \"{9432}\", \"{83909}\", \"{15568}\", \"{8825}\", \"{2631}\", \"{15599}\",\n\t\"{49407}\", \"{10406}\", \"{5638}\", \"{12590}\", \"{13987}\", \"{40763}\", \"{108747}\", \"{8322}\", \"{15882}\", \"{14278}\",\n\t\"{1046}\", \"{41082}\", \"{21848}\", \"{13271}\", \"{11423}\", \"{26968}\", \"{49835}\", \"{3614}\", \"{13358}\", \"{1782}\",\n\t\"{714}\", \"{68536}\", \"{42131}\", \"{7687}\", \"{9307}\", \"{2718}\", \"{12254}\", \"{78296}\", \"{24308}\", \"{5711}\",\n\t\"{19057}\", \"{12448}\", \"{69226}\", \"{21266}\", \"{49332}\", \"{10333}\", \"{2104}\", \"{53181}\", \"{79586}\", \"{13544}\",\n\t\"{1773}\", \"{108}\", \"{6197}\", \"{43621}\", \"{3008}\", \"{8417}\", \"{18306}\", \"{13719}\", \"{45862}\", \"{355}\",\n\t\"{48063}\", \"{7930}\", \"{3255}\", \"{43291}\", \"{63873}\", \"{1835}\", \"{5350}\", \"{24749}\", \"{89275}\", \"{42570}\",\n\t\"{2359}\", \"{9746}\", \"{3952}\", \"{43996}\", \"{42581}\", \"{14}\", \"{12009}\", \"{852}\", \"{17196}\", \"{41800}\",\n\t\"{28858}\", \"{60818}\", \"{8056}\", \"{3449}\", \"{5857}\", \"{45893}\", \"{549}\", \"{1332}\", \"{40821}\", \"{464}\",\n\t\"{4380}\", \"{289}\", \"{3564}\", \"{57293}\", \"{3389}\", \"{2973}\", \"{5461}\", \"{24078}\", \"{66830}\", \"{4876}\",\n\t\"{2468}\", \"{9077}\", \"{2285}\", \"{29879}\", \"{29888}\", \"{2274}\", \"{6911}\", \"{2499}\", \"{4887}\", \"{44843}\",\n\t\"{1918}\", \"{5490}\", \"{2982}\", \"{3378}\", \"{43551}\", \"{3595}\", \"{278}\", \"{1403}\", \"{495}\", \"{51891}\",\n\t\"{46693}\", \"{3125}\", \"{11312}\", \"{48313}\", \"{13284}\", \"{68207}\", \"{13469}\", \"{18076}\", \"{9436}\", \"{2029}\",\n\t\"{42600}\", \"{8821}\", \"{12388}\", \"{5020}\", \"{12565}\", \"{104852}\", \"{69517}\", \"{12594}\", \"{13983}\", \"{12379}\",\n\t\"{2635}\", \"{47183}\", \"{27949}\", \"{10402}\", \"{1042}\", \"{639}\", \"{100830}\", \"{13275}\", \"{3739}\", \"{8326}\",\n\t\"{15886}\", \"{43110}\", \"{41742}\", \"{1786}\", \"{710}\", \"{4619}\", \"{11427}\", \"{48426}\", \"{22209}\", \"{3610}\",\n\t\"{12250}\", \"{20869}\", \"{54790}\", \"{5715}\", \"{15259}\", \"{7683}\", \"{9303}\", \"{58386}\", \"{8908}\", \"{10337}\",\n\t\"{2100}\", \"{23519}\", \"{19053}\", \"{40052}\", \"{5109}\", \"{21262}\", \"{6193}\", \"{14549}\", \"{59496}\", \"{8413}\",\n\t\"{79582}\", \"{13540}\", \"{1777}\", \"{55080}\", \"{48067}\", \"{7934}\", \"{3251}\", \"{22648}\", \"{18302}\", \"{41303}\",\n\t\"{4258}\", \"{351}\", \"{89271}\", \"{6838}\", \"{43963}\", \"{9742}\", \"{63877}\", \"{1831}\", \"{5354}\", \"{45390}\",\n\t\"{40413}\", \"{856}\", \"{17192}\", \"{5548}\", \"{3956}\", \"{43992}\", \"{23158}\", \"{10}\", \"{5853}\", \"{45897}\",\n\t\"{19909}\", \"{1336}\", \"{14108}\", \"{88561}\", \"{8052}\", \"{47901}\", \"{2464}\", \"{56393}\", \"{2289}\", \"{3873}\",\n\t\"{41921}\", \"{18920}\", \"{973}\", \"{12128}\", \"{3568}\", \"{8177}\", \"{3385}\", \"{28979}\", \"{1213}\", \"{468}\",\n\t\"{67930}\", \"{285}\", \"{274}\", \"{45943}\", \"{499}\", \"{4590}\", \"{28988}\", \"{3374}\", \"{7811}\", \"{3599}\",\n\t\"{24668}\", \"{982}\", \"{1914}\", \"{50991}\", \"{3882}\", \"{2278}\", \"{42451}\", \"{2495}\", \"{12384}\", \"{69307}\",\n\t\"{12569}\", \"{19176}\", \"{47793}\", \"{2025}\", \"{10212}\", \"{49213}\", \"{13288}\", \"{1652}\", \"{13465}\", \"{105952}\",\n\t\"{8536}\", \"{3129}\", \"{43700}\", \"{9921}\", \"{3735}\", \"{46083}\", \"{26849}\", \"{11502}\", \"{68417}\", \"{635}\",\n\t\"{12883}\", \"{13279}\", \"{2639}\", \"{9226}\", \"{14986}\", \"{42010}\", \"{5630}\", \"{12598}\", \"{101930}\", \"{12375}\",\n\t\"{10527}\", \"{49526}\", \"{23309}\", \"{2710}\", \"{40642}\", \"{19643}\", \"{21472}\", \"{5719}\", \"{14359}\", \"{6783}\",\n\t\"{8203}\", \"{59286}\", \"{13350}\", \"{21969}\", \"{55690}\", \"{1167}\", \"{1196}\", \"{41152}\", \"{4009}\", \"{100}\",\n\t\"{9808}\", \"{11237}\", \"{3000}\", \"{22419}\", \"{78482}\", \"{12440}\", \"{5105}\", \"{54180}\", \"{7093}\", \"{15449}\",\n\t\"{58596}\", \"{9513}\", \"{19202}\", \"{40203}\", \"{5358}\", \"{17782}\", \"{49167}\", \"{6834}\", \"{2351}\", \"{23748}\",\n\t\"{62977}\", \"{13711}\", \"{1526}\", \"{44290}\", \"{88371}\", \"{7938}\", \"{42863}\", \"{8642}\", \"{2856}\", \"{42892}\",\n\t\"{22058}\", \"{3441}\", \"{41513}\", \"{18512}\", \"{541}\", \"{4448}\", \"{15008}\", \"{89461}\", \"{9152}\", \"{46801}\",\n\t\"{4953}\", \"{44997}\", \"{18809}\", \"{5544}\", \"{5469}\", \"{18924}\", \"{977}\", \"{40532}\", \"{2460}\", \"{23079}\",\n\t\"{49656}\", \"{3877}\", \"{1217}\", \"{19828}\", \"{4388}\", \"{281}\", \"{47820}\", \"{8173}\", \"{3381}\", \"{14029}\",\n\t\"{22769}\", \"{3370}\", \"{7815}\", \"{48146}\", \"{270}\", \"{4379}\", \"{41222}\", \"{4594}\", \"{3886}\", \"{43842}\",\n\t\"{6919}\", \"{2491}\", \"{51582}\", \"{986}\", \"{1910}\", \"{5498}\", \"{15389}\", \"{2021}\", \"{10216}\", \"{8829}\",\n\t\"{12380}\", \"{5028}\", \"{40173}\", \"{19172}\", \"{8532}\", \"{82809}\", \"{14468}\", \"{9925}\", \"{41692}\", \"{1656}\",\n\t\"{13461}\", \"{105956}\", \"{4738}\", \"{631}\", \"{12887}\", \"{41663}\", \"{3731}\", \"{14499}\", \"{48507}\", \"{11506}\",\n\t\"{5634}\", \"{40182}\", \"{20948}\", \"{12371}\", \"{109647}\", \"{9222}\", \"{14982}\", \"{15378}\", \"{12258}\", \"{19647}\",\n\t\"{21476}\", \"{69436}\", \"{10523}\", \"{27868}\", \"{48935}\", \"{2714}\", \"{13354}\", \"{79396}\", \"{718}\", \"{1163}\",\n\t\"{43031}\", \"{6787}\", \"{8207}\", \"{3618}\", \"{48232}\", \"{11233}\", \"{3004}\", \"{52081}\", \"{1192}\", \"{13548}\",\n\t\"{68326}\", \"{104}\", \"{7097}\", \"{42721}\", \"{2108}\", \"{9517}\", \"{78486}\", \"{12444}\", \"{5101}\", \"{24518}\",\n\t\"{49163}\", \"{6830}\", \"{2355}\", \"{42391}\", \"{19206}\", \"{1839}\", \"{44962}\", \"{17786}\", \"{88375}\", \"{43470}\",\n\t\"{3259}\", \"{8646}\", \"{62973}\", \"{13715}\", \"{1522}\", \"{359}\", \"{13109}\", \"{18516}\", \"{545}\", \"{40900}\",\n\t\"{2852}\", \"{42896}\", \"{43481}\", \"{3445}\", \"{4957}\", \"{44993}\", \"{24159}\", \"{5540}\", \"{29958}\", \"{61918}\",\n\t\"{9156}\", \"{18}\", \"{1768}\", \"{113}\", \"{1185}\", \"{41141}\", \"{3013}\", \"{27378}\", \"{48225}\", \"{11224}\",\n\t\"{5116}\", \"{54193}\", \"{25918}\", \"{12453}\", \"{28119}\", \"{9500}\", \"{7080}\", \"{10328}\", \"{26468}\", \"{2703}\",\n\t\"{10534}\", \"{49535}\", \"{21461}\", \"{69421}\", \"{40651}\", \"{19650}\", \"{8210}\", \"{29609}\", \"{11438}\", \"{6790}\",\n\t\"{55683}\", \"{1174}\", \"{13343}\", \"{1799}\", \"{11088}\", \"{3452}\", \"{2845}\", \"{42881}\", \"{552}\", \"{1329}\",\n\t\"{41500}\", \"{18501}\", \"{9141}\", \"{28558}\", \"{3949}\", \"{49768}\", \"{45593}\", \"{5557}\", \"{4940}\", \"{849}\",\n\t\"{44975}\", \"{17791}\", \"{19211}\", \"{40210}\", \"{2342}\", \"{10798}\", \"{49174}\", \"{6827}\", \"{1535}\", \"{44283}\",\n\t\"{62964}\", \"{13702}\", \"{29248}\", \"{8651}\", \"{48078}\", \"{11079}\", \"{16159}\", \"{4583}\", \"{267}\", \"{45950}\",\n\t\"{7802}\", \"{48151}\", \"{8778}\", \"{3367}\", \"{1907}\", \"{19338}\", \"{4898}\", \"{991}\", \"{9099}\", \"{2486}\",\n\t\"{3891}\", \"{7519}\", \"{49641}\", \"{3860}\", \"{2477}\", \"{9068}\", \"{960}\", \"{4869}\", \"{41932}\", \"{18933}\",\n\t\"{3396}\", \"{8789}\", \"{6209}\", \"{8164}\", \"{18428}\", \"{296}\", \"{1200}\", \"{20619}\", \"{15899}\", \"{11511}\",\n\t\"{3726}\", \"{8339}\", \"{12890}\", \"{16518}\", \"{18098}\", \"{626}\", \"{14995}\", \"{42003}\", \"{7158}\", \"{9235}\",\n\t\"{19779}\", \"{12366}\", \"{5623}\", \"{21548}\", \"{17208}\", \"{19165}\", \"{12397}\", \"{19788}\", \"{10201}\", \"{22838}\",\n\t\"{9429}\", \"{2036}\", \"{13476}\", \"{18069}\", \"{20258}\", \"{1641}\", \"{43713}\", \"{9932}\", \"{8525}\", \"{6648}\",\n\t\"{3017}\", \"{8408}\", \"{6188}\", \"{11220}\", \"{68335}\", \"{117}\", \"{1181}\", \"{16229}\", \"{7669}\", \"{9504}\",\n\t\"{7084}\", \"{42732}\", \"{5112}\", \"{21279}\", \"{19048}\", \"{12457}\", \"{21465}\", \"{69425}\", \"{17539}\", \"{19654}\",\n\t\"{9318}\", \"{2707}\", \"{10530}\", \"{7698}\", \"{20569}\", \"{1170}\", \"{13347}\", \"{18758}\", \"{8214}\", \"{6179}\",\n\t\"{43022}\", \"{6794}\", \"{556}\", \"{40913}\", \"{5848}\", \"{18505}\", \"{8049}\", \"{3456}\", \"{2841}\", \"{42885}\",\n\t\"{17189}\", \"{5553}\", \"{4944}\", \"{19409}\", \"{9145}\", \"{7228}\", \"{42373}\", \"{43989}\", \"{2346}\", \"{9759}\",\n\t\"{49170}\", \"{6823}\", \"{44971}\", \"{17795}\", \"{19215}\", \"{17178}\", \"{6538}\", \"{8655}\", \"{65969}\", \"{43463}\",\n\t\"{1531}\", \"{16699}\", \"{18319}\", \"{13706}\", \"{7806}\", \"{29588}\", \"{2999}\", \"{3363}\", \"{41231}\", \"{4587}\",\n\t\"{263}\", \"{1418}\", \"{10058}\", \"{2482}\", \"{3895}\", \"{28269}\", \"{1903}\", \"{50986}\", \"{51591}\", \"{995}\",\n\t\"{964}\", \"{40521}\", \"{41936}\", \"{18937}\", \"{28298}\", \"{3864}\", \"{2473}\", \"{26718}\", \"{67927}\", \"{292}\",\n\t\"{1204}\", \"{50281}\", \"{3392}\", \"{2968}\", \"{29579}\", \"{8160}\", \"{12894}\", \"{41670}\", \"{1059}\", \"{622}\",\n\t\"{38188}\", \"{11515}\", \"{3722}\", \"{27449}\", \"{13998}\", \"{12362}\", \"{5627}\", \"{40191}\", \"{14991}\", \"{10419}\",\n\t\"{28628}\", \"{9231}\", \"{10205}\", \"{39698}\", \"{26359}\", \"{2032}\", \"{40160}\", \"{19161}\", \"{12393}\", \"{13969}\",\n\t\"{11309}\", \"{9936}\", \"{8521}\", \"{29138}\", \"{13472}\", \"{24939}\", \"{41681}\", \"{1645}\", \"{69235}\", \"{21275}\",\n\t\"{19044}\", \"{17329}\", \"{2117}\", \"{9508}\", \"{7088}\", \"{10320}\", \"{1760}\", \"{20379}\", \"{18148}\", \"{13557}\",\n\t\"{6769}\", \"{8404}\", \"{6184}\", \"{43632}\", \"{8218}\", \"{3607}\", \"{11430}\", \"{6798}\", \"{707}\", \"{68525}\",\n\t\"{16439}\", \"{1791}\", \"{9314}\", \"{7079}\", \"{42122}\", \"{7694}\", \"{21469}\", \"{5702}\", \"{12247}\", \"{19658}\",\n\t\"{9149}\", \"{2556}\", \"{3941}\", \"{43985}\", \"{17185}\", \"{41813}\", \"{4948}\", \"{841}\", \"{8045}\", \"{6328}\",\n\t\"{43273}\", \"{42889}\", \"{16089}\", \"{1321}\", \"{5844}\", \"{18509}\", \"{45871}\", \"{346}\", \"{18315}\", \"{16078}\",\n\t\"{3246}\", \"{8659}\", \"{48070}\", \"{7923}\", \"{5343}\", \"{17799}\", \"{19219}\", \"{1826}\", \"{7438}\", \"{9755}\",\n\t\"{64869}\", \"{42563}\", \"{40331}\", \"{5487}\", \"{4890}\", \"{999}\", \"{6906}\", \"{28488}\", \"{3899}\", \"{2263}\",\n\t\"{482}\", \"{51886}\", \"{50491}\", \"{1414}\", \"{11158}\", \"{3582}\", \"{2995}\", \"{29369}\", \"{29398}\", \"{2964}\",\n\t\"{3573}\", \"{27618}\", \"{4397}\", \"{41421}\", \"{1208}\", \"{473}\", \"{2292}\", \"{3868}\", \"{28479}\", \"{9060}\",\n\t\"{968}\", \"{4861}\", \"{5476}\", \"{51381}\", \"{39088}\", \"{10415}\", \"{2622}\", \"{26549}\", \"{13994}\", \"{40770}\",\n\t\"{34889}\", \"{12583}\", \"{15891}\", \"{11519}\", \"{29728}\", \"{8331}\", \"{12898}\", \"{13262}\", \"{1055}\", \"{41091}\",\n\t\"{41060}\", \"{18061}\", \"{13293}\", \"{1649}\", \"{11305}\", \"{38798}\", \"{27259}\", \"{3132}\", \"{12572}\", \"{25839}\",\n\t\"{40781}\", \"{5037}\", \"{10209}\", \"{8836}\", \"{9421}\", \"{28038}\", \"{2113}\", \"{26278}\", \"{49325}\", \"{10324}\",\n\t\"{13848}\", \"{21271}\", \"{19040}\", \"{40041}\", \"{29019}\", \"{8400}\", \"{6180}\", \"{11228}\", \"{1764}\", \"{55093}\",\n\t\"{1189}\", \"{13553}\", \"{703}\", \"{1178}\", \"{41751}\", \"{1795}\", \"{27568}\", \"{3603}\", \"{11434}\", \"{48435}\",\n\t\"{54783}\", \"{5706}\", \"{12243}\", \"{78281}\", \"{9310}\", \"{28709}\", \"{10538}\", \"{7690}\", \"{17181}\", \"{41817}\",\n\t\"{40400}\", \"{845}\", \"{10188}\", \"{2552}\", \"{3945}\", \"{43981}\", \"{44493}\", \"{1325}\", \"{5840}\", \"{45884}\",\n\t\"{8041}\", \"{29458}\", \"{2849}\", \"{48668}\", \"{3242}\", \"{11698}\", \"{48074}\", \"{7927}\", \"{1539}\", \"{342}\",\n\t\"{18311}\", \"{41310}\", \"{28348}\", \"{9751}\", \"{49178}\", \"{10179}\", \"{5347}\", \"{45383}\", \"{63864}\", \"{1822}\",\n\t\"{6902}\", \"{49051}\", \"{9678}\", \"{2267}\", \"{17059}\", \"{5483}\", \"{4894}\", \"{44850}\", \"{8199}\", \"{3586}\",\n\t\"{2991}\", \"{6419}\", \"{486}\", \"{18238}\", \"{5998}\", \"{1410}\", \"{4393}\", \"{5969}\", \"{40832}\", \"{477}\",\n\t\"{48741}\", \"{2960}\", \"{3577}\", \"{8168}\", \"{19528}\", \"{4865}\", \"{5472}\", \"{21719}\", \"{2296}\", \"{9689}\",\n\t\"{7309}\", \"{9064}\", \"{13990}\", \"{17418}\", \"{19198}\", \"{12587}\", \"{14999}\", \"{10411}\", \"{2626}\", \"{9239}\",\n\t\"{18679}\", \"{13266}\", \"{1051}\", \"{20448}\", \"{15895}\", \"{43103}\", \"{6058}\", \"{8335}\", \"{11301}\", \"{23938}\",\n\t\"{8529}\", \"{3136}\", \"{16308}\", \"{18065}\", \"{13297}\", \"{18688}\", \"{42613}\", \"{8832}\", \"{9425}\", \"{7748}\",\n\t\"{12576}\", \"{19169}\", \"{21358}\", \"{5033}\", \"{22069}\", \"{3470}\", \"{2867}\", \"{48646}\", \"{570}\", \"{4479}\",\n\t\"{41522}\", \"{4294}\", \"{9163}\", \"{46830}\", \"{15039}\", \"{2391}\", \"{18838}\", \"{5575}\", \"{4962}\", \"{5398}\",\n\t\"{5369}\", \"{4993}\", \"{5584}\", \"{40232}\", \"{2360}\", \"{23779}\", \"{49156}\", \"{6805}\", \"{1517}\", \"{50592}\",\n\t\"{4488}\", \"{581}\", \"{42852}\", \"{2896}\", \"{3481}\", \"{7909}\", \"{4038}\", \"{131}\", \"{18162}\", \"{41163}\",\n\t\"{3031}\", \"{14399}\", \"{9839}\", \"{11206}\", \"{5134}\", \"{40682}\", \"{104946}\", \"{12471}\", \"{83819}\", \"{9522}\",\n\t\"{8935}\", \"{15478}\", \"{15489}\", \"{2721}\", \"{10516}\", \"{49517}\", \"{12480}\", \"{5728}\", \"{40673}\", \"{13897}\",\n\t\"{8232}\", \"{108657}\", \"{14368}\", \"{15992}\", \"{41192}\", \"{1156}\", \"{13361}\", \"{21958}\", \"{26878}\", \"{11533}\",\n\t\"{3704}\", \"{49925}\", \"{1692}\", \"{13248}\", \"{68426}\", \"{604}\", \"{7797}\", \"{42021}\", \"{2608}\", \"{9217}\",\n\t\"{78386}\", \"{12344}\", \"{5601}\", \"{24218}\", \"{12558}\", \"{19147}\", \"{21376}\", \"{69336}\", \"{10223}\", \"{49222}\",\n\t\"{53091}\", \"{2014}\", \"{13454}\", \"{79496}\", \"{25508}\", \"{1663}\", \"{43731}\", \"{6087}\", \"{8507}\", \"{3118}\",\n\t\"{13609}\", \"{18216}\", \"{245}\", \"{45972}\", \"{7820}\", \"{48173}\", \"{43381}\", \"{3345}\", \"{1925}\", \"{63963}\",\n\t\"{24659}\", \"{5240}\", \"{42460}\", \"{89365}\", \"{9656}\", \"{2249}\", \"{43886}\", \"{3842}\", \"{2455}\", \"{42491}\",\n\t\"{942}\", \"{12119}\", \"{41910}\", \"{17086}\", \"{60908}\", \"{28948}\", \"{3559}\", \"{8146}\", \"{45983}\", \"{5947}\",\n\t\"{1222}\", \"{459}\", \"{574}\", \"{40931}\", \"{399}\", \"{4290}\", \"{57383}\", \"{3474}\", \"{2863}\", \"{3299}\",\n\t\"{24168}\", \"{5571}\", \"{4966}\", \"{66920}\", \"{9167}\", \"{29}\", \"{29969}\", \"{2395}\", \"{2364}\", \"{29998}\",\n\t\"{2589}\", \"{6801}\", \"{44953}\", \"{4997}\", \"{5580}\", \"{1808}\", \"{3268}\", \"{2892}\", \"{3485}\", \"{43441}\",\n\t\"{1513}\", \"{368}\", \"{51981}\", \"{585}\", \"{3035}\", \"{46783}\", \"{48203}\", \"{11202}\", \"{68317}\", \"{135}\",\n\t\"{18166}\", \"{13579}\", \"{2139}\", \"{9526}\", \"{8931}\", \"{42710}\", \"{5130}\", \"{12298}\", \"{104942}\", \"{12475}\",\n\t\"{12484}\", \"{69407}\", \"{12269}\", \"{13893}\", \"{47093}\", \"{2725}\", \"{10512}\", \"{27859}\", \"{729}\", \"{1152}\",\n\t\"{13365}\", \"{100920}\", \"{8236}\", \"{3629}\", \"{43000}\", \"{15996}\", \"{1696}\", \"{41652}\", \"{4709}\", \"{600}\",\n\t\"{48536}\", \"{11537}\", \"{3700}\", \"{22319}\", \"{20979}\", \"{12340}\", \"{5605}\", \"{54680}\", \"{7793}\", \"{15349}\",\n\t\"{58296}\", \"{9213}\", \"{10227}\", \"{8818}\", \"{23409}\", \"{2010}\", \"{40142}\", \"{19143}\", \"{21372}\", \"{5019}\",\n\t\"{14459}\", \"{6083}\", \"{8503}\", \"{59586}\", \"{13450}\", \"{79492}\", \"{55190}\", \"{1667}\", \"{7824}\", \"{48177}\",\n\t\"{22758}\", \"{3341}\", \"{41213}\", \"{18212}\", \"{241}\", \"{4348}\", \"{6928}\", \"{89361}\", \"{9652}\", \"{43873}\",\n\t\"{1921}\", \"{63967}\", \"{45280}\", \"{5244}\", \"{946}\", \"{40503}\", \"{5458}\", \"{17082}\", \"{43882}\", \"{3846}\",\n\t\"{2451}\", \"{23048}\", \"{45987}\", \"{5943}\", \"{1226}\", \"{19819}\", \"{88471}\", \"{14018}\", \"{47811}\", \"{8142}\",\n\t\"{56283}\", \"{25}\", \"{3963}\", \"{2399}\", \"{18830}\", \"{41831}\", \"{12038}\", \"{863}\", \"{8067}\", \"{3478}\",\n\t\"{28869}\", \"{3295}\", \"{578}\", \"{1303}\", \"{395}\", \"{67820}\", \"{45853}\", \"{364}\", \"{4480}\", \"{589}\",\n\t\"{3264}\", \"{28898}\", \"{3489}\", \"{7901}\", \"{892}\", \"{24778}\", \"{50881}\", \"{1804}\", \"{2368}\", \"{3992}\",\n\t\"{2585}\", \"{42541}\", \"{69217}\", \"{12294}\", \"{19066}\", \"{12479}\", \"{2135}\", \"{47683}\", \"{49303}\", \"{10302}\",\n\t\"{1742}\", \"{139}\", \"{105842}\", \"{13575}\", \"{3039}\", \"{8426}\", \"{9831}\", \"{43610}\", \"{46193}\", \"{3625}\",\n\t\"{11412}\", \"{26959}\", \"{725}\", \"{68507}\", \"{13369}\", \"{12993}\", \"{9336}\", \"{2729}\", \"{42100}\", \"{14896}\",\n\t\"{12488}\", \"{5720}\", \"{12265}\", \"{101820}\", \"{49436}\", \"{10437}\", \"{2600}\", \"{23219}\", \"{19753}\", \"{40752}\",\n\t\"{5609}\", \"{21562}\", \"{6693}\", \"{14249}\", \"{59396}\", \"{8313}\", \"{21879}\", \"{13240}\", \"{1077}\", \"{55780}\",\n\t\"{41042}\", \"{1086}\", \"{20272}\", \"{4119}\", \"{11327}\", \"{9918}\", \"{22509}\", \"{3110}\", \"{12550}\", \"{78592}\",\n\t\"{54090}\", \"{5015}\", \"{15559}\", \"{7183}\", \"{9403}\", \"{58486}\", \"{40313}\", \"{19312}\", \"{17692}\", \"{5248}\",\n\t\"{6924}\", \"{49077}\", \"{23658}\", \"{2241}\", \"{13601}\", \"{62867}\", \"{44380}\", \"{1436}\", \"{7828}\", \"{88261}\",\n\t\"{8752}\", \"{42973}\", \"{42982}\", \"{2946}\", \"{3551}\", \"{22148}\", \"{18402}\", \"{41403}\", \"{4558}\", \"{451}\",\n\t\"{89571}\", \"{15118}\", \"{46911}\", \"{9042}\", \"{44887}\", \"{4843}\", \"{5454}\", \"{18919}\", \"{18834}\", \"{5579}\",\n\t\"{40422}\", \"{867}\", \"{23169}\", \"{21}\", \"{3967}\", \"{49746}\", \"{19938}\", \"{1307}\", \"{391}\", \"{4298}\",\n\t\"{8063}\", \"{47930}\", \"{14139}\", \"{3291}\", \"{3260}\", \"{22679}\", \"{48056}\", \"{7905}\", \"{4269}\", \"{360}\",\n\t\"{4484}\", \"{41332}\", \"{43952}\", \"{3996}\", \"{2581}\", \"{6809}\", \"{896}\", \"{51492}\", \"{5588}\", \"{1800}\",\n\t\"{2131}\", \"{15299}\", \"{8939}\", \"{10306}\", \"{5138}\", \"{12290}\", \"{19062}\", \"{40063}\", \"{82919}\", \"{8422}\",\n\t\"{9835}\", \"{14578}\", \"{1746}\", \"{41782}\", \"{105846}\", \"{13571}\", \"{721}\", \"{4628}\", \"{41773}\", \"{12997}\",\n\t\"{14589}\", \"{3621}\", \"{11416}\", \"{48417}\", \"{40092}\", \"{5724}\", \"{12261}\", \"{20858}\", \"{9332}\", \"{109757}\",\n\t\"{15268}\", \"{14892}\", \"{19757}\", \"{12348}\", \"{69526}\", \"{21566}\", \"{27978}\", \"{10433}\", \"{2604}\", \"{48825}\",\n\t\"{79286}\", \"{13244}\", \"{1073}\", \"{608}\", \"{6697}\", \"{43121}\", \"{3708}\", \"{8317}\", \"{11323}\", \"{48322}\",\n\t\"{52191}\", \"{3114}\", \"{13458}\", \"{1082}\", \"{20276}\", \"{68236}\", \"{42631}\", \"{7187}\", \"{9407}\", \"{2018}\",\n\t\"{12554}\", \"{78596}\", \"{24408}\", \"{5011}\", \"{6920}\", \"{49073}\", \"{42281}\", \"{2245}\", \"{1929}\", \"{19316}\",\n\t\"{17696}\", \"{44872}\", \"{43560}\", \"{88265}\", \"{8756}\", \"{3349}\", \"{13605}\", \"{62863}\", \"{249}\", \"{1432}\",\n\t\"{18406}\", \"{13019}\", \"{40810}\", \"{455}\", \"{42986}\", \"{2942}\", \"{3555}\", \"{43591}\", \"{44883}\", \"{4847}\",\n\t\"{5450}\", \"{24049}\", \"{61808}\", \"{29848}\", \"{2459}\", \"{9046}\", \"{20261}\", \"{1678}\", \"{41051}\", \"{1095}\",\n\t\"{27268}\", \"{3103}\", \"{11334}\", \"{48335}\", \"{54083}\", \"{5006}\", \"{12543}\", \"{25808}\", \"{9410}\", \"{28009}\",\n\t\"{10238}\", \"{7190}\", \"{2613}\", \"{26578}\", \"{49425}\", \"{10424}\", \"{69531}\", \"{21571}\", \"{19740}\", \"{40741}\",\n\t\"{29719}\", \"{8300}\", \"{6680}\", \"{11528}\", \"{1064}\", \"{55793}\", \"{1689}\", \"{13253}\", \"{3542}\", \"{11198}\",\n\t\"{42991}\", \"{2955}\", \"{1239}\", \"{442}\", \"{18411}\", \"{41410}\", \"{28448}\", \"{9051}\", \"{49678}\", \"{3859}\",\n\t\"{5447}\", \"{45483}\", \"{959}\", \"{4850}\", \"{3}\", \"{44865}\", \"{40300}\", \"{19301}\", \"{10688}\", \"{2252}\",\n\t\"{6937}\", \"{49064}\", \"{44393}\", \"{1425}\", \"{13612}\", \"{62874}\", \"{8741}\", \"{29358}\", \"{11169}\", \"{48168}\",\n\t\"{4493}\", \"{16049}\", \"{45840}\", \"{377}\", \"{48041}\", \"{7912}\", \"{3277}\", \"{8668}\", \"{19228}\", \"{1817}\",\n\t\"{881}\", \"{4988}\", \"{2596}\", \"{9189}\", \"{7409}\", \"{3981}\", \"{3970}\", \"{49751}\", \"{9178}\", \"{36}\",\n\t\"{4979}\", \"{870}\", \"{18823}\", \"{41822}\", \"{8699}\", \"{3286}\", \"{8074}\", \"{6319}\", \"{386}\", \"{18538}\",\n\t\"{20709}\", \"{1310}\", \"{11401}\", \"{15989}\", \"{8229}\", \"{3636}\", \"{16408}\", \"{12980}\", \"{736}\", \"{18188}\",\n\t\"{42113}\", \"{14885}\", \"{9325}\", \"{7048}\", \"{12276}\", \"{19669}\", \"{21458}\", \"{5733}\", \"{19075}\", \"{17318}\",\n\t\"{19698}\", \"{12287}\", \"{22928}\", \"{10311}\", \"{2126}\", \"{9539}\", \"{18179}\", \"{13566}\", \"{1751}\", \"{20348}\",\n\t\"{9822}\", \"{43603}\", \"{6758}\", \"{8435}\", \"{8518}\", \"{3107}\", \"{11330}\", \"{6098}\", \"{20265}\", \"{68225}\",\n\t\"{16339}\", \"{1091}\", \"{9414}\", \"{7779}\", \"{42622}\", \"{7194}\", \"{21369}\", \"{5002}\", \"{12547}\", \"{19158}\",\n\t\"{69535}\", \"{21575}\", \"{19744}\", \"{17429}\", \"{2617}\", \"{9208}\", \"{7788}\", \"{10420}\", \"{1060}\", \"{20479}\",\n\t\"{18648}\", \"{13257}\", \"{6069}\", \"{8304}\", \"{6684}\", \"{43132}\", \"{40803}\", \"{446}\", \"{18415}\", \"{5958}\",\n\t\"{3546}\", \"{8159}\", \"{42995}\", \"{2951}\", \"{5443}\", \"{17099}\", \"{19519}\", \"{4854}\", \"{7338}\", \"{9055}\",\n\t\"{43899}\", \"{42263}\", \"{9649}\", \"{2256}\", \"{6933}\", \"{49060}\", \"{7}\", \"{44861}\", \"{17068}\", \"{19305}\",\n\t\"{8745}\", \"{6428}\", \"{43573}\", \"{65879}\", \"{16789}\", \"{1421}\", \"{13616}\", \"{18209}\", \"{29498}\", \"{7916}\",\n\t\"{3273}\", \"{2889}\", \"{4497}\", \"{41321}\", \"{1508}\", \"{373}\", \"{2592}\", \"{10148}\", \"{28379}\", \"{3985}\",\n\t\"{50896}\", \"{1813}\", \"{885}\", \"{51481}\", \"{40431}\", \"{874}\", \"{18827}\", \"{41826}\", \"{3974}\", \"{28388}\",\n\t\"{26608}\", \"{32}\", \"{382}\", \"{67837}\", \"{50391}\", \"{1314}\", \"{2878}\", \"{3282}\", \"{8070}\", \"{29469}\",\n\t\"{41760}\", \"{12984}\", \"{732}\", \"{1149}\", \"{11405}\", \"{38098}\", \"{27559}\", \"{3632}\", \"{12272}\", \"{13888}\",\n\t\"{40081}\", \"{5737}\", \"{10509}\", \"{14881}\", \"{9321}\", \"{28738}\", \"{39788}\", \"{10315}\", \"{2122}\", \"{26249}\",\n\t\"{19071}\", \"{40070}\", \"{13879}\", \"{12283}\", \"{9826}\", \"{11219}\", \"{29028}\", \"{8431}\", \"{24829}\", \"{13562}\",\n\t\"{1755}\", \"{41791}\", \"{21365}\", \"{69325}\", \"{17239}\", \"{19154}\", \"{9418}\", \"{2007}\", \"{10230}\", \"{7198}\",\n\t\"{20269}\", \"{1670}\", \"{13447}\", \"{18058}\", \"{8514}\", \"{6679}\", \"{43722}\", \"{6094}\", \"{3717}\", \"{8308}\",\n\t\"{6688}\", \"{11520}\", \"{68435}\", \"{617}\", \"{1681}\", \"{16529}\", \"{7169}\", \"{9204}\", \"{7784}\", \"{42032}\",\n\t\"{5612}\", \"{21579}\", \"{19748}\", \"{12357}\", \"{2446}\", \"{9059}\", \"{43895}\", \"{3851}\", \"{41903}\", \"{17095}\",\n\t\"{951}\", \"{4858}\", \"{6238}\", \"{8155}\", \"{42999}\", \"{43363}\", \"{1231}\", \"{16199}\", \"{18419}\", \"{5954}\",\n\t\"{256}\", \"{45961}\", \"{16168}\", \"{18205}\", \"{8749}\", \"{3356}\", \"{7833}\", \"{48160}\", \"{17689}\", \"{5253}\",\n\t\"{1936}\", \"{19309}\", \"{9645}\", \"{7528}\", \"{42473}\", \"{64979}\", \"{5597}\", \"{40221}\", \"{889}\", \"{4980}\",\n\t\"{28598}\", \"{6816}\", \"{2373}\", \"{3989}\", \"{51996}\", \"{592}\", \"{1504}\", \"{50581}\", \"{3492}\", \"{11048}\",\n\t\"{29279}\", \"{2885}\", \"{2874}\", \"{29288}\", \"{27708}\", \"{3463}\", \"{41531}\", \"{4287}\", \"{563}\", \"{1318}\",\n\t\"{3978}\", \"{2382}\", \"{9170}\", \"{28569}\", \"{4971}\", \"{878}\", \"{51291}\", \"{5566}\", \"{10505}\", \"{39198}\",\n\t\"{26459}\", \"{2732}\", \"{40660}\", \"{13884}\", \"{12493}\", \"{34999}\", \"{11409}\", \"{15981}\", \"{8221}\", \"{29638}\",\n\t\"{13372}\", \"{12988}\", \"{41181}\", \"{1145}\", \"{18171}\", \"{41170}\", \"{1759}\", \"{122}\", \"{38688}\", \"{11215}\",\n\t\"{3022}\", \"{27349}\", \"{25929}\", \"{12462}\", \"{5127}\", \"{40691}\", \"{8926}\", \"{10319}\", \"{28128}\", \"{9531}\",\n\t\"{26368}\", \"{2003}\", \"{10234}\", \"{49235}\", \"{21361}\", \"{13958}\", \"{40151}\", \"{19150}\", \"{8510}\", \"{29109}\",\n\t\"{11338}\", \"{6090}\", \"{55183}\", \"{1674}\", \"{13443}\", \"{1099}\", \"{1068}\", \"{613}\", \"{1685}\", \"{41641}\",\n\t\"{3713}\", \"{27478}\", \"{48525}\", \"{11524}\", \"{5616}\", \"{54693}\", \"{78391}\", \"{12353}\", \"{28619}\", \"{9200}\",\n\t\"{7780}\", \"{10428}\", \"{41907}\", \"{17091}\", \"{955}\", \"{40510}\", \"{2442}\", \"{10098}\", \"{43891}\", \"{3855}\",\n\t\"{1235}\", \"{44583}\", \"{45994}\", \"{5950}\", \"{29548}\", \"{8151}\", \"{48778}\", \"{2959}\", \"{11788}\", \"{3352}\",\n\t\"{7837}\", \"{48164}\", \"{252}\", \"{1429}\", \"{41200}\", \"{18201}\", \"{9641}\", \"{28258}\", \"{10069}\", \"{49068}\",\n\t\"{45293}\", \"{5257}\", \"{1932}\", \"{63974}\", \"{49141}\", \"{6812}\", \"{2377}\", \"{9768}\", \"{5593}\", \"{17149}\",\n\t\"{44940}\", \"{4984}\", \"{3496}\", \"{8089}\", \"{6509}\", \"{2881}\", \"{18328}\", \"{596}\", \"{1500}\", \"{5888}\",\n\t\"{5879}\", \"{4283}\", \"{567}\", \"{40922}\", \"{2870}\", \"{48651}\", \"{8078}\", \"{3467}\", \"{4975}\", \"{19438}\",\n\t\"{21609}\", \"{5562}\", \"{9799}\", \"{2386}\", \"{9174}\", \"{7219}\", \"{17508}\", \"{13880}\", \"{12497}\", \"{19088}\",\n\t\"{10501}\", \"{14889}\", \"{9329}\", \"{2736}\", \"{13376}\", \"{18769}\", \"{20558}\", \"{1141}\", \"{43013}\", \"{15985}\",\n\t\"{8225}\", \"{6148}\", \"{23828}\", \"{11211}\", \"{3026}\", \"{8439}\", \"{18175}\", \"{16218}\", \"{18798}\", \"{126}\",\n\t\"{8922}\", \"{42703}\", \"{7658}\", \"{9535}\", \"{19079}\", \"{12466}\", \"{5123}\", \"{21248}\", \"{48576}\", \"{11577}\",\n\t\"{3740}\", \"{22359}\", \"{18613}\", \"{41612}\", \"{4749}\", \"{640}\", \"{27930}\", \"{15309}\", \"{49297}\", \"{9253}\",\n\t\"{20939}\", \"{12300}\", \"{5645}\", \"{45681}\", \"{40102}\", \"{16894}\", \"{17483}\", \"{5059}\", \"{10267}\", \"{8858}\",\n\t\"{23449}\", \"{2050}\", \"{13410}\", \"{17998}\", \"{44191}\", \"{1627}\", \"{14419}\", \"{9954}\", \"{8543}\", \"{48587}\",\n\t\"{41253}\", \"{1297}\", \"{201}\", \"{4308}\", \"{7864}\", \"{48137}\", \"{22718}\", \"{3301}\", \"{1961}\", \"{63927}\",\n\t\"{54281}\", \"{5204}\", \"{6968}\", \"{7392}\", \"{9612}\", \"{43833}\", \"{49627}\", \"{3806}\", \"{2411}\", \"{6999}\",\n\t\"{906}\", \"{40543}\", \"{5418}\", \"{1990}\", \"{6482}\", \"{14058}\", \"{47851}\", \"{7895}\", \"{54986}\", \"{5903}\",\n\t\"{1266}\", \"{19859}\", \"{46382}\", \"{3434}\", \"{2823}\", \"{48602}\", \"{534}\", \"{40971}\", \"{13178}\", \"{18567}\",\n\t\"{9127}\", \"{69}\", \"{29929}\", \"{39392}\", \"{12699}\", \"{5531}\", \"{4926}\", \"{66960}\", \"{44913}\", \"{12085}\",\n\t\"{19277}\", \"{1848}\", \"{2324}\", \"{38999}\", \"{98}\", \"{6841}\", \"{1553}\", \"{328}\", \"{40980}\", \"{13764}\",\n\t\"{3228}\", \"{8637}\", \"{38482}\", \"{43401}\", \"{68357}\", \"{175}\", \"{4691}\", \"{798}\", \"{3075}\", \"{57782}\",\n\t\"{3698}\", \"{8287}\", \"{5170}\", \"{24569}\", \"{104902}\", \"{12435}\", \"{2179}\", \"{9566}\", \"{2794}\", \"{42750}\",\n\t\"{48944}\", \"{2765}\", \"{9597}\", \"{2188}\", \"{21407}\", \"{69447}\", \"{12229}\", \"{5181}\", \"{8276}\", \"{3669}\",\n\t\"{43040}\", \"{3084}\", \"{769}\", \"{1112}\", \"{184}\", \"{100960}\", \"{18617}\", \"{13208}\", \"{68466}\", \"{644}\",\n\t\"{26838}\", \"{11573}\", \"{3744}\", \"{43780}\", \"{69387}\", \"{12304}\", \"{5641}\", \"{24258}\", \"{27934}\", \"{42061}\",\n\t\"{2648}\", \"{9257}\", \"{10263}\", \"{11899}\", \"{42090}\", \"{2054}\", \"{12518}\", \"{16890}\", \"{17487}\", \"{69376}\",\n\t\"{43771}\", \"{9950}\", \"{8547}\", \"{3158}\", \"{13414}\", \"{68497}\", \"{25548}\", \"{1623}\", \"{7860}\", \"{48133}\",\n\t\"{52380}\", \"{3305}\", \"{13649}\", \"{1293}\", \"{205}\", \"{45932}\", \"{42420}\", \"{7396}\", \"{9616}\", \"{2209}\",\n\t\"{1965}\", \"{63923}\", \"{24619}\", \"{5200}\", \"{902}\", \"{12159}\", \"{41950}\", \"{1994}\", \"{49623}\", \"{3802}\",\n\t\"{2415}\", \"{53490}\", \"{54982}\", \"{5907}\", \"{1262}\", \"{419}\", \"{6486}\", \"{28908}\", \"{3519}\", \"{7891}\",\n\t\"{530}\", \"{4439}\", \"{41562}\", \"{18563}\", \"{14798}\", \"{3430}\", \"{2827}\", \"{48606}\", \"{18878}\", \"{5535}\",\n\t\"{4922}\", \"{66964}\", \"{9123}\", \"{46870}\", \"{15079}\", \"{39396}\", \"{2320}\", \"{15088}\", \"{46881}\", \"{6845}\",\n\t\"{5329}\", \"{12081}\", \"{19273}\", \"{18889}\", \"{42812}\", \"{8633}\", \"{38486}\", \"{7949}\", \"{1557}\", \"{41593}\",\n\t\"{40984}\", \"{13760}\", \"{3071}\", \"{22468}\", \"{9879}\", \"{8283}\", \"{4078}\", \"{171}\", \"{4695}\", \"{41123}\",\n\t\"{83859}\", \"{9562}\", \"{2790}\", \"{15438}\", \"{5174}\", \"{51683}\", \"{5799}\", \"{12431}\", \"{21403}\", \"{5768}\",\n\t\"{40633}\", \"{5185}\", \"{23378}\", \"{2761}\", \"{9593}\", \"{49557}\", \"{50193}\", \"{1116}\", \"{180}\", \"{4089}\",\n\t\"{8272}\", \"{9888}\", \"{14328}\", \"{3080}\", \"{27938}\", \"{10473}\", \"{2644}\", \"{42680}\", \"{19717}\", \"{12308}\",\n\t\"{69566}\", \"{17297}\", \"{26834}\", \"{43161}\", \"{3748}\", \"{8357}\", \"{68287}\", \"{13204}\", \"{1033}\", \"{648}\",\n\t\"{13418}\", \"{17990}\", \"{16587}\", \"{68276}\", \"{11363}\", \"{10999}\", \"{43190}\", \"{3154}\", \"{12514}\", \"{69597}\",\n\t\"{24448}\", \"{5051}\", \"{42671}\", \"{8850}\", \"{9447}\", \"{2058}\", \"{1969}\", \"{19356}\", \"{21167}\", \"{44832}\",\n\t\"{6960}\", \"{49033}\", \"{53280}\", \"{2205}\", \"{13645}\", \"{62823}\", \"{209}\", \"{1472}\", \"{43520}\", \"{6296}\",\n\t\"{8716}\", \"{3309}\", \"{48723}\", \"{2902}\", \"{3515}\", \"{52590}\", \"{1483}\", \"{13059}\", \"{40850}\", \"{415}\",\n\t\"{7586}\", \"{29808}\", \"{2419}\", \"{6991}\", \"{55882}\", \"{4807}\", \"{5410}\", \"{1998}\", \"{15698}\", \"{61}\",\n\t\"{3927}\", \"{49706}\", \"{12691}\", \"{5539}\", \"{40462}\", \"{827}\", \"{8023}\", \"{47970}\", \"{14179}\", \"{38296}\",\n\t\"{19978}\", \"{1347}\", \"{5822}\", \"{67864}\", \"{4229}\", \"{320}\", \"{18373}\", \"{19989}\", \"{3220}\", \"{14188}\",\n\t\"{47981}\", \"{7945}\", \"{5325}\", \"{40493}\", \"{41884}\", \"{1840}\", \"{43912}\", \"{9733}\", \"{90}\", \"{6849}\",\n\t\"{5178}\", \"{21213}\", \"{5795}\", \"{40023}\", \"{2171}\", \"{23568}\", \"{8979}\", \"{9383}\", \"{1706}\", \"{50783}\",\n\t\"{4699}\", \"{790}\", \"{82959}\", \"{8462}\", \"{3690}\", \"{14538}\", \"{22278}\", \"{3661}\", \"{8493}\", \"{48457}\",\n\t\"{761}\", \"{4668}\", \"{41733}\", \"{4085}\", \"{9372}\", \"{8988}\", \"{15228}\", \"{2180}\", \"{51093}\", \"{5764}\",\n\t\"{12221}\", \"{5189}\", \"{19713}\", \"{40712}\", \"{5649}\", \"{17293}\", \"{49476}\", \"{10477}\", \"{2640}\", \"{23259}\",\n\t\"{21839}\", \"{13200}\", \"{1037}\", \"{44781}\", \"{26830}\", \"{14209}\", \"{48397}\", \"{8353}\", \"{11367}\", \"{9958}\",\n\t\"{22549}\", \"{3150}\", \"{41002}\", \"{17994}\", \"{16583}\", \"{4159}\", \"{15519}\", \"{8854}\", \"{9443}\", \"{49487}\",\n\t\"{12510}\", \"{16898}\", \"{45091}\", \"{5055}\", \"{6964}\", \"{49037}\", \"{23618}\", \"{2201}\", \"{40353}\", \"{19352}\",\n\t\"{21163}\", \"{5208}\", \"{7868}\", \"{6292}\", \"{8712}\", \"{42933}\", \"{13641}\", \"{62827}\", \"{55381}\", \"{1476}\",\n\t\"{1487}\", \"{41443}\", \"{4518}\", \"{411}\", \"{48727}\", \"{2906}\", \"{3511}\", \"{7899}\", \"{55886}\", \"{4803}\",\n\t\"{5414}\", \"{18959}\", \"{7582}\", \"{15158}\", \"{46951}\", \"{6995}\", \"{12695}\", \"{41871}\", \"{12078}\", \"{823}\",\n\t\"{47282}\", \"{65}\", \"{3923}\", \"{49702}\", \"{538}\", \"{1343}\", \"{5826}\", \"{67860}\", \"{8027}\", \"{3438}\",\n\t\"{28829}\", \"{38292}\", \"{3224}\", \"{39899}\", \"{47985}\", \"{7941}\", \"{45813}\", \"{324}\", \"{18377}\", \"{13768}\",\n\t\"{2328}\", \"{9737}\", \"{94}\", \"{42501}\", \"{5321}\", \"{12089}\", \"{41880}\", \"{1844}\", \"{2175}\", \"{56682}\",\n\t\"{2798}\", \"{9387}\", \"{69257}\", \"{21217}\", \"{5791}\", \"{12439}\", \"{3079}\", \"{8466}\", \"{3694}\", \"{43650}\",\n\t\"{1702}\", \"{179}\", \"{105802}\", \"{794}\", \"{765}\", \"{68547}\", \"{188}\", \"{4081}\", \"{49844}\", \"{3665}\",\n\t\"{8497}\", \"{3088}\", \"{24379}\", \"{5760}\", \"{12225}\", \"{101860}\", \"{9376}\", \"{2769}\", \"{42140}\", \"{2184}\",\n\t\"{18360}\", \"{41361}\", \"{1548}\", \"{333}\", \"{38499}\", \"{7956}\", \"{3233}\", \"{27158}\", \"{41897}\", \"{1853}\",\n\t\"{5336}\", \"{40480}\", \"{83}\", \"{10108}\", \"{28339}\", \"{9720}\", \"{3934}\", \"{39389}\", \"{26648}\", \"{72}\",\n\t\"{40471}\", \"{834}\", \"{12682}\", \"{41866}\", \"{2838}\", \"{38285}\", \"{8030}\", \"{29429}\", \"{5831}\", \"{67877}\",\n\t\"{41390}\", \"{1354}\", \"{8480}\", \"{29099}\", \"{27519}\", \"{3672}\", \"{41720}\", \"{4096}\", \"{772}\", \"{1109}\",\n\t\"{10549}\", \"{2193}\", \"{9361}\", \"{28778}\", \"{12232}\", \"{101877}\", \"{51080}\", \"{5777}\", \"{5786}\", \"{40030}\",\n\t\"{13839}\", \"{21200}\", \"{28789}\", \"{9390}\", \"{2162}\", \"{26209}\", \"{24869}\", \"{783}\", \"{1715}\", \"{50790}\",\n\t\"{3683}\", \"{11259}\", \"{29068}\", \"{8471}\", \"{16594}\", \"{68265}\", \"{16379}\", \"{17983}\", \"{8558}\", \"{3147}\",\n\t\"{11370}\", \"{23949}\", \"{17498}\", \"{5042}\", \"{12507}\", \"{19118}\", \"{9454}\", \"{7739}\", \"{42662}\", \"{8843}\",\n\t\"{2657}\", \"{9248}\", \"{49461}\", \"{10460}\", \"{69575}\", \"{17284}\", \"{19704}\", \"{17469}\", \"{6029}\", \"{8344}\",\n\t\"{26827}\", \"{43172}\", \"{1020}\", \"{16388}\", \"{18608}\", \"{13217}\", \"{3506}\", \"{8119}\", \"{6499}\", \"{2911}\",\n\t\"{40843}\", \"{406}\", \"{1490}\", \"{5918}\", \"{7378}\", \"{6982}\", \"{7595}\", \"{42223}\", \"{5403}\", \"{21768}\",\n\t\"{19559}\", \"{4814}\", \"{21174}\", \"{44821}\", \"{17028}\", \"{19345}\", \"{9609}\", \"{2216}\", \"{6973}\", \"{7389}\",\n\t\"{20078}\", \"{1461}\", \"{13656}\", \"{18249}\", \"{8705}\", \"{6468}\", \"{43533}\", \"{6285}\", \"{47996}\", \"{7952}\",\n\t\"{3237}\", \"{8628}\", \"{18364}\", \"{16009}\", \"{18589}\", \"{337}\", \"{87}\", \"{42512}\", \"{7449}\", \"{9724}\",\n\t\"{19268}\", \"{1857}\", \"{5332}\", \"{21059}\", \"{4939}\", \"{830}\", \"{12686}\", \"{19299}\", \"{3930}\", \"{49711}\",\n\t\"{9138}\", \"{76}\", \"{5835}\", \"{18578}\", \"{20749}\", \"{1350}\", \"{43202}\", \"{38281}\", \"{8034}\", \"{6359}\",\n\t\"{16448}\", \"{4092}\", \"{776}\", \"{68554}\", \"{8484}\", \"{48440}\", \"{8269}\", \"{3676}\", \"{12236}\", \"{19629}\",\n\t\"{21418}\", \"{5773}\", \"{9588}\", \"{2197}\", \"{9365}\", \"{7008}\", \"{22968}\", \"{9394}\", \"{2166}\", \"{9579}\",\n\t\"{5782}\", \"{17358}\", \"{69244}\", \"{21204}\", \"{3687}\", \"{8298}\", \"{6718}\", \"{8475}\", \"{18139}\", \"{787}\",\n\t\"{1711}\", \"{20308}\", \"{11599}\", \"{3143}\", \"{11374}\", \"{48375}\", \"{16590}\", \"{1638}\", \"{41011}\", \"{17987}\",\n\t\"{9450}\", \"{28049}\", \"{10278}\", \"{8847}\", \"{45082}\", \"{5046}\", \"{12503}\", \"{25848}\", \"{69571}\", \"{17280}\",\n\t\"{19700}\", \"{40701}\", \"{2653}\", \"{10289}\", \"{49465}\", \"{10464}\", \"{1024}\", \"{44792}\", \"{68290}\", \"{13213}\",\n\t\"{29759}\", \"{8340}\", \"{26823}\", \"{11568}\", \"{1279}\", \"{402}\", \"{1494}\", \"{41450}\", \"{3502}\", \"{27669}\",\n\t\"{48734}\", \"{2915}\", \"{5407}\", \"{54482}\", \"{919}\", \"{4810}\", \"{28408}\", \"{6986}\", \"{7591}\", \"{3819}\",\n\t\"{26179}\", \"{2212}\", \"{6977}\", \"{49024}\", \"{21170}\", \"{44825}\", \"{40340}\", \"{19341}\", \"{8701}\", \"{29318}\",\n\t\"{11129}\", \"{6281}\", \"{55392}\", \"{1465}\", \"{13652}\", \"{1288}\", \"{19264}\", \"{17109}\", \"{19489}\", \"{12096}\",\n\t\"{46896}\", \"{6852}\", \"{2337}\", \"{9728}\", \"{18368}\", \"{13777}\", \"{1540}\", \"{20159}\", \"{38491}\", \"{43412}\",\n\t\"{6549}\", \"{8624}\", \"{2830}\", \"{48611}\", \"{8038}\", \"{3427}\", \"{5839}\", \"{18574}\", \"{527}\", \"{18399}\",\n\t\"{42302}\", \"{39381}\", \"{9134}\", \"{7259}\", \"{4935}\", \"{19478}\", \"{21649}\", \"{5522}\", \"{9584}\", \"{49540}\",\n\t\"{9369}\", \"{2776}\", \"{17548}\", \"{5192}\", \"{21414}\", \"{69454}\", \"{8488}\", \"{3097}\", \"{8265}\", \"{6108}\",\n\t\"{197}\", \"{18729}\", \"{20518}\", \"{1101}\", \"{4682}\", \"{16258}\", \"{68344}\", \"{166}\", \"{23868}\", \"{8294}\",\n\t\"{3066}\", \"{8479}\", \"{19039}\", \"{12426}\", \"{5163}\", \"{21208}\", \"{2787}\", \"{9398}\", \"{7618}\", \"{9575}\",\n\t\"{17490}\", \"{13918}\", \"{40111}\", \"{16887}\", \"{10499}\", \"{2043}\", \"{10274}\", \"{49275}\", \"{44182}\", \"{1634}\",\n\t\"{13403}\", \"{24948}\", \"{8550}\", \"{29149}\", \"{11378}\", \"{9947}\", \"{3753}\", \"{11389}\", \"{48565}\", \"{11564}\",\n\t\"{1028}\", \"{653}\", \"{18600}\", \"{41601}\", \"{28659}\", \"{9240}\", \"{27923}\", \"{10468}\", \"{5656}\", \"{45692}\",\n\t\"{69390}\", \"{12313}\", \"{2402}\", \"{26769}\", \"{49634}\", \"{3815}\", \"{41947}\", \"{1983}\", \"{915}\", \"{40550}\",\n\t\"{29508}\", \"{7886}\", \"{6491}\", \"{2919}\", \"{1275}\", \"{55582}\", \"{1498}\", \"{5910}\", \"{212}\", \"{1469}\",\n\t\"{41240}\", \"{1284}\", \"{27079}\", \"{3312}\", \"{7877}\", \"{48124}\", \"{54292}\", \"{5217}\", \"{1972}\", \"{63934}\",\n\t\"{9601}\", \"{28218}\", \"{10029}\", \"{7381}\", \"{39599}\", \"{6856}\", \"{2333}\", \"{26058}\", \"{19260}\", \"{40261}\",\n\t\"{44904}\", \"{12092}\", \"{38495}\", \"{11008}\", \"{29239}\", \"{8620}\", \"{40997}\", \"{13773}\", \"{1544}\", \"{41580}\",\n\t\"{41571}\", \"{18570}\", \"{523}\", \"{1358}\", \"{2834}\", \"{38289}\", \"{27748}\", \"{3423}\", \"{4931}\", \"{838}\",\n\t\"{40290}\", \"{5526}\", \"{3938}\", \"{39385}\", \"{9130}\", \"{28529}\", \"{40620}\", \"{5196}\", \"{21410}\", \"{25998}\",\n\t\"{9580}\", \"{28199}\", \"{26419}\", \"{2772}\", \"{193}\", \"{100977}\", \"{50180}\", \"{1105}\", \"{11449}\", \"{3093}\",\n\t\"{8261}\", \"{29678}\", \"{29689}\", \"{8290}\", \"{3062}\", \"{27309}\", \"{4686}\", \"{41130}\", \"{1719}\", \"{162}\",\n\t\"{2783}\", \"{10359}\", \"{28168}\", \"{9571}\", \"{25969}\", \"{12422}\", \"{5167}\", \"{51690}\", \"{9458}\", \"{2047}\",\n\t\"{10270}\", \"{22849}\", \"{17494}\", \"{69365}\", \"{17279}\", \"{16883}\", \"{8554}\", \"{6639}\", \"{43762}\", \"{9943}\",\n\t\"{16598}\", \"{1630}\", \"{13407}\", \"{18018}\", \"{68475}\", \"{657}\", \"{18604}\", \"{16569}\", \"{3757}\", \"{8348}\",\n\t\"{48561}\", \"{11560}\", \"{5652}\", \"{17288}\", \"{19708}\", \"{12317}\", \"{7129}\", \"{9244}\", \"{27927}\", \"{42072}\",\n\t\"{41943}\", \"{1987}\", \"{911}\", \"{4818}\", \"{2406}\", \"{9019}\", \"{7599}\", \"{3811}\", \"{1271}\", \"{20668}\",\n\t\"{18459}\", \"{5914}\", \"{6278}\", \"{7882}\", \"{6495}\", \"{43323}\", \"{8709}\", \"{3316}\", \"{7873}\", \"{6289}\",\n\t\"{216}\", \"{45921}\", \"{16128}\", \"{1280}\", \"{9605}\", \"{7568}\", \"{42433}\", \"{7385}\", \"{21178}\", \"{5213}\",\n\t\"{1976}\", \"{19349}\", \"{11467}\", \"{48466}\", \"{22249}\", \"{3650}\", \"{41702}\", \"{18703}\", \"{750}\", \"{4659}\",\n\t\"{15219}\", \"{27820}\", \"{9343}\", \"{49387}\", \"{12210}\", \"{20829}\", \"{45791}\", \"{5755}\", \"{16984}\", \"{40012}\",\n\t\"{5149}\", \"{17593}\", \"{8948}\", \"{10377}\", \"{2140}\", \"{23559}\", \"{17888}\", \"{13500}\", \"{1737}\", \"{44081}\",\n\t\"{9844}\", \"{14509}\", \"{48497}\", \"{8453}\", \"{1387}\", \"{41343}\", \"{4218}\", \"{311}\", \"{48027}\", \"{7974}\",\n\t\"{3211}\", \"{22608}\", \"{63837}\", \"{1871}\", \"{5314}\", \"{54391}\", \"{7282}\", \"{6878}\", \"{43923}\", \"{9702}\",\n\t\"{3916}\", \"{49737}\", \"{6889}\", \"{50}\", \"{40453}\", \"{816}\", \"{1880}\", \"{5508}\", \"{14148}\", \"{6592}\",\n\t\"{7985}\", \"{47941}\", \"{5813}\", \"{54896}\", \"{19949}\", \"{1376}\", \"{3524}\", \"{46292}\", \"{48712}\", \"{2933}\",\n\t\"{40861}\", \"{424}\", \"{18477}\", \"{13068}\", \"{2428}\", \"{9037}\", \"{39282}\", \"{29839}\", \"{5421}\", \"{12789}\",\n\t\"{66870}\", \"{4836}\", \"{12195}\", \"{44803}\", \"{1958}\", \"{19367}\", \"{38889}\", \"{2234}\", \"{6951}\", \"{46995}\",\n\t\"{238}\", \"{1443}\", \"{13674}\", \"{40890}\", \"{8727}\", \"{3338}\", \"{43511}\", \"{38592}\", \"{20207}\", \"{68247}\",\n\t\"{688}\", \"{4781}\", \"{57692}\", \"{3165}\", \"{8397}\", \"{3788}\", \"{24479}\", \"{5060}\", \"{12525}\", \"{104812}\",\n\t\"{9476}\", \"{2069}\", \"{42640}\", \"{2684}\", \"{2675}\", \"{48854}\", \"{2098}\", \"{9487}\", \"{69557}\", \"{21517}\",\n\t\"{5091}\", \"{12339}\", \"{3779}\", \"{8366}\", \"{3194}\", \"{43150}\", \"{1002}\", \"{679}\", \"{100870}\", \"{13235}\",\n\t\"{13318}\", \"{18707}\", \"{754}\", \"{68576}\", \"{11463}\", \"{26928}\", \"{43690}\", \"{3654}\", \"{12214}\", \"{69297}\",\n\t\"{24348}\", \"{5751}\", \"{42171}\", \"{27824}\", \"{9347}\", \"{2758}\", \"{11989}\", \"{10373}\", \"{2144}\", \"{42180}\",\n\t\"{16980}\", \"{12408}\", \"{69266}\", \"{17597}\", \"{9840}\", \"{43661}\", \"{3048}\", \"{8457}\", \"{68587}\", \"{13504}\",\n\t\"{1733}\", \"{148}\", \"{48023}\", \"{7970}\", \"{3215}\", \"{52290}\", \"{1383}\", \"{13759}\", \"{45822}\", \"{315}\",\n\t\"{7286}\", \"{42530}\", \"{2319}\", \"{9706}\", \"{63833}\", \"{1875}\", \"{5310}\", \"{24709}\", \"{12049}\", \"{812}\",\n\t\"{1884}\", \"{41840}\", \"{3912}\", \"{49733}\", \"{53580}\", \"{54}\", \"{5817}\", \"{54892}\", \"{509}\", \"{1372}\",\n\t\"{28818}\", \"{6596}\", \"{7981}\", \"{3409}\", \"{4529}\", \"{420}\", \"{18473}\", \"{41472}\", \"{3520}\", \"{14688}\",\n\t\"{48716}\", \"{2937}\", \"{5425}\", \"{18968}\", \"{66874}\", \"{4832}\", \"{46960}\", \"{9033}\", \"{39286}\", \"{15169}\",\n\t\"{15198}\", \"{2230}\", \"{6955}\", \"{46991}\", \"{12191}\", \"{5239}\", \"{18999}\", \"{19363}\", \"{8723}\", \"{42902}\",\n\t\"{7859}\", \"{38596}\", \"{41483}\", \"{1447}\", \"{13670}\", \"{40894}\", \"{22578}\", \"{3161}\", \"{8393}\", \"{9969}\",\n\t\"{20203}\", \"{4168}\", \"{41033}\", \"{4785}\", \"{9472}\", \"{83949}\", \"{15528}\", \"{2680}\", \"{51793}\", \"{5064}\",\n\t\"{12521}\", \"{5689}\", \"{5678}\", \"{21513}\", \"{5095}\", \"{40723}\", \"{2671}\", \"{23268}\", \"{49447}\", \"{9483}\",\n\t\"{1006}\", \"{50083}\", \"{4199}\", \"{13231}\", \"{9998}\", \"{8362}\", \"{3190}\", \"{14238}\", \"{10563}\", \"{27828}\",\n\t\"{42790}\", \"{2754}\", \"{12218}\", \"{19607}\", \"{17387}\", \"{69476}\", \"{43071}\", \"{26924}\", \"{8247}\", \"{3658}\",\n\t\"{13314}\", \"{68397}\", \"{758}\", \"{1123}\", \"{17880}\", \"{13508}\", \"{68366}\", \"{144}\", \"{10889}\", \"{11273}\",\n\t\"{3044}\", \"{43080}\", \"{69487}\", \"{12404}\", \"{5141}\", \"{24558}\", \"{8940}\", \"{42761}\", \"{2148}\", \"{9557}\",\n\t\"{19246}\", \"{1879}\", \"{44922}\", \"{21077}\", \"{49123}\", \"{6870}\", \"{2315}\", \"{53390}\", \"{62933}\", \"{13755}\",\n\t\"{1562}\", \"{319}\", \"{6386}\", \"{43430}\", \"{3219}\", \"{8606}\", \"{2812}\", \"{48633}\", \"{52480}\", \"{3405}\",\n\t\"{13149}\", \"{1593}\", \"{505}\", \"{40940}\", \"{29918}\", \"{7496}\", \"{6881}\", \"{58}\", \"{4917}\", \"{55992}\",\n\t\"{1888}\", \"{5500}\", \"{2420}\", \"{15788}\", \"{49616}\", \"{3837}\", \"{5429}\", \"{12781}\", \"{937}\", \"{40572}\",\n\t\"{47860}\", \"{8133}\", \"{38386}\", \"{14069}\", \"{1257}\", \"{19868}\", \"{67974}\", \"{5932}\", \"{230}\", \"{4339}\",\n\t\"{19899}\", \"{18263}\", \"{14098}\", \"{3330}\", \"{7855}\", \"{47891}\", \"{40583}\", \"{5235}\", \"{1950}\", \"{41994}\",\n\t\"{9623}\", \"{43802}\", \"{6959}\", \"{39496}\", \"{21303}\", \"{5068}\", \"{40133}\", \"{5685}\", \"{23478}\", \"{2061}\",\n\t\"{9293}\", \"{8869}\", \"{50693}\", \"{1616}\", \"{680}\", \"{4789}\", \"{8572}\", \"{82849}\", \"{14428}\", \"{3780}\",\n\t\"{3771}\", \"{22368}\", \"{48547}\", \"{8583}\", \"{4778}\", \"{671}\", \"{4195}\", \"{41623}\", \"{8898}\", \"{9262}\",\n\t\"{2090}\", \"{15338}\", \"{5674}\", \"{51183}\", \"{5099}\", \"{12331}\", \"{40602}\", \"{19603}\", \"{17383}\", \"{5759}\",\n\t\"{10567}\", \"{49566}\", \"{23349}\", \"{2750}\", \"{13310}\", \"{21929}\", \"{44691}\", \"{1127}\", \"{14319}\", \"{26920}\",\n\t\"{8243}\", \"{48287}\", \"{9848}\", \"{11277}\", \"{3040}\", \"{22459}\", \"{17884}\", \"{41112}\", \"{4049}\", \"{140}\",\n\t\"{8944}\", \"{15409}\", \"{49597}\", \"{9553}\", \"{16988}\", \"{12400}\", \"{5145}\", \"{45181}\", \"{49127}\", \"{6874}\",\n\t\"{2311}\", \"{23708}\", \"{19242}\", \"{40243}\", \"{5318}\", \"{21073}\", \"{6382}\", \"{7978}\", \"{42823}\", \"{8602}\",\n\t\"{62937}\", \"{13751}\", \"{1566}\", \"{55291}\", \"{41553}\", \"{1597}\", \"{501}\", \"{4408}\", \"{2816}\", \"{48637}\",\n\t\"{7989}\", \"{3401}\", \"{4913}\", \"{55996}\", \"{18849}\", \"{5504}\", \"{15048}\", \"{7492}\", \"{6885}\", \"{46841}\",\n\t\"{41961}\", \"{12785}\", \"{933}\", \"{12168}\", \"{2424}\", \"{47392}\", \"{49612}\", \"{3833}\", \"{1253}\", \"{428}\",\n\t\"{67970}\", \"{5936}\", \"{3528}\", \"{8137}\", \"{38382}\", \"{28939}\", \"{39989}\", \"{3334}\", \"{7851}\", \"{47895}\",\n\t\"{234}\", \"{45903}\", \"{13678}\", \"{18267}\", \"{9627}\", \"{2238}\", \"{42411}\", \"{39492}\", \"{12199}\", \"{5231}\",\n\t\"{1954}\", \"{41990}\", \"{56792}\", \"{2065}\", \"{9297}\", \"{2688}\", \"{21307}\", \"{69347}\", \"{12529}\", \"{5681}\",\n\t\"{8576}\", \"{3169}\", \"{43740}\", \"{3784}\", \"{25579}\", \"{1612}\", \"{684}\", \"{105912}\", \"{68457}\", \"{675}\",\n\t\"{4191}\", \"{13239}\", \"{3775}\", \"{49954}\", \"{3198}\", \"{8587}\", \"{5670}\", \"{24269}\", \"{101970}\", \"{12335}\",\n\t\"{2679}\", \"{9266}\", \"{2094}\", \"{42050}\", \"{41271}\", \"{18270}\", \"{223}\", \"{1458}\", \"{7846}\", \"{38589}\",\n\t\"{27048}\", \"{3323}\", \"{1943}\", \"{41987}\", \"{40590}\", \"{5226}\", \"{10018}\", \"{39485}\", \"{9630}\", \"{28229}\",\n\t\"{39299}\", \"{3824}\", \"{2433}\", \"{26758}\", \"{924}\", \"{40561}\", \"{41976}\", \"{12792}\", \"{38395}\", \"{2928}\",\n\t\"{29539}\", \"{8120}\", \"{67967}\", \"{5921}\", \"{1244}\", \"{41280}\", \"{29189}\", \"{8590}\", \"{3762}\", \"{27409}\",\n\t\"{4186}\", \"{41630}\", \"{1019}\", \"{662}\", \"{2083}\", \"{10459}\", \"{28668}\", \"{9271}\", \"{101967}\", \"{12322}\",\n\t\"{5667}\", \"{51190}\", \"{40120}\", \"{5696}\", \"{21310}\", \"{13929}\", \"{9280}\", \"{28699}\", \"{26319}\", \"{2072}\",\n\t\"{693}\", \"{24979}\", \"{50680}\", \"{1605}\", \"{11349}\", \"{3793}\", \"{8561}\", \"{29178}\", \"{68375}\", \"{157}\",\n\t\"{17893}\", \"{16269}\", \"{3057}\", \"{8448}\", \"{23859}\", \"{11260}\", \"{5152}\", \"{17588}\", \"{19008}\", \"{12417}\",\n\t\"{7629}\", \"{9544}\", \"{8953}\", \"{42772}\", \"{9358}\", \"{2747}\", \"{10570}\", \"{49571}\", \"{17394}\", \"{69465}\",\n\t\"{17579}\", \"{19614}\", \"{8254}\", \"{6139}\", \"{43062}\", \"{26937}\", \"{16298}\", \"{1130}\", \"{13307}\", \"{18718}\",\n\t\"{8009}\", \"{3416}\", \"{2801}\", \"{6589}\", \"{516}\", \"{40953}\", \"{5808}\", \"{1580}\", \"{6892}\", \"{7268}\",\n\t\"{42333}\", \"{7485}\", \"{21678}\", \"{5513}\", \"{4904}\", \"{19449}\", \"{44931}\", \"{21064}\", \"{19255}\", \"{17138}\",\n\t\"{2306}\", \"{9719}\", \"{7299}\", \"{6863}\", \"{1571}\", \"{20168}\", \"{18359}\", \"{13746}\", \"{6578}\", \"{8615}\",\n\t\"{6395}\", \"{43423}\", \"{7842}\", \"{47886}\", \"{8738}\", \"{3327}\", \"{16119}\", \"{18274}\", \"{227}\", \"{18499}\",\n\t\"{42402}\", \"{39481}\", \"{9634}\", \"{7559}\", \"{1947}\", \"{19378}\", \"{21149}\", \"{5222}\", \"{920}\", \"{4829}\",\n\t\"{19389}\", \"{12796}\", \"{49601}\", \"{3820}\", \"{2437}\", \"{9028}\", \"{18468}\", \"{5925}\", \"{1240}\", \"{20659}\",\n\t\"{38391}\", \"{43312}\", \"{6249}\", \"{8124}\", \"{4182}\", \"{16558}\", \"{68444}\", \"{666}\", \"{48550}\", \"{8594}\",\n\t\"{3766}\", \"{8379}\", \"{19739}\", \"{12326}\", \"{5663}\", \"{21508}\", \"{2087}\", \"{9498}\", \"{7118}\", \"{9275}\",\n\t\"{9284}\", \"{22878}\", \"{9469}\", \"{2076}\", \"{17248}\", \"{5692}\", \"{21314}\", \"{69354}\", \"{8388}\", \"{3797}\",\n\t\"{8565}\", \"{6608}\", \"{697}\", \"{18029}\", \"{20218}\", \"{1601}\", \"{3053}\", \"{11489}\", \"{48265}\", \"{11264}\",\n\t\"{1728}\", \"{153}\", \"{17897}\", \"{41101}\", \"{28159}\", \"{9540}\", \"{8957}\", \"{10368}\", \"{5156}\", \"{45192}\",\n\t\"{25958}\", \"{12413}\", \"{17390}\", \"{69461}\", \"{40611}\", \"{19610}\", \"{10399}\", \"{2743}\", \"{10574}\", \"{49575}\",\n\t\"{44682}\", \"{1134}\", \"{13303}\", \"{68380}\", \"{8250}\", \"{29649}\", \"{11478}\", \"{26933}\", \"{512}\", \"{1369}\",\n\t\"{41540}\", \"{1584}\", \"{27779}\", \"{3412}\", \"{2805}\", \"{48624}\", \"{54592}\", \"{5517}\", \"{4900}\", \"{809}\",\n\t\"{6896}\", \"{28518}\", \"{3909}\", \"{7481}\", \"{2302}\", \"{26069}\", \"{49134}\", \"{6867}\", \"{44935}\", \"{21060}\",\n\t\"{19251}\", \"{40250}\", \"{29208}\", \"{8611}\", \"{6391}\", \"{11039}\", \"{1575}\", \"{55282}\", \"{1398}\", \"{13742}\",\n\t\"{17019}\", \"{19374}\", \"{12186}\", \"{19599}\", \"{6942}\", \"{46986}\", \"{9638}\", \"{2227}\", \"{13667}\", \"{18278}\",\n\t\"{20049}\", \"{1450}\", \"{43502}\", \"{38581}\", \"{8734}\", \"{6459}\", \"{48701}\", \"{2920}\", \"{3537}\", \"{8128}\",\n\t\"{18464}\", \"{5929}\", \"{18289}\", \"{437}\", \"{39291}\", \"{42212}\", \"{7349}\", \"{9024}\", \"{19568}\", \"{4825}\",\n\t\"{5432}\", \"{21759}\", \"{49450}\", \"{9494}\", \"{2666}\", \"{9279}\", \"{5082}\", \"{17458}\", \"{69544}\", \"{21504}\",\n\t\"{3187}\", \"{8598}\", \"{6018}\", \"{8375}\", \"{18639}\", \"{13226}\", \"{1011}\", \"{20408}\", \"{16348}\", \"{4792}\",\n\t\"{20214}\", \"{68254}\", \"{8384}\", \"{23978}\", \"{8569}\", \"{3176}\", \"{12536}\", \"{19129}\", \"{21318}\", \"{5073}\",\n\t\"{9288}\", \"{2697}\", \"{9465}\", \"{7708}\", \"{13808}\", \"{17580}\", \"{16997}\", \"{40001}\", \"{2153}\", \"{10589}\",\n\t\"{49365}\", \"{10364}\", \"{1724}\", \"{44092}\", \"{24858}\", \"{13513}\", \"{29059}\", \"{8440}\", \"{9857}\", \"{11268}\",\n\t\"{11299}\", \"{3643}\", \"{11474}\", \"{48475}\", \"{743}\", \"{1138}\", \"{41711}\", \"{18710}\", \"{9350}\", \"{28749}\",\n\t\"{10578}\", \"{27833}\", \"{45782}\", \"{5746}\", \"{12203}\", \"{69280}\", \"{26679}\", \"{43}\", \"{3905}\", \"{49724}\",\n\t\"{1893}\", \"{41857}\", \"{40440}\", \"{805}\", \"{7996}\", \"{29418}\", \"{2809}\", \"{6581}\", \"{55492}\", \"{1365}\",\n\t\"{5800}\", \"{1588}\", \"{1579}\", \"{302}\", \"{1394}\", \"{41350}\", \"{3202}\", \"{27169}\", \"{48034}\", \"{7967}\",\n\t\"{5307}\", \"{54382}\", \"{63824}\", \"{1862}\", \"{28308}\", \"{9711}\", \"{7291}\", \"{10139}\", \"{6946}\", \"{39489}\",\n\t\"{26148}\", \"{2223}\", \"{40371}\", \"{19370}\", \"{12182}\", \"{44814}\", \"{11118}\", \"{38585}\", \"{8730}\", \"{29329}\",\n\t\"{13663}\", \"{40887}\", \"{41490}\", \"{1454}\", \"{18460}\", \"{41461}\", \"{1248}\", \"{433}\", \"{38399}\", \"{2924}\",\n\t\"{3533}\", \"{27658}\", \"{928}\", \"{4821}\", \"{5436}\", \"{40380}\", \"{39295}\", \"{3828}\", \"{28439}\", \"{9020}\",\n\t\"{5086}\", \"{40730}\", \"{25888}\", \"{21500}\", \"{28089}\", \"{9490}\", \"{2662}\", \"{26509}\", \"{100867}\", \"{13222}\",\n\t\"{1015}\", \"{50090}\", \"{3183}\", \"{11559}\", \"{29768}\", \"{8371}\", \"{8380}\", \"{29799}\", \"{27219}\", \"{3172}\",\n\t\"{41020}\", \"{4796}\", \"{20210}\", \"{1609}\", \"{10249}\", \"{2693}\", \"{9461}\", \"{28078}\", \"{12532}\", \"{25879}\",\n\t\"{51780}\", \"{5077}\", \"{2157}\", \"{9548}\", \"{22959}\", \"{10360}\", \"{69275}\", \"{17584}\", \"{16993}\", \"{17369}\",\n\t\"{6729}\", \"{8444}\", \"{9853}\", \"{43672}\", \"{1720}\", \"{16488}\", \"{18108}\", \"{13517}\", \"{747}\", \"{68565}\",\n\t\"{16479}\", \"{18714}\", \"{8258}\", \"{3647}\", \"{11470}\", \"{48471}\", \"{17398}\", \"{5742}\", \"{12207}\", \"{19618}\",\n\t\"{9354}\", \"{7039}\", \"{42162}\", \"{27837}\", \"{1897}\", \"{41853}\", \"{4908}\", \"{801}\", \"{9109}\", \"{47}\",\n\t\"{3901}\", \"{7489}\", \"{20778}\", \"{1361}\", \"{5804}\", \"{18549}\", \"{7992}\", \"{6368}\", \"{43233}\", \"{6585}\",\n\t\"{3206}\", \"{8619}\", \"{6399}\", \"{7963}\", \"{45831}\", \"{306}\", \"{1390}\", \"{16038}\", \"{7478}\", \"{9715}\",\n\t\"{7295}\", \"{42523}\", \"{5303}\", \"{21068}\", \"{19259}\", \"{1866}\", \"{4129}\", \"{13281}\", \"{18073}\", \"{41072}\",\n\t\"{3120}\", \"{14288}\", \"{9928}\", \"{11317}\", \"{5025}\", \"{40793}\", \"{104857}\", \"{12560}\", \"{83908}\", \"{9433}\",\n\t\"{8824}\", \"{15569}\", \"{15598}\", \"{2630}\", \"{10407}\", \"{49406}\", \"{12591}\", \"{5639}\", \"{40762}\", \"{13986}\",\n\t\"{8323}\", \"{108746}\", \"{14279}\", \"{15883}\", \"{41083}\", \"{1047}\", \"{13270}\", \"{21849}\", \"{22178}\", \"{3561}\",\n\t\"{2976}\", \"{48757}\", \"{461}\", \"{4568}\", \"{41433}\", \"{4385}\", \"{9072}\", \"{46921}\", \"{15128}\", \"{2280}\",\n\t\"{18929}\", \"{5464}\", \"{4873}\", \"{5289}\", \"{5278}\", \"{4882}\", \"{5495}\", \"{40323}\", \"{2271}\", \"{23668}\",\n\t\"{49047}\", \"{6914}\", \"{1406}\", \"{50483}\", \"{4599}\", \"{490}\", \"{42943}\", \"{2987}\", \"{3590}\", \"{7818}\",\n\t\"{13718}\", \"{18307}\", \"{354}\", \"{45863}\", \"{7931}\", \"{48062}\", \"{43290}\", \"{3254}\", \"{1834}\", \"{63872}\",\n\t\"{24748}\", \"{5351}\", \"{42571}\", \"{89274}\", \"{9747}\", \"{2358}\", \"{43997}\", \"{3953}\", \"{15}\", \"{42580}\",\n\t\"{853}\", \"{12008}\", \"{41801}\", \"{17197}\", \"{60819}\", \"{28859}\", \"{3448}\", \"{8057}\", \"{45892}\", \"{5856}\",\n\t\"{1333}\", \"{548}\", \"{26969}\", \"{11422}\", \"{3615}\", \"{49834}\", \"{1783}\", \"{13359}\", \"{68537}\", \"{715}\",\n\t\"{7686}\", \"{42130}\", \"{2719}\", \"{9306}\", \"{78297}\", \"{12255}\", \"{5710}\", \"{24309}\", \"{12449}\", \"{19056}\",\n\t\"{21267}\", \"{69227}\", \"{10332}\", \"{49333}\", \"{53180}\", \"{2105}\", \"{13545}\", \"{79587}\", \"{109}\", \"{1772}\",\n\t\"{43620}\", \"{6196}\", \"{8416}\", \"{3009}\", \"{3124}\", \"{46692}\", \"{48312}\", \"{11313}\", \"{68206}\", \"{13285}\",\n\t\"{18077}\", \"{13468}\", \"{2028}\", \"{9437}\", \"{8820}\", \"{42601}\", \"{5021}\", \"{12389}\", \"{104853}\", \"{12564}\",\n\t\"{12595}\", \"{69516}\", \"{12378}\", \"{13982}\", \"{47182}\", \"{2634}\", \"{10403}\", \"{27948}\", \"{638}\", \"{1043}\",\n\t\"{13274}\", \"{100831}\", \"{8327}\", \"{3738}\", \"{43111}\", \"{15887}\", \"{465}\", \"{40820}\", \"{288}\", \"{4381}\",\n\t\"{57292}\", \"{3565}\", \"{2972}\", \"{3388}\", \"{24079}\", \"{5460}\", \"{4877}\", \"{66831}\", \"{9076}\", \"{2469}\",\n\t\"{29878}\", \"{2284}\", \"{2275}\", \"{29889}\", \"{2498}\", \"{6910}\", \"{44842}\", \"{4886}\", \"{5491}\", \"{1919}\",\n\t\"{3379}\", \"{2983}\", \"{3594}\", \"{43550}\", \"{1402}\", \"{279}\", \"{51890}\", \"{494}\", \"{7935}\", \"{48066}\",\n\t\"{22649}\", \"{3250}\", \"{41302}\", \"{18303}\", \"{350}\", \"{4259}\", \"{6839}\", \"{89270}\", \"{9743}\", \"{43962}\",\n\t\"{1830}\", \"{63876}\", \"{45391}\", \"{5355}\", \"{857}\", \"{40412}\", \"{5549}\", \"{17193}\", \"{43993}\", \"{3957}\",\n\t\"{11}\", \"{23159}\", \"{45896}\", \"{5852}\", \"{1337}\", \"{19908}\", \"{88560}\", \"{14109}\", \"{47900}\", \"{8053}\",\n\t\"{1787}\", \"{41743}\", \"{4618}\", \"{711}\", \"{48427}\", \"{11426}\", \"{3611}\", \"{22208}\", \"{20868}\", \"{12251}\",\n\t\"{5714}\", \"{54791}\", \"{7682}\", \"{15258}\", \"{58387}\", \"{9302}\", \"{10336}\", \"{8909}\", \"{23518}\", \"{2101}\",\n\t\"{40053}\", \"{19052}\", \"{21263}\", \"{5108}\", \"{14548}\", \"{6192}\", \"{8412}\", \"{59497}\", \"{13541}\", \"{79583}\",\n\t\"{55081}\", \"{1776}\", \"{69306}\", \"{12385}\", \"{19177}\", \"{12568}\", \"{2024}\", \"{47792}\", \"{49212}\", \"{10213}\",\n\t\"{1653}\", \"{13289}\", \"{105953}\", \"{13464}\", \"{3128}\", \"{8537}\", \"{9920}\", \"{43701}\", \"{46082}\", \"{3734}\",\n\t\"{11503}\", \"{26848}\", \"{634}\", \"{68416}\", \"{13278}\", \"{12882}\", \"{9227}\", \"{2638}\", \"{42011}\", \"{14987}\",\n\t\"{12599}\", \"{5631}\", \"{12374}\", \"{101931}\", \"{56392}\", \"{2465}\", \"{3872}\", \"{2288}\", \"{18921}\", \"{41920}\",\n\t\"{12129}\", \"{972}\", \"{8176}\", \"{3569}\", \"{28978}\", \"{3384}\", \"{469}\", \"{1212}\", \"{284}\", \"{67931}\",\n\t\"{45942}\", \"{275}\", \"{4591}\", \"{498}\", \"{3375}\", \"{28989}\", \"{3598}\", \"{7810}\", \"{983}\", \"{24669}\",\n\t\"{50990}\", \"{1915}\", \"{2279}\", \"{3883}\", \"{2494}\", \"{42450}\", \"{40202}\", \"{19203}\", \"{17783}\", \"{5359}\",\n\t\"{6835}\", \"{49166}\", \"{23749}\", \"{2350}\", \"{13710}\", \"{62976}\", \"{44291}\", \"{1527}\", \"{7939}\", \"{88370}\",\n\t\"{8643}\", \"{42862}\", \"{42893}\", \"{2857}\", \"{3440}\", \"{22059}\", \"{18513}\", \"{41512}\", \"{4449}\", \"{540}\",\n\t\"{89460}\", \"{15009}\", \"{46800}\", \"{9153}\", \"{44996}\", \"{4952}\", \"{5545}\", \"{18808}\", \"{49527}\", \"{10526}\",\n\t\"{2711}\", \"{23308}\", \"{19642}\", \"{40643}\", \"{5718}\", \"{21473}\", \"{6782}\", \"{14358}\", \"{59287}\", \"{8202}\",\n\t\"{21968}\", \"{13351}\", \"{1166}\", \"{55691}\", \"{41153}\", \"{1197}\", \"{101}\", \"{4008}\", \"{11236}\", \"{9809}\",\n\t\"{22418}\", \"{3001}\", \"{12441}\", \"{78483}\", \"{54181}\", \"{5104}\", \"{15448}\", \"{7092}\", \"{9512}\", \"{58597}\",\n\t\"{2020}\", \"{15388}\", \"{8828}\", \"{10217}\", \"{5029}\", \"{12381}\", \"{19173}\", \"{40172}\", \"{82808}\", \"{8533}\",\n\t\"{9924}\", \"{14469}\", \"{1657}\", \"{41693}\", \"{105957}\", \"{13460}\", \"{630}\", \"{4739}\", \"{41662}\", \"{12886}\",\n\t\"{14498}\", \"{3730}\", \"{11507}\", \"{48506}\", \"{40183}\", \"{5635}\", \"{12370}\", \"{20949}\", \"{9223}\", \"{109646}\",\n\t\"{15379}\", \"{14983}\", \"{18925}\", \"{5468}\", \"{40533}\", \"{976}\", \"{23078}\", \"{2461}\", \"{3876}\", \"{49657}\",\n\t\"{19829}\", \"{1216}\", \"{280}\", \"{4389}\", \"{8172}\", \"{47821}\", \"{14028}\", \"{3380}\", \"{3371}\", \"{22768}\",\n\t\"{48147}\", \"{7814}\", \"{4378}\", \"{271}\", \"{4595}\", \"{41223}\", \"{43843}\", \"{3887}\", \"{2490}\", \"{6918}\",\n\t\"{987}\", \"{51583}\", \"{5499}\", \"{1911}\", \"{6831}\", \"{49162}\", \"{42390}\", \"{2354}\", \"{1838}\", \"{19207}\",\n\t\"{17787}\", \"{44963}\", \"{43471}\", \"{88374}\", \"{8647}\", \"{3258}\", \"{13714}\", \"{62972}\", \"{358}\", \"{1523}\",\n\t\"{18517}\", \"{13108}\", \"{40901}\", \"{544}\", \"{42897}\", \"{2853}\", \"{3444}\", \"{43480}\", \"{44992}\", \"{4956}\",\n\t\"{5541}\", \"{24158}\", \"{61919}\", \"{29959}\", \"{19}\", \"{9157}\", \"{19646}\", \"{12259}\", \"{69437}\", \"{21477}\",\n\t\"{27869}\", \"{10522}\", \"{2715}\", \"{48934}\", \"{79397}\", \"{13355}\", \"{1162}\", \"{719}\", \"{6786}\", \"{43030}\",\n\t\"{3619}\", \"{8206}\", \"{11232}\", \"{48233}\", \"{52080}\", \"{3005}\", \"{13549}\", \"{1193}\", \"{105}\", \"{68327}\",\n\t\"{42720}\", \"{7096}\", \"{9516}\", \"{2109}\", \"{12445}\", \"{78487}\", \"{24519}\", \"{5100}\", \"{3453}\", \"{11089}\",\n\t\"{42880}\", \"{2844}\", \"{1328}\", \"{553}\", \"{18500}\", \"{41501}\", \"{28559}\", \"{9140}\", \"{49769}\", \"{3948}\",\n\t\"{5556}\", \"{45592}\", \"{848}\", \"{4941}\", \"{17790}\", \"{44974}\", \"{40211}\", \"{19210}\", \"{10799}\", \"{2343}\",\n\t\"{6826}\", \"{49175}\", \"{44282}\", \"{1534}\", \"{13703}\", \"{62965}\", \"{8650}\", \"{29249}\", \"{11078}\", \"{48079}\",\n\t\"{112}\", \"{1769}\", \"{41140}\", \"{1184}\", \"{27379}\", \"{3012}\", \"{11225}\", \"{48224}\", \"{54192}\", \"{5117}\",\n\t\"{12452}\", \"{25919}\", \"{9501}\", \"{28118}\", \"{10329}\", \"{7081}\", \"{2702}\", \"{26469}\", \"{49534}\", \"{10535}\",\n\t\"{69420}\", \"{21460}\", \"{19651}\", \"{40650}\", \"{29608}\", \"{8211}\", \"{6791}\", \"{11439}\", \"{1175}\", \"{55682}\",\n\t\"{1798}\", \"{13342}\", \"{11510}\", \"{15898}\", \"{8338}\", \"{3727}\", \"{16519}\", \"{12891}\", \"{627}\", \"{18099}\",\n\t\"{42002}\", \"{14994}\", \"{9234}\", \"{7159}\", \"{12367}\", \"{19778}\", \"{21549}\", \"{5622}\", \"{19164}\", \"{17209}\",\n\t\"{19789}\", \"{12396}\", \"{22839}\", \"{10200}\", \"{2037}\", \"{9428}\", \"{18068}\", \"{13477}\", \"{1640}\", \"{20259}\",\n\t\"{9933}\", \"{43712}\", \"{6649}\", \"{8524}\", \"{4582}\", \"{16158}\", \"{45951}\", \"{266}\", \"{48150}\", \"{7803}\",\n\t\"{3366}\", \"{8779}\", \"{19339}\", \"{1906}\", \"{990}\", \"{4899}\", \"{2487}\", \"{9098}\", \"{7518}\", \"{3890}\",\n\t\"{3861}\", \"{49640}\", \"{9069}\", \"{2476}\", \"{4868}\", \"{961}\", \"{18932}\", \"{41933}\", \"{8788}\", \"{3397}\",\n\t\"{8165}\", \"{6208}\", \"{297}\", \"{18429}\", \"{20618}\", \"{1201}\", \"{40912}\", \"{557}\", \"{18504}\", \"{5849}\",\n\t\"{3457}\", \"{8048}\", \"{42884}\", \"{2840}\", \"{5552}\", \"{17188}\", \"{19408}\", \"{4945}\", \"{7229}\", \"{9144}\",\n\t\"{43988}\", \"{42372}\", \"{9758}\", \"{2347}\", \"{6822}\", \"{49171}\", \"{17794}\", \"{44970}\", \"{17179}\", \"{19214}\",\n\t\"{8654}\", \"{6539}\", \"{43462}\", \"{65968}\", \"{16698}\", \"{1530}\", \"{13707}\", \"{18318}\", \"{8409}\", \"{3016}\",\n\t\"{11221}\", \"{6189}\", \"{116}\", \"{68334}\", \"{16228}\", \"{1180}\", \"{9505}\", \"{7668}\", \"{42733}\", \"{7085}\",\n\t\"{21278}\", \"{5113}\", \"{12456}\", \"{19049}\", \"{69424}\", \"{21464}\", \"{19655}\", \"{17538}\", \"{2706}\", \"{9319}\",\n\t\"{7699}\", \"{10531}\", \"{1171}\", \"{20568}\", \"{18759}\", \"{13346}\", \"{6178}\", \"{8215}\", \"{6795}\", \"{43023}\",\n\t\"{41671}\", \"{12895}\", \"{623}\", \"{1058}\", \"{11514}\", \"{38189}\", \"{27448}\", \"{3723}\", \"{12363}\", \"{13999}\",\n\t\"{40190}\", \"{5626}\", \"{10418}\", \"{14990}\", \"{9230}\", \"{28629}\", \"{39699}\", \"{10204}\", \"{2033}\", \"{26358}\",\n\t\"{19160}\", \"{40161}\", \"{13968}\", \"{12392}\", \"{9937}\", \"{11308}\", \"{29139}\", \"{8520}\", \"{24938}\", \"{13473}\",\n\t\"{1644}\", \"{41680}\", \"{29589}\", \"{7807}\", \"{3362}\", \"{2998}\", \"{4586}\", \"{41230}\", \"{1419}\", \"{262}\",\n\t\"{2483}\", \"{10059}\", \"{28268}\", \"{3894}\", \"{50987}\", \"{1902}\", \"{994}\", \"{51590}\", \"{40520}\", \"{965}\",\n\t\"{18936}\", \"{41937}\", \"{3865}\", \"{28299}\", \"{26719}\", \"{2472}\", \"{293}\", \"{67926}\", \"{50280}\", \"{1205}\",\n\t\"{2969}\", \"{3393}\", \"{8161}\", \"{29578}\", \"{2557}\", \"{9148}\", \"{43984}\", \"{3940}\", \"{41812}\", \"{17184}\",\n\t\"{840}\", \"{4949}\", \"{6329}\", \"{8044}\", \"{42888}\", \"{43272}\", \"{1320}\", \"{16088}\", \"{18508}\", \"{5845}\",\n\t\"{347}\", \"{45870}\", \"{16079}\", \"{18314}\", \"{8658}\", \"{3247}\", \"{7922}\", \"{48071}\", \"{17798}\", \"{5342}\",\n\t\"{1827}\", \"{19218}\", \"{9754}\", \"{7439}\", \"{42562}\", \"{64868}\", \"{21274}\", \"{69234}\", \"{17328}\", \"{19045}\",\n\t\"{9509}\", \"{2116}\", \"{10321}\", \"{7089}\", \"{20378}\", \"{1761}\", \"{13556}\", \"{18149}\", \"{8405}\", \"{6768}\",\n\t\"{43633}\", \"{6185}\", \"{3606}\", \"{8219}\", \"{6799}\", \"{11431}\", \"{68524}\", \"{706}\", \"{1790}\", \"{16438}\",\n\t\"{7078}\", \"{9315}\", \"{7695}\", \"{42123}\", \"{5703}\", \"{21468}\", \"{19659}\", \"{12246}\", \"{10414}\", \"{39089}\",\n\t\"{26548}\", \"{2623}\", \"{40771}\", \"{13995}\", \"{12582}\", \"{34888}\", \"{11518}\", \"{15890}\", \"{8330}\", \"{29729}\",\n\t\"{13263}\", \"{12899}\", \"{41090}\", \"{1054}\", \"{18060}\", \"{41061}\", \"{1648}\", \"{13292}\", \"{38799}\", \"{11304}\",\n\t\"{3133}\", \"{27258}\", \"{25838}\", \"{12573}\", \"{5036}\", \"{40780}\", \"{8837}\", \"{10208}\", \"{28039}\", \"{9420}\",\n\t\"{5486}\", \"{40330}\", \"{998}\", \"{4891}\", \"{28489}\", \"{6907}\", \"{2262}\", \"{3898}\", \"{51887}\", \"{483}\",\n\t\"{1415}\", \"{50490}\", \"{3583}\", \"{11159}\", \"{29368}\", \"{2994}\", \"{2965}\", \"{29399}\", \"{27619}\", \"{3572}\",\n\t\"{41420}\", \"{4396}\", \"{472}\", \"{1209}\", \"{3869}\", \"{2293}\", \"{9061}\", \"{28478}\", \"{4860}\", \"{969}\",\n\t\"{51380}\", \"{5477}\", \"{41816}\", \"{17180}\", \"{844}\", \"{40401}\", \"{2553}\", \"{10189}\", \"{43980}\", \"{3944}\",\n\t\"{1324}\", \"{44492}\", \"{45885}\", \"{5841}\", \"{29459}\", \"{8040}\", \"{48669}\", \"{2848}\", \"{11699}\", \"{3243}\",\n\t\"{7926}\", \"{48075}\", \"{343}\", \"{1538}\", \"{41311}\", \"{18310}\", \"{9750}\", \"{28349}\", \"{10178}\", \"{49179}\",\n\t\"{45382}\", \"{5346}\", \"{1823}\", \"{63865}\", \"{26279}\", \"{2112}\", \"{10325}\", \"{49324}\", \"{21270}\", \"{13849}\",\n\t\"{40040}\", \"{19041}\", \"{8401}\", \"{29018}\", \"{11229}\", \"{6181}\", \"{55092}\", \"{1765}\", \"{13552}\", \"{1188}\",\n\t\"{1179}\", \"{702}\", \"{1794}\", \"{41750}\", \"{3602}\", \"{27569}\", \"{48434}\", \"{11435}\", \"{5707}\", \"{54782}\",\n\t\"{78280}\", \"{12242}\", \"{28708}\", \"{9311}\", \"{7691}\", \"{10539}\", \"{17419}\", \"{13991}\", \"{12586}\", \"{19199}\",\n\t\"{10410}\", \"{14998}\", \"{9238}\", \"{2627}\", \"{13267}\", \"{18678}\", \"{20449}\", \"{1050}\", \"{43102}\", \"{15894}\",\n\t\"{8334}\", \"{6059}\", \"{23939}\", \"{11300}\", \"{3137}\", \"{8528}\", \"{18064}\", \"{16309}\", \"{18689}\", \"{13296}\",\n\t\"{8833}\", \"{42612}\", \"{7749}\", \"{9424}\", \"{19168}\", \"{12577}\", \"{5032}\", \"{21359}\", \"{49050}\", \"{6903}\",\n\t\"{2266}\", \"{9679}\", \"{5482}\", \"{17058}\", \"{44851}\", \"{4895}\", \"{3587}\", \"{8198}\", \"{6418}\", \"{2990}\",\n\t\"{18239}\", \"{487}\", \"{1411}\", \"{5999}\", \"{5968}\", \"{4392}\", \"{476}\", \"{40833}\", \"{2961}\", \"{48740}\",\n\t\"{8169}\", \"{3576}\", \"{4864}\", \"{19529}\", \"{21718}\", \"{5473}\", \"{9688}\", \"{2297}\", \"{9065}\", \"{7308}\",\n\t\"{130}\", \"{4039}\", \"{41162}\", \"{18163}\", \"{14398}\", \"{3030}\", \"{11207}\", \"{9838}\", \"{40683}\", \"{5135}\",\n\t\"{12470}\", \"{104947}\", \"{9523}\", \"{83818}\", \"{15479}\", \"{8934}\", \"{2720}\", \"{15488}\", \"{49516}\", \"{10517}\",\n\t\"{5729}\", \"{12481}\", \"{13896}\", \"{40672}\", \"{108656}\", \"{8233}\", \"{15993}\", \"{14369}\", \"{1157}\", \"{41193}\",\n\t\"{21959}\", \"{13360}\", \"{3471}\", \"{22068}\", \"{48647}\", \"{2866}\", \"{4478}\", \"{571}\", \"{4295}\", \"{41523}\",\n\t\"{46831}\", \"{9162}\", \"{2390}\", \"{15038}\", \"{5574}\", \"{18839}\", \"{5399}\", \"{4963}\", \"{4992}\", \"{5368}\",\n\t\"{40233}\", \"{5585}\", \"{23778}\", \"{2361}\", \"{6804}\", \"{49157}\", \"{50593}\", \"{1516}\", \"{580}\", \"{4489}\",\n\t\"{2897}\", \"{42853}\", \"{7908}\", \"{3480}\", \"{18217}\", \"{13608}\", \"{45973}\", \"{244}\", \"{48172}\", \"{7821}\",\n\t\"{3344}\", \"{43380}\", \"{63962}\", \"{1924}\", \"{5241}\", \"{24658}\", \"{89364}\", \"{42461}\", \"{2248}\", \"{9657}\",\n\t\"{3843}\", \"{43887}\", \"{42490}\", \"{2454}\", \"{12118}\", \"{943}\", \"{17087}\", \"{41911}\", \"{28949}\", \"{60909}\",\n\t\"{8147}\", \"{3558}\", \"{5946}\", \"{45982}\", \"{458}\", \"{1223}\", \"{11532}\", \"{26879}\", \"{49924}\", \"{3705}\",\n\t\"{13249}\", \"{1693}\", \"{605}\", \"{68427}\", \"{42020}\", \"{7796}\", \"{9216}\", \"{2609}\", \"{12345}\", \"{78387}\",\n\t\"{24219}\", \"{5600}\", \"{19146}\", \"{12559}\", \"{69337}\", \"{21377}\", \"{49223}\", \"{10222}\", \"{2015}\", \"{53090}\",\n\t\"{79497}\", \"{13455}\", \"{1662}\", \"{25509}\", \"{6086}\", \"{43730}\", \"{3119}\", \"{8506}\", \"{46782}\", \"{3034}\",\n\t\"{11203}\", \"{48202}\", \"{134}\", \"{68316}\", \"{13578}\", \"{18167}\", \"{9527}\", \"{2138}\", \"{42711}\", \"{8930}\",\n\t\"{12299}\", \"{5131}\", \"{12474}\", \"{104943}\", \"{69406}\", \"{12485}\", \"{13892}\", \"{12268}\", \"{2724}\", \"{47092}\",\n\t\"{27858}\", \"{10513}\", \"{1153}\", \"{728}\", \"{100921}\", \"{13364}\", \"{3628}\", \"{8237}\", \"{15997}\", \"{43001}\",\n\t\"{40930}\", \"{575}\", \"{4291}\", \"{398}\", \"{3475}\", \"{57382}\", \"{3298}\", \"{2862}\", \"{5570}\", \"{24169}\",\n\t\"{66921}\", \"{4967}\", \"{28}\", \"{9166}\", \"{2394}\", \"{29968}\", \"{29999}\", \"{2365}\", \"{6800}\", \"{2588}\",\n\t\"{4996}\", \"{44952}\", \"{1809}\", \"{5581}\", \"{2893}\", \"{3269}\", \"{43440}\", \"{3484}\", \"{369}\", \"{1512}\",\n\t\"{584}\", \"{51980}\", \"{48176}\", \"{7825}\", \"{3340}\", \"{22759}\", \"{18213}\", \"{41212}\", \"{4349}\", \"{240}\",\n\t\"{89360}\", \"{6929}\", \"{43872}\", \"{9653}\", \"{63966}\", \"{1920}\", \"{5245}\", \"{45281}\", \"{40502}\", \"{947}\",\n\t\"{17083}\", \"{5459}\", \"{3847}\", \"{43883}\", \"{23049}\", \"{2450}\", \"{5942}\", \"{45986}\", \"{19818}\", \"{1227}\",\n\t\"{14019}\", \"{88470}\", \"{8143}\", \"{47810}\", \"{41653}\", \"{1697}\", \"{601}\", \"{4708}\", \"{11536}\", \"{48537}\",\n\t\"{22318}\", \"{3701}\", \"{12341}\", \"{20978}\", \"{54681}\", \"{5604}\", \"{15348}\", \"{7792}\", \"{9212}\", \"{58297}\",\n\t\"{8819}\", \"{10226}\", \"{2011}\", \"{23408}\", \"{19142}\", \"{40143}\", \"{5018}\", \"{21373}\", \"{6082}\", \"{14458}\",\n\t\"{59587}\", \"{8502}\", \"{79493}\", \"{13451}\", \"{1666}\", \"{55191}\", \"{12295}\", \"{69216}\", \"{12478}\", \"{19067}\",\n\t\"{47682}\", \"{2134}\", \"{10303}\", \"{49302}\", \"{138}\", \"{1743}\", \"{13574}\", \"{105843}\", \"{8427}\", \"{3038}\",\n\t\"{43611}\", \"{9830}\", \"{3624}\", \"{46192}\", \"{26958}\", \"{11413}\", \"{68506}\", \"{724}\", \"{12992}\", \"{13368}\",\n\t\"{2728}\", \"{9337}\", \"{14897}\", \"{42101}\", \"{5721}\", \"{12489}\", \"{101821}\", \"{12264}\", \"{24}\", \"{56282}\",\n\t\"{2398}\", \"{3962}\", \"{41830}\", \"{18831}\", \"{862}\", \"{12039}\", \"{3479}\", \"{8066}\", \"{3294}\", \"{28868}\",\n\t\"{1302}\", \"{579}\", \"{67821}\", \"{394}\", \"{365}\", \"{45852}\", \"{588}\", \"{4481}\", \"{28899}\", \"{3265}\",\n\t\"{7900}\", \"{3488}\", \"{24779}\", \"{893}\", \"{1805}\", \"{50880}\", \"{3993}\", \"{2369}\", \"{42540}\", \"{2584}\",\n\t\"{19313}\", \"{40312}\", \"{5249}\", \"{17693}\", \"{49076}\", \"{6925}\", \"{2240}\", \"{23659}\", \"{62866}\", \"{13600}\",\n\t\"{1437}\", \"{44381}\", \"{88260}\", \"{7829}\", \"{42972}\", \"{8753}\", \"{2947}\", \"{42983}\", \"{22149}\", \"{3550}\",\n\t\"{41402}\", \"{18403}\", \"{450}\", \"{4559}\", \"{15119}\", \"{89570}\", \"{9043}\", \"{46910}\", \"{4842}\", \"{44886}\",\n\t\"{18918}\", \"{5455}\", \"{10436}\", \"{49437}\", \"{23218}\", \"{2601}\", \"{40753}\", \"{19752}\", \"{21563}\", \"{5608}\",\n\t\"{14248}\", \"{6692}\", \"{8312}\", \"{59397}\", \"{13241}\", \"{21878}\", \"{55781}\", \"{1076}\", \"{1087}\", \"{41043}\",\n\t\"{4118}\", \"{20273}\", \"{9919}\", \"{11326}\", \"{3111}\", \"{22508}\", \"{78593}\", \"{12551}\", \"{5014}\", \"{54091}\",\n\t\"{7182}\", \"{15558}\", \"{58487}\", \"{9402}\", \"{15298}\", \"{2130}\", \"{10307}\", \"{8938}\", \"{12291}\", \"{5139}\",\n\t\"{40062}\", \"{19063}\", \"{8423}\", \"{82918}\", \"{14579}\", \"{9834}\", \"{41783}\", \"{1747}\", \"{13570}\", \"{105847}\",\n\t\"{4629}\", \"{720}\", \"{12996}\", \"{41772}\", \"{3620}\", \"{14588}\", \"{48416}\", \"{11417}\", \"{5725}\", \"{40093}\",\n\t\"{20859}\", \"{12260}\", \"{109756}\", \"{9333}\", \"{14893}\", \"{15269}\", \"{5578}\", \"{18835}\", \"{866}\", \"{40423}\",\n\t\"{20}\", \"{23168}\", \"{49747}\", \"{3966}\", \"{1306}\", \"{19939}\", \"{4299}\", \"{390}\", \"{47931}\", \"{8062}\",\n\t\"{3290}\", \"{14138}\", \"{22678}\", \"{3261}\", \"{7904}\", \"{48057}\", \"{361}\", \"{4268}\", \"{41333}\", \"{4485}\",\n\t\"{3997}\", \"{43953}\", \"{6808}\", \"{2580}\", \"{51493}\", \"{897}\", \"{1801}\", \"{5589}\", \"{49072}\", \"{6921}\",\n\t\"{2244}\", \"{42280}\", \"{19317}\", \"{1928}\", \"{44873}\", \"{17697}\", \"{88264}\", \"{43561}\", \"{3348}\", \"{8757}\",\n\t\"{62862}\", \"{13604}\", \"{1433}\", \"{248}\", \"{13018}\", \"{18407}\", \"{454}\", \"{40811}\", \"{2943}\", \"{42987}\",\n\t\"{43590}\", \"{3554}\", \"{4846}\", \"{44882}\", \"{24048}\", \"{5451}\", \"{29849}\", \"{61809}\", \"{9047}\", \"{2458}\",\n\t\"{12349}\", \"{19756}\", \"{21567}\", \"{69527}\", \"{10432}\", \"{27979}\", \"{48824}\", \"{2605}\", \"{13245}\", \"{79287}\",\n\t\"{609}\", \"{1072}\", \"{43120}\", \"{6696}\", \"{8316}\", \"{3709}\", \"{48323}\", \"{11322}\", \"{3115}\", \"{52190}\",\n\t\"{1083}\", \"{13459}\", \"{68237}\", \"{20277}\", \"{7186}\", \"{42630}\", \"{2019}\", \"{9406}\", \"{78597}\", \"{12555}\",\n\t\"{5010}\", \"{24409}\", \"{11199}\", \"{3543}\", \"{2954}\", \"{42990}\", \"{443}\", \"{1238}\", \"{41411}\", \"{18410}\",\n\t\"{9050}\", \"{28449}\", \"{3858}\", \"{49679}\", \"{45482}\", \"{5446}\", \"{4851}\", \"{958}\", \"{44864}\", \"{2}\",\n\t\"{19300}\", \"{40301}\", \"{2253}\", \"{10689}\", \"{49065}\", \"{6936}\", \"{1424}\", \"{44392}\", \"{62875}\", \"{13613}\",\n\t\"{29359}\", \"{8740}\", \"{48169}\", \"{11168}\", \"{1679}\", \"{20260}\", \"{1094}\", \"{41050}\", \"{3102}\", \"{27269}\",\n\t\"{48334}\", \"{11335}\", \"{5007}\", \"{54082}\", \"{25809}\", \"{12542}\", \"{28008}\", \"{9411}\", \"{7191}\", \"{10239}\",\n\t\"{26579}\", \"{2612}\", \"{10425}\", \"{49424}\", \"{21570}\", \"{69530}\", \"{40740}\", \"{19741}\", \"{8301}\", \"{29718}\",\n\t\"{11529}\", \"{6681}\", \"{55792}\", \"{1065}\", \"{13252}\", \"{1688}\", \"{15988}\", \"{11400}\", \"{3637}\", \"{8228}\",\n\t\"{12981}\", \"{16409}\", \"{18189}\", \"{737}\", \"{14884}\", \"{42112}\", \"{7049}\", \"{9324}\", \"{19668}\", \"{12277}\",\n\t\"{5732}\", \"{21459}\", \"{17319}\", \"{19074}\", \"{12286}\", \"{19699}\", \"{10310}\", \"{22929}\", \"{9538}\", \"{2127}\",\n\t\"{13567}\", \"{18178}\", \"{20349}\", \"{1750}\", \"{43602}\", \"{9823}\", \"{8434}\", \"{6759}\", \"{16048}\", \"{4492}\",\n\t\"{376}\", \"{45841}\", \"{7913}\", \"{48040}\", \"{8669}\", \"{3276}\", \"{1816}\", \"{19229}\", \"{4989}\", \"{880}\",\n\t\"{9188}\", \"{2597}\", \"{3980}\", \"{7408}\", \"{49750}\", \"{3971}\", \"{37}\", \"{9179}\", \"{871}\", \"{4978}\",\n\t\"{41823}\", \"{18822}\", \"{3287}\", \"{8698}\", \"{6318}\", \"{8075}\", \"{18539}\", \"{387}\", \"{1311}\", \"{20708}\",\n\t\"{447}\", \"{40802}\", \"{5959}\", \"{18414}\", \"{8158}\", \"{3547}\", \"{2950}\", \"{42994}\", \"{17098}\", \"{5442}\",\n\t\"{4855}\", \"{19518}\", \"{9054}\", \"{7339}\", \"{42262}\", \"{43898}\", \"{2257}\", \"{9648}\", \"{49061}\", \"{6932}\",\n\t\"{44860}\", \"{6}\", \"{19304}\", \"{17069}\", \"{6429}\", \"{8744}\", \"{65878}\", \"{43572}\", \"{1420}\", \"{16788}\",\n\t\"{18208}\", \"{13617}\", \"{3106}\", \"{8519}\", \"{6099}\", \"{11331}\", \"{68224}\", \"{20264}\", \"{1090}\", \"{16338}\",\n\t\"{7778}\", \"{9415}\", \"{7195}\", \"{42623}\", \"{5003}\", \"{21368}\", \"{19159}\", \"{12546}\", \"{21574}\", \"{69534}\",\n\t\"{17428}\", \"{19745}\", \"{9209}\", \"{2616}\", \"{10421}\", \"{7789}\", \"{20478}\", \"{1061}\", \"{13256}\", \"{18649}\",\n\t\"{8305}\", \"{6068}\", \"{43133}\", \"{6685}\", \"{12985}\", \"{41761}\", \"{1148}\", \"{733}\", \"{38099}\", \"{11404}\",\n\t\"{3633}\", \"{27558}\", \"{13889}\", \"{12273}\", \"{5736}\", \"{40080}\", \"{14880}\", \"{10508}\", \"{28739}\", \"{9320}\",\n\t\"{10314}\", \"{39789}\", \"{26248}\", \"{2123}\", \"{40071}\", \"{19070}\", \"{12282}\", \"{13878}\", \"{11218}\", \"{9827}\",\n\t\"{8430}\", \"{29029}\", \"{13563}\", \"{24828}\", \"{41790}\", \"{1754}\", \"{7917}\", \"{29499}\", \"{2888}\", \"{3272}\",\n\t\"{41320}\", \"{4496}\", \"{372}\", \"{1509}\", \"{10149}\", \"{2593}\", \"{3984}\", \"{28378}\", \"{1812}\", \"{50897}\",\n\t\"{51480}\", \"{884}\", \"{875}\", \"{40430}\", \"{41827}\", \"{18826}\", \"{28389}\", \"{3975}\", \"{33}\", \"{26609}\",\n\t\"{67836}\", \"{383}\", \"{1315}\", \"{50390}\", \"{3283}\", \"{2879}\", \"{29468}\", \"{8071}\", \"{9058}\", \"{2447}\",\n\t\"{3850}\", \"{43894}\", \"{17094}\", \"{41902}\", \"{4859}\", \"{950}\", \"{8154}\", \"{6239}\", \"{43362}\", \"{42998}\",\n\t\"{16198}\", \"{1230}\", \"{5955}\", \"{18418}\", \"{45960}\", \"{257}\", \"{18204}\", \"{16169}\", \"{3357}\", \"{8748}\",\n\t\"{48161}\", \"{7832}\", \"{5252}\", \"{17688}\", \"{19308}\", \"{1937}\", \"{7529}\", \"{9644}\", \"{64978}\", \"{42472}\",\n\t\"{69324}\", \"{21364}\", \"{19155}\", \"{17238}\", \"{2006}\", \"{9419}\", \"{7199}\", \"{10231}\", \"{1671}\", \"{20268}\",\n\t\"{18059}\", \"{13446}\", \"{6678}\", \"{8515}\", \"{6095}\", \"{43723}\", \"{8309}\", \"{3716}\", \"{11521}\", \"{6689}\",\n\t\"{616}\", \"{68434}\", \"{16528}\", \"{1680}\", \"{9205}\", \"{7168}\", \"{42033}\", \"{7785}\", \"{21578}\", \"{5613}\",\n\t\"{12356}\", \"{19749}\", \"{39199}\", \"{10504}\", \"{2733}\", \"{26458}\", \"{13885}\", \"{40661}\", \"{34998}\", \"{12492}\",\n\t\"{15980}\", \"{11408}\", \"{29639}\", \"{8220}\", \"{12989}\", \"{13373}\", \"{1144}\", \"{41180}\", \"{41171}\", \"{18170}\",\n\t\"{123}\", \"{1758}\", \"{11214}\", \"{38689}\", \"{27348}\", \"{3023}\", \"{12463}\", \"{25928}\", \"{40690}\", \"{5126}\",\n\t\"{10318}\", \"{8927}\", \"{9530}\", \"{28129}\", \"{40220}\", \"{5596}\", \"{4981}\", \"{888}\", \"{6817}\", \"{28599}\",\n\t\"{3988}\", \"{2372}\", \"{593}\", \"{51997}\", \"{50580}\", \"{1505}\", \"{11049}\", \"{3493}\", \"{2884}\", \"{29278}\",\n\t\"{29289}\", \"{2875}\", \"{3462}\", \"{27709}\", \"{4286}\", \"{41530}\", \"{1319}\", \"{562}\", \"{2383}\", \"{3979}\",\n\t\"{28568}\", \"{9171}\", \"{879}\", \"{4970}\", \"{5567}\", \"{51290}\", \"{17090}\", \"{41906}\", \"{40511}\", \"{954}\",\n\t\"{10099}\", \"{2443}\", \"{3854}\", \"{43890}\", \"{44582}\", \"{1234}\", \"{5951}\", \"{45995}\", \"{8150}\", \"{29549}\",\n\t\"{2958}\", \"{48779}\", \"{3353}\", \"{11789}\", \"{48165}\", \"{7836}\", \"{1428}\", \"{253}\", \"{18200}\", \"{41201}\",\n\t\"{28259}\", \"{9640}\", \"{49069}\", \"{10068}\", \"{5256}\", \"{45292}\", \"{63975}\", \"{1933}\", \"{2002}\", \"{26369}\",\n\t\"{49234}\", \"{10235}\", \"{13959}\", \"{21360}\", \"{19151}\", \"{40150}\", \"{29108}\", \"{8511}\", \"{6091}\", \"{11339}\",\n\t\"{1675}\", \"{55182}\", \"{1098}\", \"{13442}\", \"{612}\", \"{1069}\", \"{41640}\", \"{1684}\", \"{27479}\", \"{3712}\",\n\t\"{11525}\", \"{48524}\", \"{54692}\", \"{5617}\", \"{12352}\", \"{78390}\", \"{9201}\", \"{28618}\", \"{10429}\", \"{7781}\",\n\t\"{13881}\", \"{17509}\", \"{19089}\", \"{12496}\", \"{14888}\", \"{10500}\", \"{2737}\", \"{9328}\", \"{18768}\", \"{13377}\",\n\t\"{1140}\", \"{20559}\", \"{15984}\", \"{43012}\", \"{6149}\", \"{8224}\", \"{11210}\", \"{23829}\", \"{8438}\", \"{3027}\",\n\t\"{16219}\", \"{18174}\", \"{127}\", \"{18799}\", \"{42702}\", \"{8923}\", \"{9534}\", \"{7659}\", \"{12467}\", \"{19078}\",\n\t\"{21249}\", \"{5122}\", \"{6813}\", \"{49140}\", \"{9769}\", \"{2376}\", \"{17148}\", \"{5592}\", \"{4985}\", \"{44941}\",\n\t\"{8088}\", \"{3497}\", \"{2880}\", \"{6508}\", \"{597}\", \"{18329}\", \"{5889}\", \"{1501}\", \"{4282}\", \"{5878}\",\n\t\"{40923}\", \"{566}\", \"{48650}\", \"{2871}\", \"{3466}\", \"{8079}\", \"{19439}\", \"{4974}\", \"{5563}\", \"{21608}\",\n\t\"{2387}\", \"{9798}\", \"{7218}\", \"{9175}\", \"{1296}\", \"{41252}\", \"{4309}\", \"{200}\", \"{48136}\", \"{7865}\",\n\t\"{3300}\", \"{22719}\", \"{63926}\", \"{1960}\", \"{5205}\", \"{54280}\", \"{7393}\", \"{6969}\", \"{43832}\", \"{9613}\",\n\t\"{3807}\", \"{49626}\", \"{6998}\", \"{2410}\", \"{40542}\", \"{907}\", \"{1991}\", \"{5419}\", \"{14059}\", \"{6483}\",\n\t\"{7894}\", \"{47850}\", \"{5902}\", \"{54987}\", \"{19858}\", \"{1267}\", \"{11576}\", \"{48577}\", \"{22358}\", \"{3741}\",\n\t\"{41613}\", \"{18612}\", \"{641}\", \"{4748}\", \"{15308}\", \"{27931}\", \"{9252}\", \"{49296}\", \"{12301}\", \"{20938}\",\n\t\"{45680}\", \"{5644}\", \"{16895}\", \"{40103}\", \"{5058}\", \"{17482}\", \"{8859}\", \"{10266}\", \"{2051}\", \"{23448}\",\n\t\"{17999}\", \"{13411}\", \"{1626}\", \"{44190}\", \"{9955}\", \"{14418}\", \"{48586}\", \"{8542}\", \"{174}\", \"{68356}\",\n\t\"{799}\", \"{4690}\", \"{57783}\", \"{3074}\", \"{8286}\", \"{3699}\", \"{24568}\", \"{5171}\", \"{12434}\", \"{104903}\",\n\t\"{9567}\", \"{2178}\", \"{42751}\", \"{2795}\", \"{2764}\", \"{48945}\", \"{2189}\", \"{9596}\", \"{69446}\", \"{21406}\",\n\t\"{5180}\", \"{12228}\", \"{3668}\", \"{8277}\", \"{3085}\", \"{43041}\", \"{1113}\", \"{768}\", \"{100961}\", \"{185}\",\n\t\"{3435}\", \"{46383}\", \"{48603}\", \"{2822}\", \"{40970}\", \"{535}\", \"{18566}\", \"{13179}\", \"{68}\", \"{9126}\",\n\t\"{39393}\", \"{29928}\", \"{5530}\", \"{12698}\", \"{66961}\", \"{4927}\", \"{12084}\", \"{44912}\", \"{1849}\", \"{19276}\",\n\t\"{38998}\", \"{2325}\", \"{6840}\", \"{99}\", \"{329}\", \"{1552}\", \"{13765}\", \"{40981}\", \"{8636}\", \"{3229}\",\n\t\"{43400}\", \"{38483}\", \"{48132}\", \"{7861}\", \"{3304}\", \"{52381}\", \"{1292}\", \"{13648}\", \"{45933}\", \"{204}\",\n\t\"{7397}\", \"{42421}\", \"{2208}\", \"{9617}\", \"{63922}\", \"{1964}\", \"{5201}\", \"{24618}\", \"{12158}\", \"{903}\",\n\t\"{1995}\", \"{41951}\", \"{3803}\", \"{49622}\", \"{53491}\", \"{2414}\", \"{5906}\", \"{54983}\", \"{418}\", \"{1263}\",\n\t\"{28909}\", \"{6487}\", \"{7890}\", \"{3518}\", \"{13209}\", \"{18616}\", \"{645}\", \"{68467}\", \"{11572}\", \"{26839}\",\n\t\"{43781}\", \"{3745}\", \"{12305}\", \"{69386}\", \"{24259}\", \"{5640}\", \"{42060}\", \"{27935}\", \"{9256}\", \"{2649}\",\n\t\"{11898}\", \"{10262}\", \"{2055}\", \"{42091}\", \"{16891}\", \"{12519}\", \"{69377}\", \"{17486}\", \"{9951}\", \"{43770}\",\n\t\"{3159}\", \"{8546}\", \"{68496}\", \"{13415}\", \"{1622}\", \"{25549}\", \"{22469}\", \"{3070}\", \"{8282}\", \"{9878}\",\n\t\"{170}\", \"{4079}\", \"{41122}\", \"{4694}\", \"{9563}\", \"{83858}\", \"{15439}\", \"{2791}\", \"{51682}\", \"{5175}\",\n\t\"{12430}\", \"{5798}\", \"{5769}\", \"{21402}\", \"{5184}\", \"{40632}\", \"{2760}\", \"{23379}\", \"{49556}\", \"{9592}\",\n\t\"{1117}\", \"{50192}\", \"{4088}\", \"{181}\", \"{9889}\", \"{8273}\", \"{3081}\", \"{14329}\", \"{4438}\", \"{531}\",\n\t\"{18562}\", \"{41563}\", \"{3431}\", \"{14799}\", \"{48607}\", \"{2826}\", \"{5534}\", \"{18879}\", \"{66965}\", \"{4923}\",\n\t\"{46871}\", \"{9122}\", \"{39397}\", \"{15078}\", \"{15089}\", \"{2321}\", \"{6844}\", \"{46880}\", \"{12080}\", \"{5328}\",\n\t\"{18888}\", \"{19272}\", \"{8632}\", \"{42813}\", \"{7948}\", \"{38487}\", \"{41592}\", \"{1556}\", \"{13761}\", \"{40985}\",\n\t\"{19357}\", \"{1968}\", \"{44833}\", \"{21166}\", \"{49032}\", \"{6961}\", \"{2204}\", \"{53281}\", \"{62822}\", \"{13644}\",\n\t\"{1473}\", \"{208}\", \"{6297}\", \"{43521}\", \"{3308}\", \"{8717}\", \"{2903}\", \"{48722}\", \"{52591}\", \"{3514}\",\n\t\"{13058}\", \"{1482}\", \"{414}\", \"{40851}\", \"{29809}\", \"{7587}\", \"{6990}\", \"{2418}\", \"{4806}\", \"{55883}\",\n\t\"{1999}\", \"{5411}\", \"{10472}\", \"{27939}\", \"{42681}\", \"{2645}\", \"{12309}\", \"{19716}\", \"{17296}\", \"{69567}\",\n\t\"{43160}\", \"{26835}\", \"{8356}\", \"{3749}\", \"{13205}\", \"{68286}\", \"{649}\", \"{1032}\", \"{17991}\", \"{13419}\",\n\t\"{68277}\", \"{16586}\", \"{10998}\", \"{11362}\", \"{3155}\", \"{43191}\", \"{69596}\", \"{12515}\", \"{5050}\", \"{24449}\",\n\t\"{8851}\", \"{42670}\", \"{2059}\", \"{9446}\", \"{21212}\", \"{5179}\", \"{40022}\", \"{5794}\", \"{23569}\", \"{2170}\",\n\t\"{9382}\", \"{8978}\", \"{50782}\", \"{1707}\", \"{791}\", \"{4698}\", \"{8463}\", \"{82958}\", \"{14539}\", \"{3691}\",\n\t\"{3660}\", \"{22279}\", \"{48456}\", \"{8492}\", \"{4669}\", \"{760}\", \"{4084}\", \"{41732}\", \"{8989}\", \"{9373}\",\n\t\"{2181}\", \"{15229}\", \"{5765}\", \"{51092}\", \"{5188}\", \"{12220}\", \"{60}\", \"{15699}\", \"{49707}\", \"{3926}\",\n\t\"{5538}\", \"{12690}\", \"{826}\", \"{40463}\", \"{47971}\", \"{8022}\", \"{38297}\", \"{14178}\", \"{1346}\", \"{19979}\",\n\t\"{67865}\", \"{5823}\", \"{321}\", \"{4228}\", \"{19988}\", \"{18372}\", \"{14189}\", \"{3221}\", \"{7944}\", \"{47980}\",\n\t\"{40492}\", \"{5324}\", \"{1841}\", \"{41885}\", \"{9732}\", \"{43913}\", \"{6848}\", \"{91}\", \"{49036}\", \"{6965}\",\n\t\"{2200}\", \"{23619}\", \"{19353}\", \"{40352}\", \"{5209}\", \"{21162}\", \"{6293}\", \"{7869}\", \"{42932}\", \"{8713}\",\n\t\"{62826}\", \"{13640}\", \"{1477}\", \"{55380}\", \"{41442}\", \"{1486}\", \"{410}\", \"{4519}\", \"{2907}\", \"{48726}\",\n\t\"{7898}\", \"{3510}\", \"{4802}\", \"{55887}\", \"{18958}\", \"{5415}\", \"{15159}\", \"{7583}\", \"{6994}\", \"{46950}\",\n\t\"{40713}\", \"{19712}\", \"{17292}\", \"{5648}\", \"{10476}\", \"{49477}\", \"{23258}\", \"{2641}\", \"{13201}\", \"{21838}\",\n\t\"{44780}\", \"{1036}\", \"{14208}\", \"{26831}\", \"{8352}\", \"{48396}\", \"{9959}\", \"{11366}\", \"{3151}\", \"{22548}\",\n\t\"{17995}\", \"{41003}\", \"{4158}\", \"{16582}\", \"{8855}\", \"{15518}\", \"{49486}\", \"{9442}\", \"{16899}\", \"{12511}\",\n\t\"{5054}\", \"{45090}\", \"{56683}\", \"{2174}\", \"{9386}\", \"{2799}\", \"{21216}\", \"{69256}\", \"{12438}\", \"{5790}\",\n\t\"{8467}\", \"{3078}\", \"{43651}\", \"{3695}\", \"{178}\", \"{1703}\", \"{795}\", \"{105803}\", \"{68546}\", \"{764}\",\n\t\"{4080}\", \"{189}\", \"{3664}\", \"{49845}\", \"{3089}\", \"{8496}\", \"{5761}\", \"{24378}\", \"{101861}\", \"{12224}\",\n\t\"{2768}\", \"{9377}\", \"{2185}\", \"{42141}\", \"{41870}\", \"{12694}\", \"{822}\", \"{12079}\", \"{64}\", \"{47283}\",\n\t\"{49703}\", \"{3922}\", \"{1342}\", \"{539}\", \"{67861}\", \"{5827}\", \"{3439}\", \"{8026}\", \"{38293}\", \"{28828}\",\n\t\"{39898}\", \"{3225}\", \"{7940}\", \"{47984}\", \"{325}\", \"{45812}\", \"{13769}\", \"{18376}\", \"{9736}\", \"{2329}\",\n\t\"{42500}\", \"{95}\", \"{12088}\", \"{5320}\", \"{1845}\", \"{41881}\", \"{29098}\", \"{8481}\", \"{3673}\", \"{27518}\",\n\t\"{4097}\", \"{41721}\", \"{1108}\", \"{773}\", \"{2192}\", \"{10548}\", \"{28779}\", \"{9360}\", \"{101876}\", \"{12233}\",\n\t\"{5776}\", \"{51081}\", \"{40031}\", \"{5787}\", \"{21201}\", \"{13838}\", \"{9391}\", \"{28788}\", \"{26208}\", \"{2163}\",\n\t\"{782}\", \"{24868}\", \"{50791}\", \"{1714}\", \"{11258}\", \"{3682}\", \"{8470}\", \"{29069}\", \"{41360}\", \"{18361}\",\n\t\"{332}\", \"{1549}\", \"{7957}\", \"{38498}\", \"{27159}\", \"{3232}\", \"{1852}\", \"{41896}\", \"{40481}\", \"{5337}\",\n\t\"{10109}\", \"{82}\", \"{9721}\", \"{28338}\", \"{39388}\", \"{3935}\", \"{73}\", \"{26649}\", \"{835}\", \"{40470}\",\n\t\"{41867}\", \"{12683}\", \"{38284}\", \"{2839}\", \"{29428}\", \"{8031}\", \"{67876}\", \"{5830}\", \"{1355}\", \"{41391}\",\n\t\"{8118}\", \"{3507}\", \"{2910}\", \"{6498}\", \"{407}\", \"{40842}\", \"{5919}\", \"{1491}\", \"{6983}\", \"{7379}\",\n\t\"{42222}\", \"{7594}\", \"{21769}\", \"{5402}\", \"{4815}\", \"{19558}\", \"{44820}\", \"{21175}\", \"{19344}\", \"{17029}\",\n\t\"{2217}\", \"{9608}\", \"{7388}\", \"{6972}\", \"{1460}\", \"{20079}\", \"{18248}\", \"{13657}\", \"{6469}\", \"{8704}\",\n\t\"{6284}\", \"{43532}\", \"{68264}\", \"{16595}\", \"{17982}\", \"{16378}\", \"{3146}\", \"{8559}\", \"{23948}\", \"{11371}\",\n\t\"{5043}\", \"{17499}\", \"{19119}\", \"{12506}\", \"{7738}\", \"{9455}\", \"{8842}\", \"{42663}\", \"{9249}\", \"{2656}\",\n\t\"{10461}\", \"{49460}\", \"{17285}\", \"{69574}\", \"{17468}\", \"{19705}\", \"{8345}\", \"{6028}\", \"{43173}\", \"{26826}\",\n\t\"{16389}\", \"{1021}\", \"{13216}\", \"{18609}\", \"{4093}\", \"{16449}\", \"{68555}\", \"{777}\", \"{48441}\", \"{8485}\",\n\t\"{3677}\", \"{8268}\", \"{19628}\", \"{12237}\", \"{5772}\", \"{21419}\", \"{2196}\", \"{9589}\", \"{7009}\", \"{9364}\",\n\t\"{9395}\", \"{22969}\", \"{9578}\", \"{2167}\", \"{17359}\", \"{5783}\", \"{21205}\", \"{69245}\", \"{8299}\", \"{3686}\",\n\t\"{8474}\", \"{6719}\", \"{786}\", \"{18138}\", \"{20309}\", \"{1710}\", \"{7953}\", \"{47997}\", \"{8629}\", \"{3236}\",\n\t\"{16008}\", \"{18365}\", \"{336}\", \"{18588}\", \"{42513}\", \"{86}\", \"{9725}\", \"{7448}\", \"{1856}\", \"{19269}\",\n\t\"{21058}\", \"{5333}\", \"{831}\", \"{4938}\", \"{19298}\", \"{12687}\", \"{49710}\", \"{3931}\", \"{77}\", \"{9139}\",\n\t\"{18579}\", \"{5834}\", \"{1351}\", \"{20748}\", \"{38280}\", \"{43203}\", \"{6358}\", \"{8035}\", \"{403}\", \"{1278}\",\n\t\"{41451}\", \"{1495}\", \"{27668}\", \"{3503}\", \"{2914}\", \"{48735}\", \"{54483}\", \"{5406}\", \"{4811}\", \"{918}\",\n\t\"{6987}\", \"{28409}\", \"{3818}\", \"{7590}\", \"{2213}\", \"{26178}\", \"{49025}\", \"{6976}\", \"{44824}\", \"{21171}\",\n\t\"{19340}\", \"{40341}\", \"{29319}\", \"{8700}\", \"{6280}\", \"{11128}\", \"{1464}\", \"{55393}\", \"{1289}\", \"{13653}\",\n\t\"{3142}\", \"{11598}\", \"{48374}\", \"{11375}\", \"{1639}\", \"{16591}\", \"{17986}\", \"{41010}\", \"{28048}\", \"{9451}\",\n\t\"{8846}\", \"{10279}\", \"{5047}\", \"{45083}\", \"{25849}\", \"{12502}\", \"{17281}\", \"{69570}\", \"{40700}\", \"{19701}\",\n\t\"{10288}\", \"{2652}\", \"{10465}\", \"{49464}\", \"{44793}\", \"{1025}\", \"{13212}\", \"{68291}\", \"{8341}\", \"{29758}\",\n\t\"{11569}\", \"{26822}\", \"{49541}\", \"{9585}\", \"{2777}\", \"{9368}\", \"{5193}\", \"{17549}\", \"{69455}\", \"{21415}\",\n\t\"{3096}\", \"{8489}\", \"{6109}\", \"{8264}\", \"{18728}\", \"{196}\", \"{1100}\", \"{20519}\", \"{16259}\", \"{4683}\",\n\t\"{167}\", \"{68345}\", \"{8295}\", \"{23869}\", \"{8478}\", \"{3067}\", \"{12427}\", \"{19038}\", \"{21209}\", \"{5162}\",\n\t\"{9399}\", \"{2786}\", \"{9574}\", \"{7619}\", \"{17108}\", \"{19265}\", \"{12097}\", \"{19488}\", \"{6853}\", \"{46897}\",\n\t\"{9729}\", \"{2336}\", \"{13776}\", \"{18369}\", \"{20158}\", \"{1541}\", \"{43413}\", \"{38490}\", \"{8625}\", \"{6548}\",\n\t\"{48610}\", \"{2831}\", \"{3426}\", \"{8039}\", \"{18575}\", \"{5838}\", \"{18398}\", \"{526}\", \"{39380}\", \"{42303}\",\n\t\"{7258}\", \"{9135}\", \"{19479}\", \"{4934}\", \"{5523}\", \"{21648}\", \"{26768}\", \"{2403}\", \"{3814}\", \"{49635}\",\n\t\"{1982}\", \"{41946}\", \"{40551}\", \"{914}\", \"{7887}\", \"{29509}\", \"{2918}\", \"{6490}\", \"{55583}\", \"{1274}\",\n\t\"{5911}\", \"{1499}\", \"{1468}\", \"{213}\", \"{1285}\", \"{41241}\", \"{3313}\", \"{27078}\", \"{48125}\", \"{7876}\",\n\t\"{5216}\", \"{54293}\", \"{63935}\", \"{1973}\", \"{28219}\", \"{9600}\", \"{7380}\", \"{10028}\", \"{13919}\", \"{17491}\",\n\t\"{16886}\", \"{40110}\", \"{2042}\", \"{10498}\", \"{49274}\", \"{10275}\", \"{1635}\", \"{44183}\", \"{24949}\", \"{13402}\",\n\t\"{29148}\", \"{8551}\", \"{9946}\", \"{11379}\", \"{11388}\", \"{3752}\", \"{11565}\", \"{48564}\", \"{652}\", \"{1029}\",\n\t\"{41600}\", \"{18601}\", \"{9241}\", \"{28658}\", \"{10469}\", \"{27922}\", \"{45693}\", \"{5657}\", \"{12312}\", \"{69391}\",\n\t\"{5197}\", \"{40621}\", \"{25999}\", \"{21411}\", \"{28198}\", \"{9581}\", \"{2773}\", \"{26418}\", \"{100976}\", \"{192}\",\n\t\"{1104}\", \"{50181}\", \"{3092}\", \"{11448}\", \"{29679}\", \"{8260}\", \"{8291}\", \"{29688}\", \"{27308}\", \"{3063}\",\n\t\"{41131}\", \"{4687}\", \"{163}\", \"{1718}\", \"{10358}\", \"{2782}\", \"{9570}\", \"{28169}\", \"{12423}\", \"{25968}\",\n\t\"{51691}\", \"{5166}\", \"{6857}\", \"{39598}\", \"{26059}\", \"{2332}\", \"{40260}\", \"{19261}\", \"{12093}\", \"{44905}\",\n\t\"{11009}\", \"{38494}\", \"{8621}\", \"{29238}\", \"{13772}\", \"{40996}\", \"{41581}\", \"{1545}\", \"{18571}\", \"{41570}\",\n\t\"{1359}\", \"{522}\", \"{38288}\", \"{2835}\", \"{3422}\", \"{27749}\", \"{839}\", \"{4930}\", \"{5527}\", \"{40291}\",\n\t\"{39384}\", \"{3939}\", \"{28528}\", \"{9131}\", \"{1986}\", \"{41942}\", \"{4819}\", \"{910}\", \"{9018}\", \"{2407}\",\n\t\"{3810}\", \"{7598}\", \"{20669}\", \"{1270}\", \"{5915}\", \"{18458}\", \"{7883}\", \"{6279}\", \"{43322}\", \"{6494}\",\n\t\"{3317}\", \"{8708}\", \"{6288}\", \"{7872}\", \"{45920}\", \"{217}\", \"{1281}\", \"{16129}\", \"{7569}\", \"{9604}\",\n\t\"{7384}\", \"{42432}\", \"{5212}\", \"{21179}\", \"{19348}\", \"{1977}\", \"{2046}\", \"{9459}\", \"{22848}\", \"{10271}\",\n\t\"{69364}\", \"{17495}\", \"{16882}\", \"{17278}\", \"{6638}\", \"{8555}\", \"{9942}\", \"{43763}\", \"{1631}\", \"{16599}\",\n\t\"{18019}\", \"{13406}\", \"{656}\", \"{68474}\", \"{16568}\", \"{18605}\", \"{8349}\", \"{3756}\", \"{11561}\", \"{48560}\",\n\t\"{17289}\", \"{5653}\", \"{12316}\", \"{19709}\", \"{9245}\", \"{7128}\", \"{42073}\", \"{27926}\", \"{41342}\", \"{1386}\",\n\t\"{310}\", \"{4219}\", \"{7975}\", \"{48026}\", \"{22609}\", \"{3210}\", \"{1870}\", \"{63836}\", \"{54390}\", \"{5315}\",\n\t\"{6879}\", \"{7283}\", \"{9703}\", \"{43922}\", \"{49736}\", \"{3917}\", \"{51}\", \"{6888}\", \"{817}\", \"{40452}\",\n\t\"{5509}\", \"{1881}\", \"{6593}\", \"{14149}\", \"{47940}\", \"{7984}\", \"{54897}\", \"{5812}\", \"{1377}\", \"{19948}\",\n\t\"{48467}\", \"{11466}\", \"{3651}\", \"{22248}\", \"{18702}\", \"{41703}\", \"{4658}\", \"{751}\", \"{27821}\", \"{15218}\",\n\t\"{49386}\", \"{9342}\", \"{20828}\", \"{12211}\", \"{5754}\", \"{45790}\", \"{40013}\", \"{16985}\", \"{17592}\", \"{5148}\",\n\t\"{10376}\", \"{8949}\", \"{23558}\", \"{2141}\", \"{13501}\", \"{17889}\", \"{44080}\", \"{1736}\", \"{14508}\", \"{9845}\",\n\t\"{8452}\", \"{48496}\", \"{68246}\", \"{20206}\", \"{4780}\", \"{689}\", \"{3164}\", \"{57693}\", \"{3789}\", \"{8396}\",\n\t\"{5061}\", \"{24478}\", \"{104813}\", \"{12524}\", \"{2068}\", \"{9477}\", \"{2685}\", \"{42641}\", \"{48855}\", \"{2674}\",\n\t\"{9486}\", \"{2099}\", \"{21516}\", \"{69556}\", \"{12338}\", \"{5090}\", \"{8367}\", \"{3778}\", \"{43151}\", \"{3195}\",\n\t\"{678}\", \"{1003}\", \"{13234}\", \"{100871}\", \"{46293}\", \"{3525}\", \"{2932}\", \"{48713}\", \"{425}\", \"{40860}\",\n\t\"{13069}\", \"{18476}\", \"{9036}\", \"{2429}\", \"{29838}\", \"{39283}\", \"{12788}\", \"{5420}\", \"{4837}\", \"{66871}\",\n\t\"{44802}\", \"{12194}\", \"{19366}\", \"{1959}\", \"{2235}\", \"{38888}\", \"{46994}\", \"{6950}\", \"{1442}\", \"{239}\",\n\t\"{40891}\", \"{13675}\", \"{3339}\", \"{8726}\", \"{38593}\", \"{43510}\", \"{7971}\", \"{48022}\", \"{52291}\", \"{3214}\",\n\t\"{13758}\", \"{1382}\", \"{314}\", \"{45823}\", \"{42531}\", \"{7287}\", \"{9707}\", \"{2318}\", \"{1874}\", \"{63832}\",\n\t\"{24708}\", \"{5311}\", \"{813}\", \"{12048}\", \"{41841}\", \"{1885}\", \"{49732}\", \"{3913}\", \"{55}\", \"{53581}\",\n\t\"{54893}\", \"{5816}\", \"{1373}\", \"{508}\", \"{6597}\", \"{28819}\", \"{3408}\", \"{7980}\", \"{18706}\", \"{13319}\",\n\t\"{68577}\", \"{755}\", \"{26929}\", \"{11462}\", \"{3655}\", \"{43691}\", \"{69296}\", \"{12215}\", \"{5750}\", \"{24349}\",\n\t\"{27825}\", \"{42170}\", \"{2759}\", \"{9346}\", \"{10372}\", \"{11988}\", \"{42181}\", \"{2145}\", \"{12409}\", \"{16981}\",\n\t\"{17596}\", \"{69267}\", \"{43660}\", \"{9841}\", \"{8456}\", \"{3049}\", \"{13505}\", \"{68586}\", \"{149}\", \"{1732}\",\n\t\"{3160}\", \"{22579}\", \"{9968}\", \"{8392}\", \"{4169}\", \"{20202}\", \"{4784}\", \"{41032}\", \"{83948}\", \"{9473}\",\n\t\"{2681}\", \"{15529}\", \"{5065}\", \"{51792}\", \"{5688}\", \"{12520}\", \"{21512}\", \"{5679}\", \"{40722}\", \"{5094}\",\n\t\"{23269}\", \"{2670}\", \"{9482}\", \"{49446}\", \"{50082}\", \"{1007}\", \"{13230}\", \"{4198}\", \"{8363}\", \"{9999}\",\n\t\"{14239}\", \"{3191}\", \"{421}\", \"{4528}\", \"{41473}\", \"{18472}\", \"{14689}\", \"{3521}\", \"{2936}\", \"{48717}\",\n\t\"{18969}\", \"{5424}\", \"{4833}\", \"{66875}\", \"{9032}\", \"{46961}\", \"{15168}\", \"{39287}\", \"{2231}\", \"{15199}\",\n\t\"{46990}\", \"{6954}\", \"{5238}\", \"{12190}\", \"{19362}\", \"{18998}\", \"{42903}\", \"{8722}\", \"{38597}\", \"{7858}\",\n\t\"{1446}\", \"{41482}\", \"{40895}\", \"{13671}\", \"{1878}\", \"{19247}\", \"{21076}\", \"{44923}\", \"{6871}\", \"{49122}\",\n\t\"{53391}\", \"{2314}\", \"{13754}\", \"{62932}\", \"{318}\", \"{1563}\", \"{43431}\", \"{6387}\", \"{8607}\", \"{3218}\",\n\t\"{48632}\", \"{2813}\", \"{3404}\", \"{52481}\", \"{1592}\", \"{13148}\", \"{40941}\", \"{504}\", \"{7497}\", \"{29919}\",\n\t\"{59}\", \"{6880}\", \"{55993}\", \"{4916}\", \"{5501}\", \"{1889}\", \"{27829}\", \"{10562}\", \"{2755}\", \"{42791}\",\n\t\"{19606}\", \"{12219}\", \"{69477}\", \"{17386}\", \"{26925}\", \"{43070}\", \"{3659}\", \"{8246}\", \"{68396}\", \"{13315}\",\n\t\"{1122}\", \"{759}\", \"{13509}\", \"{17881}\", \"{145}\", \"{68367}\", \"{11272}\", \"{10888}\", \"{43081}\", \"{3045}\",\n\t\"{12405}\", \"{69486}\", \"{24559}\", \"{5140}\", \"{42760}\", \"{8941}\", \"{9556}\", \"{2149}\", \"{5069}\", \"{21302}\",\n\t\"{5684}\", \"{40132}\", \"{2060}\", \"{23479}\", \"{8868}\", \"{9292}\", \"{1617}\", \"{50692}\", \"{4788}\", \"{681}\",\n\t\"{82848}\", \"{8573}\", \"{3781}\", \"{14429}\", \"{22369}\", \"{3770}\", \"{8582}\", \"{48546}\", \"{670}\", \"{4779}\",\n\t\"{41622}\", \"{4194}\", \"{9263}\", \"{8899}\", \"{15339}\", \"{2091}\", \"{51182}\", \"{5675}\", \"{12330}\", \"{5098}\",\n\t\"{15789}\", \"{2421}\", \"{3836}\", \"{49617}\", \"{12780}\", \"{5428}\", \"{40573}\", \"{936}\", \"{8132}\", \"{47861}\",\n\t\"{14068}\", \"{38387}\", \"{19869}\", \"{1256}\", \"{5933}\", \"{67975}\", \"{4338}\", \"{231}\", \"{18262}\", \"{19898}\",\n\t\"{3331}\", \"{14099}\", \"{47890}\", \"{7854}\", \"{5234}\", \"{40582}\", \"{41995}\", \"{1951}\", \"{43803}\", \"{9622}\",\n\t\"{39497}\", \"{6958}\", \"{6875}\", \"{49126}\", \"{23709}\", \"{2310}\", \"{40242}\", \"{19243}\", \"{21072}\", \"{5319}\",\n\t\"{7979}\", \"{6383}\", \"{8603}\", \"{42822}\", \"{13750}\", \"{62936}\", \"{55290}\", \"{1567}\", \"{1596}\", \"{41552}\",\n\t\"{4409}\", \"{500}\", \"{48636}\", \"{2817}\", \"{3400}\", \"{7988}\", \"{55997}\", \"{4912}\", \"{5505}\", \"{18848}\",\n\t\"{7493}\", \"{15049}\", \"{46840}\", \"{6884}\", \"{19602}\", \"{40603}\", \"{5758}\", \"{17382}\", \"{49567}\", \"{10566}\",\n\t\"{2751}\", \"{23348}\", \"{21928}\", \"{13311}\", \"{1126}\", \"{44690}\", \"{26921}\", \"{14318}\", \"{48286}\", \"{8242}\",\n\t\"{11276}\", \"{9849}\", \"{22458}\", \"{3041}\", \"{41113}\", \"{17885}\", \"{141}\", \"{4048}\", \"{15408}\", \"{8945}\",\n\t\"{9552}\", \"{49596}\", \"{12401}\", \"{16989}\", \"{45180}\", \"{5144}\", \"{2064}\", \"{56793}\", \"{2689}\", \"{9296}\",\n\t\"{69346}\", \"{21306}\", \"{5680}\", \"{12528}\", \"{3168}\", \"{8577}\", \"{3785}\", \"{43741}\", \"{1613}\", \"{25578}\",\n\t\"{105913}\", \"{685}\", \"{674}\", \"{68456}\", \"{13238}\", \"{4190}\", \"{49955}\", \"{3774}\", \"{8586}\", \"{3199}\",\n\t\"{24268}\", \"{5671}\", \"{12334}\", \"{101971}\", \"{9267}\", \"{2678}\", \"{42051}\", \"{2095}\", \"{12784}\", \"{41960}\",\n\t\"{12169}\", \"{932}\", \"{47393}\", \"{2425}\", \"{3832}\", \"{49613}\", \"{429}\", \"{1252}\", \"{5937}\", \"{67971}\",\n\t\"{8136}\", \"{3529}\", \"{28938}\", \"{38383}\", \"{3335}\", \"{39988}\", \"{47894}\", \"{7850}\", \"{45902}\", \"{235}\",\n\t\"{18266}\", \"{13679}\", \"{2239}\", \"{9626}\", \"{39493}\", \"{42410}\", \"{5230}\", \"{12198}\", \"{41991}\", \"{1955}\",\n\t\"{8591}\", \"{29188}\", \"{27408}\", \"{3763}\", \"{41631}\", \"{4187}\", \"{663}\", \"{1018}\", \"{10458}\", \"{2082}\",\n\t\"{9270}\", \"{28669}\", \"{12323}\", \"{101966}\", \"{51191}\", \"{5666}\", \"{5697}\", \"{40121}\", \"{13928}\", \"{21311}\",\n\t\"{28698}\", \"{9281}\", \"{2073}\", \"{26318}\", \"{24978}\", \"{692}\", \"{1604}\", \"{50681}\", \"{3792}\", \"{11348}\",\n\t\"{29179}\", \"{8560}\", \"{18271}\", \"{41270}\", \"{1459}\", \"{222}\", \"{38588}\", \"{7847}\", \"{3322}\", \"{27049}\",\n\t\"{41986}\", \"{1942}\", \"{5227}\", \"{40591}\", \"{39484}\", \"{10019}\", \"{28228}\", \"{9631}\", \"{3825}\", \"{39298}\",\n\t\"{26759}\", \"{2432}\", \"{40560}\", \"{925}\", \"{12793}\", \"{41977}\", \"{2929}\", \"{38394}\", \"{8121}\", \"{29538}\",\n\t\"{5920}\", \"{67966}\", \"{41281}\", \"{1245}\", \"{3417}\", \"{8008}\", \"{6588}\", \"{2800}\", \"{40952}\", \"{517}\",\n\t\"{1581}\", \"{5809}\", \"{7269}\", \"{6893}\", \"{7484}\", \"{42332}\", \"{5512}\", \"{21679}\", \"{19448}\", \"{4905}\",\n\t\"{21065}\", \"{44930}\", \"{17139}\", \"{19254}\", \"{9718}\", \"{2307}\", \"{6862}\", \"{7298}\", \"{20169}\", \"{1570}\",\n\t\"{13747}\", \"{18358}\", \"{8614}\", \"{6579}\", \"{43422}\", \"{6394}\", \"{156}\", \"{68374}\", \"{16268}\", \"{17892}\",\n\t\"{8449}\", \"{3056}\", \"{11261}\", \"{23858}\", \"{17589}\", \"{5153}\", \"{12416}\", \"{19009}\", \"{9545}\", \"{7628}\",\n\t\"{42773}\", \"{8952}\", \"{2746}\", \"{9359}\", \"{49570}\", \"{10571}\", \"{69464}\", \"{17395}\", \"{19615}\", \"{17578}\",\n\t\"{6138}\", \"{8255}\", \"{26936}\", \"{43063}\", \"{1131}\", \"{16299}\", \"{18719}\", \"{13306}\", \"{16559}\", \"{4183}\",\n\t\"{667}\", \"{68445}\", \"{8595}\", \"{48551}\", \"{8378}\", \"{3767}\", \"{12327}\", \"{19738}\", \"{21509}\", \"{5662}\",\n\t\"{9499}\", \"{2086}\", \"{9274}\", \"{7119}\", \"{22879}\", \"{9285}\", \"{2077}\", \"{9468}\", \"{5693}\", \"{17249}\",\n\t\"{69355}\", \"{21315}\", \"{3796}\", \"{8389}\", \"{6609}\", \"{8564}\", \"{18028}\", \"{696}\", \"{1600}\", \"{20219}\",\n\t\"{47887}\", \"{7843}\", \"{3326}\", \"{8739}\", \"{18275}\", \"{16118}\", \"{18498}\", \"{226}\", \"{39480}\", \"{42403}\",\n\t\"{7558}\", \"{9635}\", \"{19379}\", \"{1946}\", \"{5223}\", \"{21148}\", \"{4828}\", \"{921}\", \"{12797}\", \"{19388}\",\n\t\"{3821}\", \"{49600}\", \"{9029}\", \"{2436}\", \"{5924}\", \"{18469}\", \"{20658}\", \"{1241}\", \"{43313}\", \"{38390}\",\n\t\"{8125}\", \"{6248}\", \"{1368}\", \"{513}\", \"{1585}\", \"{41541}\", \"{3413}\", \"{27778}\", \"{48625}\", \"{2804}\",\n\t\"{5516}\", \"{54593}\", \"{808}\", \"{4901}\", \"{28519}\", \"{6897}\", \"{7480}\", \"{3908}\", \"{26068}\", \"{2303}\",\n\t\"{6866}\", \"{49135}\", \"{21061}\", \"{44934}\", \"{40251}\", \"{19250}\", \"{8610}\", \"{29209}\", \"{11038}\", \"{6390}\",\n\t\"{55283}\", \"{1574}\", \"{13743}\", \"{1399}\", \"{11488}\", \"{3052}\", \"{11265}\", \"{48264}\", \"{152}\", \"{1729}\",\n\t\"{41100}\", \"{17896}\", \"{9541}\", \"{28158}\", \"{10369}\", \"{8956}\", \"{45193}\", \"{5157}\", \"{12412}\", \"{25959}\",\n\t\"{69460}\", \"{17391}\", \"{19611}\", \"{40610}\", \"{2742}\", \"{10398}\", \"{49574}\", \"{10575}\", \"{1135}\", \"{44683}\",\n\t\"{68381}\", \"{13302}\", \"{29648}\", \"{8251}\", \"{26932}\", \"{11479}\", \"{9495}\", \"{49451}\", \"{9278}\", \"{2667}\",\n\t\"{17459}\", \"{5083}\", \"{21505}\", \"{69545}\", \"{8599}\", \"{3186}\", \"{8374}\", \"{6019}\", \"{13227}\", \"{18638}\",\n\t\"{20409}\", \"{1010}\", \"{4793}\", \"{16349}\", \"{68255}\", \"{20215}\", \"{23979}\", \"{8385}\", \"{3177}\", \"{8568}\",\n\t\"{19128}\", \"{12537}\", \"{5072}\", \"{21319}\", \"{2696}\", \"{9289}\", \"{7709}\", \"{9464}\", \"{19375}\", \"{17018}\",\n\t\"{19598}\", \"{12187}\", \"{46987}\", \"{6943}\", \"{2226}\", \"{9639}\", \"{18279}\", \"{13666}\", \"{1451}\", \"{20048}\",\n\t\"{38580}\", \"{43503}\", \"{6458}\", \"{8735}\", \"{2921}\", \"{48700}\", \"{8129}\", \"{3536}\", \"{5928}\", \"{18465}\",\n\t\"{436}\", \"{18288}\", \"{42213}\", \"{39290}\", \"{9025}\", \"{7348}\", \"{4824}\", \"{19569}\", \"{21758}\", \"{5433}\",\n\t\"{42}\", \"{26678}\", \"{49725}\", \"{3904}\", \"{41856}\", \"{1892}\", \"{804}\", \"{40441}\", \"{29419}\", \"{7997}\",\n\t\"{6580}\", \"{2808}\", \"{1364}\", \"{55493}\", \"{1589}\", \"{5801}\", \"{303}\", \"{1578}\", \"{41351}\", \"{1395}\",\n\t\"{27168}\", \"{3203}\", \"{7966}\", \"{48035}\", \"{54383}\", \"{5306}\", \"{1863}\", \"{63825}\", \"{9710}\", \"{28309}\",\n\t\"{10138}\", \"{7290}\", \"{17581}\", \"{13809}\", \"{40000}\", \"{16996}\", \"{10588}\", \"{2152}\", \"{10365}\", \"{49364}\",\n\t\"{44093}\", \"{1725}\", \"{13512}\", \"{24859}\", \"{8441}\", \"{29058}\", \"{11269}\", \"{9856}\", \"{3642}\", \"{11298}\",\n\t\"{48474}\", \"{11475}\", \"{1139}\", \"{742}\", \"{18711}\", \"{41710}\", \"{28748}\", \"{9351}\", \"{27832}\", \"{10579}\",\n\t\"{5747}\", \"{45783}\", \"{69281}\", \"{12202}\", \"{40731}\", \"{5087}\", \"{21501}\", \"{25889}\", \"{9491}\", \"{28088}\",\n\t\"{26508}\", \"{2663}\", \"{13223}\", \"{100866}\", \"{50091}\", \"{1014}\", \"{11558}\", \"{3182}\", \"{8370}\", \"{29769}\",\n\t\"{29798}\", \"{8381}\", \"{3173}\", \"{27218}\", \"{4797}\", \"{41021}\", \"{1608}\", \"{20211}\", \"{2692}\", \"{10248}\",\n\t\"{28079}\", \"{9460}\", \"{25878}\", \"{12533}\", \"{5076}\", \"{51781}\", \"{39488}\", \"{6947}\", \"{2222}\", \"{26149}\",\n\t\"{19371}\", \"{40370}\", \"{44815}\", \"{12183}\", \"{38584}\", \"{11119}\", \"{29328}\", \"{8731}\", \"{40886}\", \"{13662}\",\n\t\"{1455}\", \"{41491}\", \"{41460}\", \"{18461}\", \"{432}\", \"{1249}\", \"{2925}\", \"{38398}\", \"{27659}\", \"{3532}\",\n\t\"{4820}\", \"{929}\", \"{40381}\", \"{5437}\", \"{3829}\", \"{39294}\", \"{9021}\", \"{28438}\", \"{41852}\", \"{1896}\",\n\t\"{800}\", \"{4909}\", \"{46}\", \"{9108}\", \"{7488}\", \"{3900}\", \"{1360}\", \"{20779}\", \"{18548}\", \"{5805}\",\n\t\"{6369}\", \"{7993}\", \"{6584}\", \"{43232}\", \"{8618}\", \"{3207}\", \"{7962}\", \"{6398}\", \"{307}\", \"{45830}\",\n\t\"{16039}\", \"{1391}\", \"{9714}\", \"{7479}\", \"{42522}\", \"{7294}\", \"{21069}\", \"{5302}\", \"{1867}\", \"{19258}\",\n\t\"{9549}\", \"{2156}\", \"{10361}\", \"{22958}\", \"{17585}\", \"{69274}\", \"{17368}\", \"{16992}\", \"{8445}\", \"{6728}\",\n\t\"{43673}\", \"{9852}\", \"{16489}\", \"{1721}\", \"{13516}\", \"{18109}\", \"{68564}\", \"{746}\", \"{18715}\", \"{16478}\",\n\t\"{3646}\", \"{8259}\", \"{48470}\", \"{11471}\", \"{5743}\", \"{17399}\", \"{19619}\", \"{12206}\", \"{7038}\", \"{9355}\",\n\t\"{27836}\", \"{42163}\", \"{8209}\", \"{3616}\", \"{11421}\", \"{6789}\", \"{716}\", \"{68534}\", \"{16428}\", \"{1780}\",\n\t\"{9305}\", \"{7068}\", \"{42133}\", \"{7685}\", \"{21478}\", \"{5713}\", \"{12256}\", \"{19649}\", \"{69224}\", \"{21264}\",\n\t\"{19055}\", \"{17338}\", \"{2106}\", \"{9519}\", \"{7099}\", \"{10331}\", \"{1771}\", \"{20368}\", \"{18159}\", \"{13546}\",\n\t\"{6778}\", \"{8415}\", \"{6195}\", \"{43623}\", \"{45860}\", \"{357}\", \"{18304}\", \"{16069}\", \"{3257}\", \"{8648}\",\n\t\"{48061}\", \"{7932}\", \"{5352}\", \"{17788}\", \"{19208}\", \"{1837}\", \"{7429}\", \"{9744}\", \"{64878}\", \"{42572}\",\n\t\"{9158}\", \"{16}\", \"{3950}\", \"{43994}\", \"{17194}\", \"{41802}\", \"{4959}\", \"{850}\", \"{8054}\", \"{6339}\",\n\t\"{43262}\", \"{42898}\", \"{16098}\", \"{1330}\", \"{5855}\", \"{18518}\", \"{29389}\", \"{2975}\", \"{3562}\", \"{27609}\",\n\t\"{4386}\", \"{41430}\", \"{1219}\", \"{462}\", \"{2283}\", \"{3879}\", \"{28468}\", \"{9071}\", \"{979}\", \"{4870}\",\n\t\"{5467}\", \"{51390}\", \"{40320}\", \"{5496}\", \"{4881}\", \"{988}\", \"{6917}\", \"{28499}\", \"{3888}\", \"{2272}\",\n\t\"{493}\", \"{51897}\", \"{50480}\", \"{1405}\", \"{11149}\", \"{3593}\", \"{2984}\", \"{29378}\", \"{41071}\", \"{18070}\",\n\t\"{13282}\", \"{1658}\", \"{11314}\", \"{38789}\", \"{27248}\", \"{3123}\", \"{12563}\", \"{25828}\", \"{40790}\", \"{5026}\",\n\t\"{10218}\", \"{8827}\", \"{9430}\", \"{28029}\", \"{39099}\", \"{10404}\", \"{2633}\", \"{26558}\", \"{13985}\", \"{40761}\",\n\t\"{34898}\", \"{12592}\", \"{15880}\", \"{11508}\", \"{29739}\", \"{8320}\", \"{12889}\", \"{13273}\", \"{1044}\", \"{41080}\",\n\t\"{712}\", \"{1169}\", \"{41740}\", \"{1784}\", \"{27579}\", \"{3612}\", \"{11425}\", \"{48424}\", \"{54792}\", \"{5717}\",\n\t\"{12252}\", \"{78290}\", \"{9301}\", \"{28718}\", \"{10529}\", \"{7681}\", \"{2102}\", \"{26269}\", \"{49334}\", \"{10335}\",\n\t\"{13859}\", \"{21260}\", \"{19051}\", \"{40050}\", \"{29008}\", \"{8411}\", \"{6191}\", \"{11239}\", \"{1775}\", \"{55082}\",\n\t\"{1198}\", \"{13542}\", \"{3253}\", \"{11689}\", \"{48065}\", \"{7936}\", \"{1528}\", \"{353}\", \"{18300}\", \"{41301}\",\n\t\"{28359}\", \"{9740}\", \"{49169}\", \"{10168}\", \"{5356}\", \"{45392}\", \"{63875}\", \"{1833}\", \"{17190}\", \"{41806}\",\n\t\"{40411}\", \"{854}\", \"{10199}\", \"{12}\", \"{3954}\", \"{43990}\", \"{44482}\", \"{1334}\", \"{5851}\", \"{45895}\",\n\t\"{8050}\", \"{29449}\", \"{2858}\", \"{48679}\", \"{4382}\", \"{5978}\", \"{40823}\", \"{466}\", \"{48750}\", \"{2971}\",\n\t\"{3566}\", \"{8179}\", \"{19539}\", \"{4874}\", \"{5463}\", \"{21708}\", \"{2287}\", \"{9698}\", \"{7318}\", \"{9075}\",\n\t\"{6913}\", \"{49040}\", \"{9669}\", \"{2276}\", \"{17048}\", \"{5492}\", \"{4885}\", \"{44841}\", \"{8188}\", \"{3597}\",\n\t\"{2980}\", \"{6408}\", \"{497}\", \"{18229}\", \"{5989}\", \"{1401}\", \"{11310}\", \"{23929}\", \"{8538}\", \"{3127}\",\n\t\"{16319}\", \"{18074}\", \"{13286}\", \"{18699}\", \"{42602}\", \"{8823}\", \"{9434}\", \"{7759}\", \"{12567}\", \"{19178}\",\n\t\"{21349}\", \"{5022}\", \"{13981}\", \"{17409}\", \"{19189}\", \"{12596}\", \"{14988}\", \"{10400}\", \"{2637}\", \"{9228}\",\n\t\"{18668}\", \"{13277}\", \"{1040}\", \"{20459}\", \"{15884}\", \"{43112}\", \"{6049}\", \"{8324}\", \"{26479}\", \"{2712}\",\n\t\"{10525}\", \"{49524}\", \"{21470}\", \"{69430}\", \"{40640}\", \"{19641}\", \"{8201}\", \"{29618}\", \"{11429}\", \"{6781}\",\n\t\"{55692}\", \"{1165}\", \"{13352}\", \"{1788}\", \"{1779}\", \"{102}\", \"{1194}\", \"{41150}\", \"{3002}\", \"{27369}\",\n\t\"{48234}\", \"{11235}\", \"{5107}\", \"{54182}\", \"{25909}\", \"{12442}\", \"{28108}\", \"{9511}\", \"{7091}\", \"{10339}\",\n\t\"{44964}\", \"{17780}\", \"{19200}\", \"{40201}\", \"{2353}\", \"{10789}\", \"{49165}\", \"{6836}\", \"{1524}\", \"{44292}\",\n\t\"{62975}\", \"{13713}\", \"{29259}\", \"{8640}\", \"{48069}\", \"{11068}\", \"{11099}\", \"{3443}\", \"{2854}\", \"{42890}\",\n\t\"{543}\", \"{1338}\", \"{41511}\", \"{18510}\", \"{9150}\", \"{28549}\", \"{3958}\", \"{49779}\", \"{45582}\", \"{5546}\",\n\t\"{4951}\", \"{858}\", \"{49650}\", \"{3871}\", \"{2466}\", \"{9079}\", \"{971}\", \"{4878}\", \"{41923}\", \"{18922}\",\n\t\"{3387}\", \"{8798}\", \"{6218}\", \"{8175}\", \"{18439}\", \"{287}\", \"{1211}\", \"{20608}\", \"{16148}\", \"{4592}\",\n\t\"{276}\", \"{45941}\", \"{7813}\", \"{48140}\", \"{8769}\", \"{3376}\", \"{1916}\", \"{19329}\", \"{4889}\", \"{980}\",\n\t\"{9088}\", \"{2497}\", \"{3880}\", \"{7508}\", \"{17219}\", \"{19174}\", \"{12386}\", \"{19799}\", \"{10210}\", \"{22829}\",\n\t\"{9438}\", \"{2027}\", \"{13467}\", \"{18078}\", \"{20249}\", \"{1650}\", \"{43702}\", \"{9923}\", \"{8534}\", \"{6659}\",\n\t\"{15888}\", \"{11500}\", \"{3737}\", \"{8328}\", \"{12881}\", \"{16509}\", \"{18089}\", \"{637}\", \"{14984}\", \"{42012}\",\n\t\"{7149}\", \"{9224}\", \"{19768}\", \"{12377}\", \"{5632}\", \"{21559}\", \"{21474}\", \"{69434}\", \"{17528}\", \"{19645}\",\n\t\"{9309}\", \"{2716}\", \"{10521}\", \"{7689}\", \"{20578}\", \"{1161}\", \"{13356}\", \"{18749}\", \"{8205}\", \"{6168}\",\n\t\"{43033}\", \"{6785}\", \"{3006}\", \"{8419}\", \"{6199}\", \"{11231}\", \"{68324}\", \"{106}\", \"{1190}\", \"{16238}\",\n\t\"{7678}\", \"{9515}\", \"{7095}\", \"{42723}\", \"{5103}\", \"{21268}\", \"{19059}\", \"{12446}\", \"{2357}\", \"{9748}\",\n\t\"{49161}\", \"{6832}\", \"{44960}\", \"{17784}\", \"{19204}\", \"{17169}\", \"{6529}\", \"{8644}\", \"{65978}\", \"{43472}\",\n\t\"{1520}\", \"{16688}\", \"{18308}\", \"{13717}\", \"{547}\", \"{40902}\", \"{5859}\", \"{18514}\", \"{8058}\", \"{3447}\",\n\t\"{2850}\", \"{42894}\", \"{17198}\", \"{5542}\", \"{4955}\", \"{19418}\", \"{9154}\", \"{7239}\", \"{42362}\", \"{43998}\",\n\t\"{975}\", \"{40530}\", \"{41927}\", \"{18926}\", \"{28289}\", \"{3875}\", \"{2462}\", \"{26709}\", \"{67936}\", \"{283}\",\n\t\"{1215}\", \"{50290}\", \"{3383}\", \"{2979}\", \"{29568}\", \"{8171}\", \"{7817}\", \"{29599}\", \"{2988}\", \"{3372}\",\n\t\"{41220}\", \"{4596}\", \"{272}\", \"{1409}\", \"{10049}\", \"{2493}\", \"{3884}\", \"{28278}\", \"{1912}\", \"{50997}\",\n\t\"{51580}\", \"{984}\", \"{10214}\", \"{39689}\", \"{26348}\", \"{2023}\", \"{40171}\", \"{19170}\", \"{12382}\", \"{13978}\",\n\t\"{11318}\", \"{9927}\", \"{8530}\", \"{29129}\", \"{13463}\", \"{24928}\", \"{41690}\", \"{1654}\", \"{12885}\", \"{41661}\",\n\t\"{1048}\", \"{633}\", \"{38199}\", \"{11504}\", \"{3733}\", \"{27458}\", \"{13989}\", \"{12373}\", \"{5636}\", \"{40180}\",\n\t\"{14980}\", \"{10408}\", \"{28639}\", \"{9220}\", \"{265}\", \"{45952}\", \"{488}\", \"{4581}\", \"{28999}\", \"{3365}\",\n\t\"{7800}\", \"{3588}\", \"{24679}\", \"{993}\", \"{1905}\", \"{50980}\", \"{3893}\", \"{2269}\", \"{42440}\", \"{2484}\",\n\t\"{2475}\", \"{56382}\", \"{2298}\", \"{3862}\", \"{41930}\", \"{18931}\", \"{962}\", \"{12139}\", \"{3579}\", \"{8166}\",\n\t\"{3394}\", \"{28968}\", \"{1202}\", \"{479}\", \"{67921}\", \"{294}\", \"{3724}\", \"{46092}\", \"{26858}\", \"{11513}\",\n\t\"{68406}\", \"{624}\", \"{12892}\", \"{13268}\", \"{2628}\", \"{9237}\", \"{14997}\", \"{42001}\", \"{5621}\", \"{12589}\",\n\t\"{101921}\", \"{12364}\", \"{12395}\", \"{69316}\", \"{12578}\", \"{19167}\", \"{47782}\", \"{2034}\", \"{10203}\", \"{49202}\",\n\t\"{13299}\", \"{1643}\", \"{13474}\", \"{105943}\", \"{8527}\", \"{3138}\", \"{43711}\", \"{9930}\", \"{1187}\", \"{41143}\",\n\t\"{4018}\", \"{111}\", \"{9819}\", \"{11226}\", \"{3011}\", \"{22408}\", \"{78493}\", \"{12451}\", \"{5114}\", \"{54191}\",\n\t\"{7082}\", \"{15458}\", \"{58587}\", \"{9502}\", \"{10536}\", \"{49537}\", \"{23318}\", \"{2701}\", \"{40653}\", \"{19652}\",\n\t\"{21463}\", \"{5708}\", \"{14348}\", \"{6792}\", \"{8212}\", \"{59297}\", \"{13341}\", \"{21978}\", \"{55681}\", \"{1176}\",\n\t\"{2847}\", \"{42883}\", \"{22049}\", \"{3450}\", \"{41502}\", \"{18503}\", \"{550}\", \"{4459}\", \"{15019}\", \"{89470}\",\n\t\"{9143}\", \"{46810}\", \"{4942}\", \"{44986}\", \"{18818}\", \"{5555}\", \"{19213}\", \"{40212}\", \"{5349}\", \"{17793}\",\n\t\"{49176}\", \"{6825}\", \"{2340}\", \"{23759}\", \"{62966}\", \"{13700}\", \"{1537}\", \"{44281}\", \"{88360}\", \"{7929}\",\n\t\"{42872}\", \"{8653}\", \"{22778}\", \"{3361}\", \"{7804}\", \"{48157}\", \"{261}\", \"{4368}\", \"{41233}\", \"{4585}\",\n\t\"{3897}\", \"{43853}\", \"{6908}\", \"{2480}\", \"{51593}\", \"{997}\", \"{1901}\", \"{5489}\", \"{5478}\", \"{18935}\",\n\t\"{966}\", \"{40523}\", \"{2471}\", \"{23068}\", \"{49647}\", \"{3866}\", \"{1206}\", \"{19839}\", \"{4399}\", \"{290}\",\n\t\"{47831}\", \"{8162}\", \"{3390}\", \"{14038}\", \"{4729}\", \"{620}\", \"{12896}\", \"{41672}\", \"{3720}\", \"{14488}\",\n\t\"{48516}\", \"{11517}\", \"{5625}\", \"{40193}\", \"{20959}\", \"{12360}\", \"{109656}\", \"{9233}\", \"{14993}\", \"{15369}\",\n\t\"{15398}\", \"{2030}\", \"{10207}\", \"{8838}\", \"{12391}\", \"{5039}\", \"{40162}\", \"{19163}\", \"{8523}\", \"{82818}\",\n\t\"{14479}\", \"{9934}\", \"{41683}\", \"{1647}\", \"{13470}\", \"{105947}\", \"{48223}\", \"{11222}\", \"{3015}\", \"{52090}\",\n\t\"{1183}\", \"{13559}\", \"{68337}\", \"{115}\", \"{7086}\", \"{42730}\", \"{2119}\", \"{9506}\", \"{78497}\", \"{12455}\",\n\t\"{5110}\", \"{24509}\", \"{12249}\", \"{19656}\", \"{21467}\", \"{69427}\", \"{10532}\", \"{27879}\", \"{48924}\", \"{2705}\",\n\t\"{13345}\", \"{79387}\", \"{709}\", \"{1172}\", \"{43020}\", \"{6796}\", \"{8216}\", \"{3609}\", \"{13118}\", \"{18507}\",\n\t\"{554}\", \"{40911}\", \"{2843}\", \"{42887}\", \"{43490}\", \"{3454}\", \"{4946}\", \"{44982}\", \"{24148}\", \"{5551}\",\n\t\"{29949}\", \"{61909}\", \"{9147}\", \"{2558}\", \"{49172}\", \"{6821}\", \"{2344}\", \"{42380}\", \"{19217}\", \"{1828}\",\n\t\"{44973}\", \"{17797}\", \"{88364}\", \"{43461}\", \"{3248}\", \"{8657}\", \"{62962}\", \"{13704}\", \"{1533}\", \"{348}\",\n\t\"{4892}\", \"{5268}\", \"{40333}\", \"{5485}\", \"{23678}\", \"{2261}\", \"{6904}\", \"{49057}\", \"{50493}\", \"{1416}\",\n\t\"{480}\", \"{4589}\", \"{2997}\", \"{42953}\", \"{7808}\", \"{3580}\", \"{3571}\", \"{22168}\", \"{48747}\", \"{2966}\",\n\t\"{4578}\", \"{471}\", \"{4395}\", \"{41423}\", \"{46931}\", \"{9062}\", \"{2290}\", \"{15138}\", \"{5474}\", \"{18939}\",\n\t\"{5299}\", \"{4863}\", \"{2620}\", \"{15588}\", \"{49416}\", \"{10417}\", \"{5629}\", \"{12581}\", \"{13996}\", \"{40772}\",\n\t\"{108756}\", \"{8333}\", \"{15893}\", \"{14269}\", \"{1057}\", \"{41093}\", \"{21859}\", \"{13260}\", \"{13291}\", \"{4139}\",\n\t\"{41062}\", \"{18063}\", \"{14298}\", \"{3130}\", \"{11307}\", \"{9938}\", \"{40783}\", \"{5035}\", \"{12570}\", \"{104847}\",\n\t\"{9423}\", \"{83918}\", \"{15579}\", \"{8834}\", \"{19046}\", \"{12459}\", \"{69237}\", \"{21277}\", \"{49323}\", \"{10322}\",\n\t\"{2115}\", \"{53190}\", \"{79597}\", \"{13555}\", \"{1762}\", \"{119}\", \"{6186}\", \"{43630}\", \"{3019}\", \"{8406}\",\n\t\"{11432}\", \"{26979}\", \"{49824}\", \"{3605}\", \"{13349}\", \"{1793}\", \"{705}\", \"{68527}\", \"{42120}\", \"{7696}\",\n\t\"{9316}\", \"{2709}\", \"{12245}\", \"{78287}\", \"{24319}\", \"{5700}\", \"{3943}\", \"{43987}\", \"{42590}\", \"{2554}\",\n\t\"{12018}\", \"{843}\", \"{17187}\", \"{41811}\", \"{28849}\", \"{60809}\", \"{8047}\", \"{3458}\", \"{5846}\", \"{45882}\",\n\t\"{558}\", \"{1323}\", \"{18317}\", \"{13708}\", \"{45873}\", \"{344}\", \"{48072}\", \"{7921}\", \"{3244}\", \"{43280}\",\n\t\"{63862}\", \"{1824}\", \"{5341}\", \"{24758}\", \"{89264}\", \"{42561}\", \"{2348}\", \"{9757}\", \"{29899}\", \"{2265}\",\n\t\"{6900}\", \"{2488}\", \"{4896}\", \"{44852}\", \"{1909}\", \"{5481}\", \"{2993}\", \"{3369}\", \"{43540}\", \"{3584}\",\n\t\"{269}\", \"{1412}\", \"{484}\", \"{51880}\", \"{40830}\", \"{475}\", \"{4391}\", \"{298}\", \"{3575}\", \"{57282}\",\n\t\"{3398}\", \"{2962}\", \"{5470}\", \"{24069}\", \"{66821}\", \"{4867}\", \"{2479}\", \"{9066}\", \"{2294}\", \"{29868}\",\n\t\"{69506}\", \"{12585}\", \"{13992}\", \"{12368}\", \"{2624}\", \"{47192}\", \"{27958}\", \"{10413}\", \"{1053}\", \"{628}\",\n\t\"{100821}\", \"{13264}\", \"{3728}\", \"{8337}\", \"{15897}\", \"{43101}\", \"{46682}\", \"{3134}\", \"{11303}\", \"{48302}\",\n\t\"{13295}\", \"{68216}\", \"{13478}\", \"{18067}\", \"{9427}\", \"{2038}\", \"{42611}\", \"{8830}\", \"{12399}\", \"{5031}\",\n\t\"{12574}\", \"{104843}\", \"{8919}\", \"{10326}\", \"{2111}\", \"{23508}\", \"{19042}\", \"{40043}\", \"{5118}\", \"{21273}\",\n\t\"{6182}\", \"{14558}\", \"{59487}\", \"{8402}\", \"{79593}\", \"{13551}\", \"{1766}\", \"{55091}\", \"{41753}\", \"{1797}\",\n\t\"{701}\", \"{4608}\", \"{11436}\", \"{48437}\", \"{22218}\", \"{3601}\", \"{12241}\", \"{20878}\", \"{54781}\", \"{5704}\",\n\t\"{15248}\", \"{7692}\", \"{9312}\", \"{58397}\", \"{40402}\", \"{847}\", \"{17183}\", \"{5559}\", \"{3947}\", \"{43983}\",\n\t\"{23149}\", \"{2550}\", \"{5842}\", \"{45886}\", \"{19918}\", \"{1327}\", \"{14119}\", \"{88570}\", \"{8043}\", \"{47910}\",\n\t\"{48076}\", \"{7925}\", \"{3240}\", \"{22659}\", \"{18313}\", \"{41312}\", \"{4249}\", \"{340}\", \"{89260}\", \"{6829}\",\n\t\"{43972}\", \"{9753}\", \"{63866}\", \"{1820}\", \"{5345}\", \"{45381}\", \"{3706}\", \"{8319}\", \"{6699}\", \"{11531}\",\n\t\"{68424}\", \"{606}\", \"{1690}\", \"{16538}\", \"{7178}\", \"{9215}\", \"{7795}\", \"{42023}\", \"{5603}\", \"{21568}\",\n\t\"{19759}\", \"{12346}\", \"{21374}\", \"{69334}\", \"{17228}\", \"{19145}\", \"{9409}\", \"{2016}\", \"{10221}\", \"{7189}\",\n\t\"{20278}\", \"{1661}\", \"{13456}\", \"{18049}\", \"{8505}\", \"{6668}\", \"{43733}\", \"{6085}\", \"{247}\", \"{45970}\",\n\t\"{16179}\", \"{18214}\", \"{8758}\", \"{3347}\", \"{7822}\", \"{48171}\", \"{17698}\", \"{5242}\", \"{1927}\", \"{19318}\",\n\t\"{9654}\", \"{7539}\", \"{42462}\", \"{64968}\", \"{2457}\", \"{9048}\", \"{43884}\", \"{3840}\", \"{41912}\", \"{17084}\",\n\t\"{940}\", \"{4849}\", \"{6229}\", \"{8144}\", \"{42988}\", \"{43372}\", \"{1220}\", \"{16188}\", \"{18408}\", \"{5945}\",\n\t\"{2865}\", \"{29299}\", \"{27719}\", \"{3472}\", \"{41520}\", \"{4296}\", \"{572}\", \"{1309}\", \"{3969}\", \"{2393}\",\n\t\"{9161}\", \"{28578}\", \"{4960}\", \"{869}\", \"{51280}\", \"{5577}\", \"{5586}\", \"{40230}\", \"{898}\", \"{4991}\",\n\t\"{28589}\", \"{6807}\", \"{2362}\", \"{3998}\", \"{51987}\", \"{583}\", \"{1515}\", \"{50590}\", \"{3483}\", \"{11059}\",\n\t\"{29268}\", \"{2894}\", \"{18160}\", \"{41161}\", \"{1748}\", \"{133}\", \"{38699}\", \"{11204}\", \"{3033}\", \"{27358}\",\n\t\"{25938}\", \"{12473}\", \"{5136}\", \"{40680}\", \"{8937}\", \"{10308}\", \"{28139}\", \"{9520}\", \"{10514}\", \"{39189}\",\n\t\"{26448}\", \"{2723}\", \"{40671}\", \"{13895}\", \"{12482}\", \"{34988}\", \"{11418}\", \"{15990}\", \"{8230}\", \"{29629}\",\n\t\"{13363}\", \"{12999}\", \"{41190}\", \"{1154}\", \"{1079}\", \"{602}\", \"{1694}\", \"{41650}\", \"{3702}\", \"{27469}\",\n\t\"{48534}\", \"{11535}\", \"{5607}\", \"{54682}\", \"{78380}\", \"{12342}\", \"{28608}\", \"{9211}\", \"{7791}\", \"{10439}\",\n\t\"{26379}\", \"{2012}\", \"{10225}\", \"{49224}\", \"{21370}\", \"{13949}\", \"{40140}\", \"{19141}\", \"{8501}\", \"{29118}\",\n\t\"{11329}\", \"{6081}\", \"{55192}\", \"{1665}\", \"{13452}\", \"{1088}\", \"{11799}\", \"{3343}\", \"{7826}\", \"{48175}\",\n\t\"{243}\", \"{1438}\", \"{41211}\", \"{18210}\", \"{9650}\", \"{28249}\", \"{10078}\", \"{49079}\", \"{45282}\", \"{5246}\",\n\t\"{1923}\", \"{63965}\", \"{41916}\", \"{17080}\", \"{944}\", \"{40501}\", \"{2453}\", \"{10089}\", \"{43880}\", \"{3844}\",\n\t\"{1224}\", \"{44592}\", \"{45985}\", \"{5941}\", \"{29559}\", \"{8140}\", \"{48769}\", \"{2948}\", \"{5868}\", \"{4292}\",\n\t\"{576}\", \"{40933}\", \"{2861}\", \"{48640}\", \"{8069}\", \"{3476}\", \"{4964}\", \"{19429}\", \"{21618}\", \"{5573}\",\n\t\"{9788}\", \"{2397}\", \"{9165}\", \"{7208}\", \"{49150}\", \"{6803}\", \"{2366}\", \"{9779}\", \"{5582}\", \"{17158}\",\n\t\"{44951}\", \"{4995}\", \"{3487}\", \"{8098}\", \"{6518}\", \"{2890}\", \"{18339}\", \"{587}\", \"{1511}\", \"{5899}\",\n\t\"{23839}\", \"{11200}\", \"{3037}\", \"{8428}\", \"{18164}\", \"{16209}\", \"{18789}\", \"{137}\", \"{8933}\", \"{42712}\",\n\t\"{7649}\", \"{9524}\", \"{19068}\", \"{12477}\", \"{5132}\", \"{21259}\", \"{17519}\", \"{13891}\", \"{12486}\", \"{19099}\",\n\t\"{10510}\", \"{14898}\", \"{9338}\", \"{2727}\", \"{13367}\", \"{18778}\", \"{20549}\", \"{1150}\", \"{43002}\", \"{15994}\",\n\t\"{8234}\", \"{6159}\", \"{2602}\", \"{26569}\", \"{49434}\", \"{10435}\", \"{69520}\", \"{21560}\", \"{19751}\", \"{40750}\",\n\t\"{29708}\", \"{8311}\", \"{6691}\", \"{11539}\", \"{1075}\", \"{55782}\", \"{1698}\", \"{13242}\", \"{20270}\", \"{1669}\",\n\t\"{41040}\", \"{1084}\", \"{27279}\", \"{3112}\", \"{11325}\", \"{48324}\", \"{54092}\", \"{5017}\", \"{12552}\", \"{25819}\",\n\t\"{9401}\", \"{28018}\", \"{10229}\", \"{7181}\", \"{17690}\", \"{44874}\", \"{40311}\", \"{19310}\", \"{10699}\", \"{2243}\",\n\t\"{6926}\", \"{49075}\", \"{44382}\", \"{1434}\", \"{13603}\", \"{62865}\", \"{8750}\", \"{29349}\", \"{11178}\", \"{48179}\",\n\t\"{3553}\", \"{11189}\", \"{42980}\", \"{2944}\", \"{1228}\", \"{453}\", \"{18400}\", \"{41401}\", \"{28459}\", \"{9040}\",\n\t\"{49669}\", \"{3848}\", \"{5456}\", \"{45492}\", \"{948}\", \"{4841}\", \"{3961}\", \"{49740}\", \"{9169}\", \"{27}\",\n\t\"{4968}\", \"{861}\", \"{18832}\", \"{41833}\", \"{8688}\", \"{3297}\", \"{8065}\", \"{6308}\", \"{397}\", \"{18529}\",\n\t\"{20718}\", \"{1301}\", \"{4482}\", \"{16058}\", \"{45851}\", \"{366}\", \"{48050}\", \"{7903}\", \"{3266}\", \"{8679}\",\n\t\"{19239}\", \"{1806}\", \"{890}\", \"{4999}\", \"{2587}\", \"{9198}\", \"{7418}\", \"{3990}\", \"{19064}\", \"{17309}\",\n\t\"{19689}\", \"{12296}\", \"{22939}\", \"{10300}\", \"{2137}\", \"{9528}\", \"{18168}\", \"{13577}\", \"{1740}\", \"{20359}\",\n\t\"{9833}\", \"{43612}\", \"{6749}\", \"{8424}\", \"{11410}\", \"{15998}\", \"{8238}\", \"{3627}\", \"{16419}\", \"{12991}\",\n\t\"{727}\", \"{18199}\", \"{42102}\", \"{14894}\", \"{9334}\", \"{7059}\", \"{12267}\", \"{19678}\", \"{21449}\", \"{5722}\",\n\t\"{69524}\", \"{21564}\", \"{19755}\", \"{17438}\", \"{2606}\", \"{9219}\", \"{7799}\", \"{10431}\", \"{1071}\", \"{20468}\",\n\t\"{18659}\", \"{13246}\", \"{6078}\", \"{8315}\", \"{6695}\", \"{43123}\", \"{8509}\", \"{3116}\", \"{11321}\", \"{6089}\",\n\t\"{20274}\", \"{68234}\", \"{16328}\", \"{1080}\", \"{9405}\", \"{7768}\", \"{42633}\", \"{7185}\", \"{21378}\", \"{5013}\",\n\t\"{12556}\", \"{19149}\", \"{9658}\", \"{2247}\", \"{6922}\", \"{49071}\", \"{17694}\", \"{44870}\", \"{17079}\", \"{19314}\",\n\t\"{8754}\", \"{6439}\", \"{43562}\", \"{65868}\", \"{16798}\", \"{1430}\", \"{13607}\", \"{18218}\", \"{40812}\", \"{457}\",\n\t\"{18404}\", \"{5949}\", \"{3557}\", \"{8148}\", \"{42984}\", \"{2940}\", \"{5452}\", \"{17088}\", \"{19508}\", \"{4845}\",\n\t\"{7329}\", \"{9044}\", \"{43888}\", \"{42272}\", \"{40420}\", \"{865}\", \"{18836}\", \"{41837}\", \"{3965}\", \"{28399}\",\n\t\"{26619}\", \"{23}\", \"{393}\", \"{67826}\", \"{50380}\", \"{1305}\", \"{2869}\", \"{3293}\", \"{8061}\", \"{29478}\",\n\t\"{29489}\", \"{7907}\", \"{3262}\", \"{2898}\", \"{4486}\", \"{41330}\", \"{1519}\", \"{362}\", \"{2583}\", \"{10159}\",\n\t\"{28368}\", \"{3994}\", \"{50887}\", \"{1802}\", \"{894}\", \"{51490}\", \"{39799}\", \"{10304}\", \"{2133}\", \"{26258}\",\n\t\"{19060}\", \"{40061}\", \"{13868}\", \"{12292}\", \"{9837}\", \"{11208}\", \"{29039}\", \"{8420}\", \"{24838}\", \"{13573}\",\n\t\"{1744}\", \"{41780}\", \"{41771}\", \"{12995}\", \"{723}\", \"{1158}\", \"{11414}\", \"{38089}\", \"{27548}\", \"{3623}\",\n\t\"{12263}\", \"{13899}\", \"{40090}\", \"{5726}\", \"{10518}\", \"{14890}\", \"{9330}\", \"{28729}\", \"{45842}\", \"{375}\",\n\t\"{4491}\", \"{598}\", \"{3275}\", \"{28889}\", \"{3498}\", \"{7910}\", \"{883}\", \"{24769}\", \"{50890}\", \"{1815}\",\n\t\"{2379}\", \"{3983}\", \"{2594}\", \"{42550}\", \"{56292}\", \"{34}\", \"{3972}\", \"{2388}\", \"{18821}\", \"{41820}\",\n\t\"{12029}\", \"{872}\", \"{8076}\", \"{3469}\", \"{28878}\", \"{3284}\", \"{569}\", \"{1312}\", \"{384}\", \"{67831}\",\n\t\"{46182}\", \"{3634}\", \"{11403}\", \"{26948}\", \"{734}\", \"{68516}\", \"{13378}\", \"{12982}\", \"{9327}\", \"{2738}\",\n\t\"{42111}\", \"{14887}\", \"{12499}\", \"{5731}\", \"{12274}\", \"{101831}\", \"{69206}\", \"{12285}\", \"{19077}\", \"{12468}\",\n\t\"{2124}\", \"{47692}\", \"{49312}\", \"{10313}\", \"{1753}\", \"{128}\", \"{105853}\", \"{13564}\", \"{3028}\", \"{8437}\",\n\t\"{9820}\", \"{43601}\", \"{41053}\", \"{1097}\", \"{20263}\", \"{4108}\", \"{11336}\", \"{9909}\", \"{22518}\", \"{3101}\",\n\t\"{12541}\", \"{78583}\", \"{54081}\", \"{5004}\", \"{15548}\", \"{7192}\", \"{9412}\", \"{58497}\", \"{49427}\", \"{10426}\",\n\t\"{2611}\", \"{23208}\", \"{19742}\", \"{40743}\", \"{5618}\", \"{21573}\", \"{6682}\", \"{14258}\", \"{59387}\", \"{8302}\",\n\t\"{21868}\", \"{13251}\", \"{1066}\", \"{55791}\", \"{42993}\", \"{2957}\", \"{3540}\", \"{22159}\", \"{18413}\", \"{41412}\",\n\t\"{4549}\", \"{440}\", \"{89560}\", \"{15109}\", \"{46900}\", \"{9053}\", \"{44896}\", \"{4852}\", \"{5445}\", \"{18908}\",\n\t\"{40302}\", \"{19303}\", \"{1}\", \"{5259}\", \"{6935}\", \"{49066}\", \"{23649}\", \"{2250}\", \"{13610}\", \"{62876}\",\n\t\"{44391}\", \"{1427}\", \"{7839}\", \"{88270}\", \"{8743}\", \"{42962}\", \"{3271}\", \"{22668}\", \"{48047}\", \"{7914}\",\n\t\"{4278}\", \"{371}\", \"{4495}\", \"{41323}\", \"{43943}\", \"{3987}\", \"{2590}\", \"{6818}\", \"{887}\", \"{51483}\",\n\t\"{5599}\", \"{1811}\", \"{18825}\", \"{5568}\", \"{40433}\", \"{876}\", \"{23178}\", \"{30}\", \"{3976}\", \"{49757}\",\n\t\"{19929}\", \"{1316}\", \"{380}\", \"{4289}\", \"{8072}\", \"{47921}\", \"{14128}\", \"{3280}\", \"{730}\", \"{4639}\",\n\t\"{41762}\", \"{12986}\", \"{14598}\", \"{3630}\", \"{11407}\", \"{48406}\", \"{40083}\", \"{5735}\", \"{12270}\", \"{20849}\",\n\t\"{9323}\", \"{109746}\", \"{15279}\", \"{14883}\", \"{2120}\", \"{15288}\", \"{8928}\", \"{10317}\", \"{5129}\", \"{12281}\",\n\t\"{19073}\", \"{40072}\", \"{82908}\", \"{8433}\", \"{9824}\", \"{14569}\", \"{1757}\", \"{41793}\", \"{105857}\", \"{13560}\",\n\t\"{11332}\", \"{48333}\", \"{52180}\", \"{3105}\", \"{13449}\", \"{1093}\", \"{20267}\", \"{68227}\", \"{42620}\", \"{7196}\",\n\t\"{9416}\", \"{2009}\", \"{12545}\", \"{78587}\", \"{24419}\", \"{5000}\", \"{19746}\", \"{12359}\", \"{69537}\", \"{21577}\",\n\t\"{27969}\", \"{10422}\", \"{2615}\", \"{48834}\", \"{79297}\", \"{13255}\", \"{1062}\", \"{619}\", \"{6686}\", \"{43130}\",\n\t\"{3719}\", \"{8306}\", \"{18417}\", \"{13008}\", \"{40801}\", \"{444}\", \"{42997}\", \"{2953}\", \"{3544}\", \"{43580}\",\n\t\"{44892}\", \"{4856}\", \"{5441}\", \"{24058}\", \"{61819}\", \"{29859}\", \"{2448}\", \"{9057}\", \"{6931}\", \"{49062}\",\n\t\"{42290}\", \"{2254}\", \"{1938}\", \"{19307}\", \"{5}\", \"{44863}\", \"{43571}\", \"{88274}\", \"{8747}\", \"{3358}\",\n\t\"{13614}\", \"{62872}\", \"{258}\", \"{1423}\", \"{5378}\", \"{4982}\", \"{5595}\", \"{40223}\", \"{2371}\", \"{23768}\",\n\t\"{49147}\", \"{6814}\", \"{1506}\", \"{50583}\", \"{4499}\", \"{590}\", \"{42843}\", \"{2887}\", \"{3490}\", \"{7918}\",\n\t\"{22078}\", \"{3461}\", \"{2876}\", \"{48657}\", \"{561}\", \"{4468}\", \"{41533}\", \"{4285}\", \"{9172}\", \"{46821}\",\n\t\"{15028}\", \"{2380}\", \"{18829}\", \"{5564}\", \"{4973}\", \"{5389}\", \"{15498}\", \"{2730}\", \"{10507}\", \"{49506}\",\n\t\"{12491}\", \"{5739}\", \"{40662}\", \"{13886}\", \"{8223}\", \"{108646}\", \"{14379}\", \"{15983}\", \"{41183}\", \"{1147}\",\n\t\"{13370}\", \"{21949}\", \"{4029}\", \"{120}\", \"{18173}\", \"{41172}\", \"{3020}\", \"{14388}\", \"{9828}\", \"{11217}\",\n\t\"{5125}\", \"{40693}\", \"{104957}\", \"{12460}\", \"{83808}\", \"{9533}\", \"{8924}\", \"{15469}\", \"{12549}\", \"{19156}\",\n\t\"{21367}\", \"{69327}\", \"{10232}\", \"{49233}\", \"{53080}\", \"{2005}\", \"{13445}\", \"{79487}\", \"{25519}\", \"{1672}\",\n\t\"{43720}\", \"{6096}\", \"{8516}\", \"{3109}\", \"{26869}\", \"{11522}\", \"{3715}\", \"{49934}\", \"{1683}\", \"{13259}\",\n\t\"{68437}\", \"{615}\", \"{7786}\", \"{42030}\", \"{2619}\", \"{9206}\", \"{78397}\", \"{12355}\", \"{5610}\", \"{24209}\",\n\t\"{43897}\", \"{3853}\", \"{2444}\", \"{42480}\", \"{953}\", \"{12108}\", \"{41901}\", \"{17097}\", \"{60919}\", \"{28959}\",\n\t\"{3548}\", \"{8157}\", \"{45992}\", \"{5956}\", \"{1233}\", \"{448}\", \"{13618}\", \"{18207}\", \"{254}\", \"{45963}\",\n\t\"{7831}\", \"{48162}\", \"{43390}\", \"{3354}\", \"{1934}\", \"{63972}\", \"{9}\", \"{5251}\", \"{42471}\", \"{89374}\",\n\t\"{9647}\", \"{2258}\", \"{2375}\", \"{29989}\", \"{2598}\", \"{6810}\", \"{44942}\", \"{4986}\", \"{5591}\", \"{1819}\",\n\t\"{3279}\", \"{2883}\", \"{3494}\", \"{43450}\", \"{1502}\", \"{379}\", \"{51990}\", \"{594}\", \"{565}\", \"{40920}\",\n\t\"{388}\", \"{4281}\", \"{57392}\", \"{3465}\", \"{2872}\", \"{3288}\", \"{24179}\", \"{5560}\", \"{4977}\", \"{66931}\",\n\t\"{9176}\", \"{38}\", \"{29978}\", \"{2384}\", \"{12495}\", \"{69416}\", \"{12278}\", \"{13882}\", \"{47082}\", \"{2734}\",\n\t\"{10503}\", \"{27848}\", \"{738}\", \"{1143}\", \"{13374}\", \"{100931}\", \"{8227}\", \"{3638}\", \"{43011}\", \"{15987}\",\n\t\"{3024}\", \"{46792}\", \"{48212}\", \"{11213}\", \"{68306}\", \"{124}\", \"{18177}\", \"{13568}\", \"{2128}\", \"{9537}\",\n\t\"{8920}\", \"{42701}\", \"{5121}\", \"{12289}\", \"{104953}\", \"{12464}\", \"{10236}\", \"{8809}\", \"{23418}\", \"{2001}\",\n\t\"{40153}\", \"{19152}\", \"{21363}\", \"{5008}\", \"{14448}\", \"{6092}\", \"{8512}\", \"{59597}\", \"{13441}\", \"{79483}\",\n\t\"{55181}\", \"{1676}\", \"{1687}\", \"{41643}\", \"{4718}\", \"{611}\", \"{48527}\", \"{11526}\", \"{3711}\", \"{22308}\",\n\t\"{20968}\", \"{12351}\", \"{5614}\", \"{54691}\", \"{7782}\", \"{15358}\", \"{58287}\", \"{9202}\", \"{957}\", \"{40512}\",\n\t\"{5449}\", \"{17093}\", \"{43893}\", \"{3857}\", \"{2440}\", \"{23059}\", \"{45996}\", \"{5952}\", \"{1237}\", \"{19808}\",\n\t\"{88460}\", \"{14009}\", \"{47800}\", \"{8153}\", \"{7835}\", \"{48166}\", \"{22749}\", \"{3350}\", \"{41202}\", \"{18203}\",\n\t\"{250}\", \"{4359}\", \"{6939}\", \"{89370}\", \"{9643}\", \"{43862}\", \"{1930}\", \"{63976}\", \"{45291}\", \"{5255}\",\n\t\"{2821}\", \"{48600}\", \"{8029}\", \"{3436}\", \"{5828}\", \"{18565}\", \"{536}\", \"{18388}\", \"{42313}\", \"{39390}\",\n\t\"{9125}\", \"{7248}\", \"{4924}\", \"{19469}\", \"{21658}\", \"{5533}\", \"{19275}\", \"{17118}\", \"{19498}\", \"{12087}\",\n\t\"{46887}\", \"{6843}\", \"{2326}\", \"{9739}\", \"{18379}\", \"{13766}\", \"{1551}\", \"{20148}\", \"{38480}\", \"{43403}\",\n\t\"{6558}\", \"{8635}\", \"{4693}\", \"{16249}\", \"{68355}\", \"{177}\", \"{23879}\", \"{8285}\", \"{3077}\", \"{8468}\",\n\t\"{19028}\", \"{12437}\", \"{5172}\", \"{21219}\", \"{2796}\", \"{9389}\", \"{7609}\", \"{9564}\", \"{9595}\", \"{49551}\",\n\t\"{9378}\", \"{2767}\", \"{17559}\", \"{5183}\", \"{21405}\", \"{69445}\", \"{8499}\", \"{3086}\", \"{8274}\", \"{6119}\",\n\t\"{186}\", \"{18738}\", \"{20509}\", \"{1110}\", \"{3742}\", \"{11398}\", \"{48574}\", \"{11575}\", \"{1039}\", \"{642}\",\n\t\"{18611}\", \"{41610}\", \"{28648}\", \"{9251}\", \"{27932}\", \"{10479}\", \"{5647}\", \"{45683}\", \"{69381}\", \"{12302}\",\n\t\"{17481}\", \"{13909}\", \"{40100}\", \"{16896}\", \"{10488}\", \"{2052}\", \"{10265}\", \"{49264}\", \"{44193}\", \"{1625}\",\n\t\"{13412}\", \"{24959}\", \"{8541}\", \"{29158}\", \"{11369}\", \"{9956}\", \"{203}\", \"{1478}\", \"{41251}\", \"{1295}\",\n\t\"{27068}\", \"{3303}\", \"{7866}\", \"{48135}\", \"{54283}\", \"{5206}\", \"{1963}\", \"{63925}\", \"{9610}\", \"{28209}\",\n\t\"{10038}\", \"{7390}\", \"{2413}\", \"{26778}\", \"{49625}\", \"{3804}\", \"{41956}\", \"{1992}\", \"{904}\", \"{40541}\",\n\t\"{29519}\", \"{7897}\", \"{6480}\", \"{2908}\", \"{1264}\", \"{55593}\", \"{1489}\", \"{5901}\", \"{41560}\", \"{18561}\",\n\t\"{532}\", \"{1349}\", \"{2825}\", \"{38298}\", \"{27759}\", \"{3432}\", \"{4920}\", \"{829}\", \"{40281}\", \"{5537}\",\n\t\"{3929}\", \"{39394}\", \"{9121}\", \"{28538}\", \"{39588}\", \"{6847}\", \"{2322}\", \"{26049}\", \"{19271}\", \"{40270}\",\n\t\"{44915}\", \"{12083}\", \"{38484}\", \"{11019}\", \"{29228}\", \"{8631}\", \"{40986}\", \"{13762}\", \"{1555}\", \"{41591}\",\n\t\"{29698}\", \"{8281}\", \"{3073}\", \"{27318}\", \"{4697}\", \"{41121}\", \"{1708}\", \"{173}\", \"{2792}\", \"{10348}\",\n\t\"{28179}\", \"{9560}\", \"{25978}\", \"{12433}\", \"{5176}\", \"{51681}\", \"{40631}\", \"{5187}\", \"{21401}\", \"{25989}\",\n\t\"{9591}\", \"{28188}\", \"{26408}\", \"{2763}\", \"{182}\", \"{100966}\", \"{50191}\", \"{1114}\", \"{11458}\", \"{3082}\",\n\t\"{8270}\", \"{29669}\", \"{68464}\", \"{646}\", \"{18615}\", \"{16578}\", \"{3746}\", \"{8359}\", \"{48570}\", \"{11571}\",\n\t\"{5643}\", \"{17299}\", \"{19719}\", \"{12306}\", \"{7138}\", \"{9255}\", \"{27936}\", \"{42063}\", \"{9449}\", \"{2056}\",\n\t\"{10261}\", \"{22858}\", \"{17485}\", \"{69374}\", \"{17268}\", \"{16892}\", \"{8545}\", \"{6628}\", \"{43773}\", \"{9952}\",\n\t\"{16589}\", \"{1621}\", \"{13416}\", \"{18009}\", \"{8718}\", \"{3307}\", \"{7862}\", \"{6298}\", \"{207}\", \"{45930}\",\n\t\"{16139}\", \"{1291}\", \"{9614}\", \"{7579}\", \"{42422}\", \"{7394}\", \"{21169}\", \"{5202}\", \"{1967}\", \"{19358}\",\n\t\"{41952}\", \"{1996}\", \"{900}\", \"{4809}\", \"{2417}\", \"{9008}\", \"{7588}\", \"{3800}\", \"{1260}\", \"{20679}\",\n\t\"{18448}\", \"{5905}\", \"{6269}\", \"{7893}\", \"{6484}\", \"{43332}\", \"{3925}\", \"{39398}\", \"{26659}\", \"{63}\",\n\t\"{40460}\", \"{825}\", \"{12693}\", \"{41877}\", \"{2829}\", \"{38294}\", \"{8021}\", \"{29438}\", \"{5820}\", \"{67866}\",\n\t\"{41381}\", \"{1345}\", \"{18371}\", \"{41370}\", \"{1559}\", \"{322}\", \"{38488}\", \"{7947}\", \"{3222}\", \"{27149}\",\n\t\"{41886}\", \"{1842}\", \"{5327}\", \"{40491}\", \"{92}\", \"{10119}\", \"{28328}\", \"{9731}\", \"{5797}\", \"{40021}\",\n\t\"{13828}\", \"{21211}\", \"{28798}\", \"{9381}\", \"{2173}\", \"{26218}\", \"{24878}\", \"{792}\", \"{1704}\", \"{50781}\",\n\t\"{3692}\", \"{11248}\", \"{29079}\", \"{8460}\", \"{8491}\", \"{29088}\", \"{27508}\", \"{3663}\", \"{41731}\", \"{4087}\",\n\t\"{763}\", \"{1118}\", \"{10558}\", \"{2182}\", \"{9370}\", \"{28769}\", \"{12223}\", \"{101866}\", \"{51091}\", \"{5766}\",\n\t\"{2646}\", \"{9259}\", \"{49470}\", \"{10471}\", \"{69564}\", \"{17295}\", \"{19715}\", \"{17478}\", \"{6038}\", \"{8355}\",\n\t\"{26836}\", \"{43163}\", \"{1031}\", \"{16399}\", \"{18619}\", \"{13206}\", \"{16585}\", \"{68274}\", \"{16368}\", \"{17992}\",\n\t\"{8549}\", \"{3156}\", \"{11361}\", \"{23958}\", \"{17489}\", \"{5053}\", \"{12516}\", \"{19109}\", \"{9445}\", \"{7728}\",\n\t\"{42673}\", \"{8852}\", \"{21165}\", \"{44830}\", \"{17039}\", \"{19354}\", \"{9618}\", \"{2207}\", \"{6962}\", \"{7398}\",\n\t\"{20069}\", \"{1470}\", \"{13647}\", \"{18258}\", \"{8714}\", \"{6479}\", \"{43522}\", \"{6294}\", \"{3517}\", \"{8108}\",\n\t\"{6488}\", \"{2900}\", \"{40852}\", \"{417}\", \"{1481}\", \"{5909}\", \"{7369}\", \"{6993}\", \"{7584}\", \"{42232}\",\n\t\"{5412}\", \"{21779}\", \"{19548}\", \"{4805}\", \"{4928}\", \"{821}\", \"{12697}\", \"{19288}\", \"{3921}\", \"{49700}\",\n\t\"{9129}\", \"{67}\", \"{5824}\", \"{18569}\", \"{20758}\", \"{1341}\", \"{43213}\", \"{38290}\", \"{8025}\", \"{6348}\",\n\t\"{47987}\", \"{7943}\", \"{3226}\", \"{8639}\", \"{18375}\", \"{16018}\", \"{18598}\", \"{326}\", \"{96}\", \"{42503}\",\n\t\"{7458}\", \"{9735}\", \"{19279}\", \"{1846}\", \"{5323}\", \"{21048}\", \"{22979}\", \"{9385}\", \"{2177}\", \"{9568}\",\n\t\"{5793}\", \"{17349}\", \"{69255}\", \"{21215}\", \"{3696}\", \"{8289}\", \"{6709}\", \"{8464}\", \"{18128}\", \"{796}\",\n\t\"{1700}\", \"{20319}\", \"{16459}\", \"{4083}\", \"{767}\", \"{68545}\", \"{8495}\", \"{48451}\", \"{8278}\", \"{3667}\",\n\t\"{12227}\", \"{19638}\", \"{21409}\", \"{5762}\", \"{9599}\", \"{2186}\", \"{9374}\", \"{7019}\", \"{69560}\", \"{17291}\",\n\t\"{19711}\", \"{40710}\", \"{2642}\", \"{10298}\", \"{49474}\", \"{10475}\", \"{1035}\", \"{44783}\", \"{68281}\", \"{13202}\",\n\t\"{29748}\", \"{8351}\", \"{26832}\", \"{11579}\", \"{11588}\", \"{3152}\", \"{11365}\", \"{48364}\", \"{16581}\", \"{1629}\",\n\t\"{41000}\", \"{17996}\", \"{9441}\", \"{28058}\", \"{10269}\", \"{8856}\", \"{45093}\", \"{5057}\", \"{12512}\", \"{25859}\",\n\t\"{26168}\", \"{2203}\", \"{6966}\", \"{49035}\", \"{21161}\", \"{44834}\", \"{40351}\", \"{19350}\", \"{8710}\", \"{29309}\",\n\t\"{11138}\", \"{6290}\", \"{55383}\", \"{1474}\", \"{13643}\", \"{1299}\", \"{1268}\", \"{413}\", \"{1485}\", \"{41441}\",\n\t\"{3513}\", \"{27678}\", \"{48725}\", \"{2904}\", \"{5416}\", \"{54493}\", \"{908}\", \"{4801}\", \"{28419}\", \"{6997}\",\n\t\"{7580}\", \"{3808}\", \"{13409}\", \"{17981}\", \"{16596}\", \"{68267}\", \"{11372}\", \"{10988}\", \"{43181}\", \"{3145}\",\n\t\"{12505}\", \"{69586}\", \"{24459}\", \"{5040}\", \"{42660}\", \"{8841}\", \"{9456}\", \"{2049}\", \"{27929}\", \"{10462}\",\n\t\"{2655}\", \"{42691}\", \"{19706}\", \"{12319}\", \"{69577}\", \"{17286}\", \"{26825}\", \"{43170}\", \"{3759}\", \"{8346}\",\n\t\"{68296}\", \"{13215}\", \"{1022}\", \"{659}\", \"{48732}\", \"{2913}\", \"{3504}\", \"{52581}\", \"{1492}\", \"{13048}\",\n\t\"{40841}\", \"{404}\", \"{7597}\", \"{29819}\", \"{2408}\", \"{6980}\", \"{55893}\", \"{4816}\", \"{5401}\", \"{1989}\",\n\t\"{1978}\", \"{19347}\", \"{21176}\", \"{44823}\", \"{6971}\", \"{49022}\", \"{53291}\", \"{2214}\", \"{13654}\", \"{62832}\",\n\t\"{218}\", \"{1463}\", \"{43531}\", \"{6287}\", \"{8707}\", \"{3318}\", \"{4238}\", \"{331}\", \"{18362}\", \"{19998}\",\n\t\"{3231}\", \"{14199}\", \"{47990}\", \"{7954}\", \"{5334}\", \"{40482}\", \"{41895}\", \"{1851}\", \"{43903}\", \"{9722}\",\n\t\"{81}\", \"{6858}\", \"{15689}\", \"{70}\", \"{3936}\", \"{49717}\", \"{12680}\", \"{5528}\", \"{40473}\", \"{836}\",\n\t\"{8032}\", \"{47961}\", \"{14168}\", \"{38287}\", \"{19969}\", \"{1356}\", \"{5833}\", \"{67875}\", \"{22269}\", \"{3670}\",\n\t\"{8482}\", \"{48446}\", \"{770}\", \"{4679}\", \"{41722}\", \"{4094}\", \"{9363}\", \"{8999}\", \"{15239}\", \"{2191}\",\n\t\"{51082}\", \"{5775}\", \"{12230}\", \"{5198}\", \"{5169}\", \"{21202}\", \"{5784}\", \"{40032}\", \"{2160}\", \"{23579}\",\n\t\"{8968}\", \"{9392}\", \"{1717}\", \"{50792}\", \"{4688}\", \"{781}\", \"{82948}\", \"{8473}\", \"{3681}\", \"{14529}\",\n\t\"{11376}\", \"{9949}\", \"{22558}\", \"{3141}\", \"{41013}\", \"{17985}\", \"{16592}\", \"{4148}\", \"{15508}\", \"{8845}\",\n\t\"{9452}\", \"{49496}\", \"{12501}\", \"{16889}\", \"{45080}\", \"{5044}\", \"{19702}\", \"{40703}\", \"{5658}\", \"{17282}\",\n\t\"{49467}\", \"{10466}\", \"{2651}\", \"{23248}\", \"{21828}\", \"{13211}\", \"{1026}\", \"{44790}\", \"{26821}\", \"{14218}\",\n\t\"{48386}\", \"{8342}\", \"{1496}\", \"{41452}\", \"{4509}\", \"{400}\", \"{48736}\", \"{2917}\", \"{3500}\", \"{7888}\",\n\t\"{55897}\", \"{4812}\", \"{5405}\", \"{18948}\", \"{7593}\", \"{15149}\", \"{46940}\", \"{6984}\", \"{6975}\", \"{49026}\",\n\t\"{23609}\", \"{2210}\", \"{40342}\", \"{19343}\", \"{21172}\", \"{5219}\", \"{7879}\", \"{6283}\", \"{8703}\", \"{42922}\",\n\t\"{13650}\", \"{62836}\", \"{55390}\", \"{1467}\", \"{3235}\", \"{39888}\", \"{47994}\", \"{7950}\", \"{45802}\", \"{335}\",\n\t\"{18366}\", \"{13779}\", \"{2339}\", \"{9726}\", \"{85}\", \"{42510}\", \"{5330}\", \"{12098}\", \"{41891}\", \"{1855}\",\n\t\"{12684}\", \"{41860}\", \"{12069}\", \"{832}\", \"{47293}\", \"{74}\", \"{3932}\", \"{49713}\", \"{529}\", \"{1352}\",\n\t\"{5837}\", \"{67871}\", \"{8036}\", \"{3429}\", \"{28838}\", \"{38283}\", \"{774}\", \"{68556}\", \"{199}\", \"{4090}\",\n\t\"{49855}\", \"{3674}\", \"{8486}\", \"{3099}\", \"{24368}\", \"{5771}\", \"{12234}\", \"{101871}\", \"{9367}\", \"{2778}\",\n\t\"{42151}\", \"{2195}\", \"{2164}\", \"{56693}\", \"{2789}\", \"{9396}\", \"{69246}\", \"{21206}\", \"{5780}\", \"{12428}\",\n\t\"{3068}\", \"{8477}\", \"{3685}\", \"{43641}\", \"{1713}\", \"{168}\", \"{105813}\", \"{785}\", \"{40113}\", \"{16885}\",\n\t\"{17492}\", \"{5048}\", \"{10276}\", \"{8849}\", \"{23458}\", \"{2041}\", \"{13401}\", \"{17989}\", \"{44180}\", \"{1636}\",\n\t\"{14408}\", \"{9945}\", \"{8552}\", \"{48596}\", \"{48567}\", \"{11566}\", \"{3751}\", \"{22348}\", \"{18602}\", \"{41603}\",\n\t\"{4758}\", \"{651}\", \"{27921}\", \"{15318}\", \"{49286}\", \"{9242}\", \"{20928}\", \"{12311}\", \"{5654}\", \"{45690}\",\n\t\"{49636}\", \"{3817}\", \"{2400}\", \"{6988}\", \"{917}\", \"{40552}\", \"{5409}\", \"{1981}\", \"{6493}\", \"{14049}\",\n\t\"{47840}\", \"{7884}\", \"{54997}\", \"{5912}\", \"{1277}\", \"{19848}\", \"{41242}\", \"{1286}\", \"{210}\", \"{4319}\",\n\t\"{7875}\", \"{48126}\", \"{22709}\", \"{3310}\", \"{1970}\", \"{63936}\", \"{54290}\", \"{5215}\", \"{6979}\", \"{7383}\",\n\t\"{9603}\", \"{43822}\", \"{44902}\", \"{12094}\", \"{19266}\", \"{1859}\", \"{2335}\", \"{38988}\", \"{89}\", \"{6850}\",\n\t\"{1542}\", \"{339}\", \"{40991}\", \"{13775}\", \"{3239}\", \"{8626}\", \"{38493}\", \"{43410}\", \"{46393}\", \"{3425}\",\n\t\"{2832}\", \"{48613}\", \"{525}\", \"{40960}\", \"{13169}\", \"{18576}\", \"{9136}\", \"{78}\", \"{29938}\", \"{39383}\",\n\t\"{12688}\", \"{5520}\", \"{4937}\", \"{66971}\", \"{48955}\", \"{2774}\", \"{9586}\", \"{2199}\", \"{21416}\", \"{69456}\",\n\t\"{12238}\", \"{5190}\", \"{8267}\", \"{3678}\", \"{43051}\", \"{3095}\", \"{778}\", \"{1103}\", \"{195}\", \"{100971}\",\n\t\"{68346}\", \"{164}\", \"{4680}\", \"{789}\", \"{3064}\", \"{57793}\", \"{3689}\", \"{8296}\", \"{5161}\", \"{24578}\",\n\t\"{104913}\", \"{12424}\", \"{2168}\", \"{9577}\", \"{2785}\", \"{42741}\", \"{10272}\", \"{11888}\", \"{42081}\", \"{2045}\",\n\t\"{12509}\", \"{16881}\", \"{17496}\", \"{69367}\", \"{43760}\", \"{9941}\", \"{8556}\", \"{3149}\", \"{13405}\", \"{68486}\",\n\t\"{25559}\", \"{1632}\", \"{18606}\", \"{13219}\", \"{68477}\", \"{655}\", \"{26829}\", \"{11562}\", \"{3755}\", \"{43791}\",\n\t\"{69396}\", \"{12315}\", \"{5650}\", \"{24249}\", \"{27925}\", \"{42070}\", \"{2659}\", \"{9246}\", \"{913}\", \"{12148}\",\n\t\"{41941}\", \"{1985}\", \"{49632}\", \"{3813}\", \"{2404}\", \"{53481}\", \"{54993}\", \"{5916}\", \"{1273}\", \"{408}\",\n\t\"{6497}\", \"{28919}\", \"{3508}\", \"{7880}\", \"{7871}\", \"{48122}\", \"{52391}\", \"{3314}\", \"{13658}\", \"{1282}\",\n\t\"{214}\", \"{45923}\", \"{42431}\", \"{7387}\", \"{9607}\", \"{2218}\", \"{1974}\", \"{63932}\", \"{24608}\", \"{5211}\",\n\t\"{2331}\", \"{15099}\", \"{46890}\", \"{6854}\", \"{5338}\", \"{12090}\", \"{19262}\", \"{18898}\", \"{42803}\", \"{8622}\",\n\t\"{38497}\", \"{7958}\", \"{1546}\", \"{41582}\", \"{40995}\", \"{13771}\", \"{521}\", \"{4428}\", \"{41573}\", \"{18572}\",\n\t\"{14789}\", \"{3421}\", \"{2836}\", \"{48617}\", \"{18869}\", \"{5524}\", \"{4933}\", \"{66975}\", \"{9132}\", \"{46861}\",\n\t\"{15068}\", \"{39387}\", \"{21412}\", \"{5779}\", \"{40622}\", \"{5194}\", \"{23369}\", \"{2770}\", \"{9582}\", \"{49546}\",\n\t\"{50182}\", \"{1107}\", \"{191}\", \"{4098}\", \"{8263}\", \"{9899}\", \"{14339}\", \"{3091}\", \"{3060}\", \"{22479}\",\n\t\"{9868}\", \"{8292}\", \"{4069}\", \"{160}\", \"{4684}\", \"{41132}\", \"{83848}\", \"{9573}\", \"{2781}\", \"{15429}\",\n\t\"{5165}\", \"{51692}\", \"{5788}\", \"{12420}\", \"{48710}\", \"{2931}\", \"{3526}\", \"{8139}\", \"{18475}\", \"{5938}\",\n\t\"{18298}\", \"{426}\", \"{39280}\", \"{42203}\", \"{7358}\", \"{9035}\", \"{19579}\", \"{4834}\", \"{5423}\", \"{21748}\",\n\t\"{17008}\", \"{19365}\", \"{12197}\", \"{19588}\", \"{6953}\", \"{46997}\", \"{9629}\", \"{2236}\", \"{13676}\", \"{18269}\",\n\t\"{20058}\", \"{1441}\", \"{43513}\", \"{38590}\", \"{8725}\", \"{6448}\", \"{16359}\", \"{4783}\", \"{20205}\", \"{68245}\",\n\t\"{8395}\", \"{23969}\", \"{8578}\", \"{3167}\", \"{12527}\", \"{19138}\", \"{21309}\", \"{5062}\", \"{9299}\", \"{2686}\",\n\t\"{9474}\", \"{7719}\", \"{49441}\", \"{9485}\", \"{2677}\", \"{9268}\", \"{5093}\", \"{17449}\", \"{69555}\", \"{21515}\",\n\t\"{3196}\", \"{8589}\", \"{6009}\", \"{8364}\", \"{18628}\", \"{13237}\", \"{1000}\", \"{20419}\", \"{11288}\", \"{3652}\",\n\t\"{11465}\", \"{48464}\", \"{752}\", \"{1129}\", \"{41700}\", \"{18701}\", \"{9341}\", \"{28758}\", \"{10569}\", \"{27822}\",\n\t\"{45793}\", \"{5757}\", \"{12212}\", \"{69291}\", \"{13819}\", \"{17591}\", \"{16986}\", \"{40010}\", \"{2142}\", \"{10598}\",\n\t\"{49374}\", \"{10375}\", \"{1735}\", \"{44083}\", \"{24849}\", \"{13502}\", \"{29048}\", \"{8451}\", \"{9846}\", \"{11279}\",\n\t\"{1568}\", \"{313}\", \"{1385}\", \"{41341}\", \"{3213}\", \"{27178}\", \"{48025}\", \"{7976}\", \"{5316}\", \"{54393}\",\n\t\"{63835}\", \"{1873}\", \"{28319}\", \"{9700}\", \"{7280}\", \"{10128}\", \"{26668}\", \"{52}\", \"{3914}\", \"{49735}\",\n\t\"{1882}\", \"{41846}\", \"{40451}\", \"{814}\", \"{7987}\", \"{29409}\", \"{2818}\", \"{6590}\", \"{55483}\", \"{1374}\",\n\t\"{5811}\", \"{1599}\", \"{18471}\", \"{41470}\", \"{1259}\", \"{422}\", \"{38388}\", \"{2935}\", \"{3522}\", \"{27649}\",\n\t\"{939}\", \"{4830}\", \"{5427}\", \"{40391}\", \"{39284}\", \"{3839}\", \"{28428}\", \"{9031}\", \"{6957}\", \"{39498}\",\n\t\"{26159}\", \"{2232}\", \"{40360}\", \"{19361}\", \"{12193}\", \"{44805}\", \"{11109}\", \"{38594}\", \"{8721}\", \"{29338}\",\n\t\"{13672}\", \"{40896}\", \"{41481}\", \"{1445}\", \"{8391}\", \"{29788}\", \"{27208}\", \"{3163}\", \"{41031}\", \"{4787}\",\n\t\"{20201}\", \"{1618}\", \"{10258}\", \"{2682}\", \"{9470}\", \"{28069}\", \"{12523}\", \"{25868}\", \"{51791}\", \"{5066}\",\n\t\"{5097}\", \"{40721}\", \"{25899}\", \"{21511}\", \"{28098}\", \"{9481}\", \"{2673}\", \"{26518}\", \"{100876}\", \"{13233}\",\n\t\"{1004}\", \"{50081}\", \"{3192}\", \"{11548}\", \"{29779}\", \"{8360}\", \"{756}\", \"{68574}\", \"{16468}\", \"{18705}\",\n\t\"{8249}\", \"{3656}\", \"{11461}\", \"{48460}\", \"{17389}\", \"{5753}\", \"{12216}\", \"{19609}\", \"{9345}\", \"{7028}\",\n\t\"{42173}\", \"{27826}\", \"{2146}\", \"{9559}\", \"{22948}\", \"{10371}\", \"{69264}\", \"{17595}\", \"{16982}\", \"{17378}\",\n\t\"{6738}\", \"{8455}\", \"{9842}\", \"{43663}\", \"{1731}\", \"{16499}\", \"{18119}\", \"{13506}\", \"{3217}\", \"{8608}\",\n\t\"{6388}\", \"{7972}\", \"{45820}\", \"{317}\", \"{1381}\", \"{16029}\", \"{7469}\", \"{9704}\", \"{7284}\", \"{42532}\",\n\t\"{5312}\", \"{21079}\", \"{19248}\", \"{1877}\", \"{1886}\", \"{41842}\", \"{4919}\", \"{810}\", \"{9118}\", \"{56}\",\n\t\"{3910}\", \"{7498}\", \"{20769}\", \"{1370}\", \"{5815}\", \"{18558}\", \"{7983}\", \"{6379}\", \"{43222}\", \"{6594}\",\n\t\"{39288}\", \"{3835}\", \"{2422}\", \"{26749}\", \"{935}\", \"{40570}\", \"{41967}\", \"{12783}\", \"{38384}\", \"{2939}\",\n\t\"{29528}\", \"{8131}\", \"{67976}\", \"{5930}\", \"{1255}\", \"{41291}\", \"{41260}\", \"{18261}\", \"{232}\", \"{1449}\",\n\t\"{7857}\", \"{38598}\", \"{27059}\", \"{3332}\", \"{1952}\", \"{41996}\", \"{40581}\", \"{5237}\", \"{10009}\", \"{39494}\",\n\t\"{9621}\", \"{28238}\", \"{40131}\", \"{5687}\", \"{21301}\", \"{13938}\", \"{9291}\", \"{28688}\", \"{26308}\", \"{2063}\",\n\t\"{682}\", \"{24968}\", \"{50691}\", \"{1614}\", \"{11358}\", \"{3782}\", \"{8570}\", \"{29169}\", \"{29198}\", \"{8581}\",\n\t\"{3773}\", \"{27418}\", \"{4197}\", \"{41621}\", \"{1008}\", \"{673}\", \"{2092}\", \"{10448}\", \"{28679}\", \"{9260}\",\n\t\"{101976}\", \"{12333}\", \"{5676}\", \"{51181}\", \"{9349}\", \"{2756}\", \"{10561}\", \"{49560}\", \"{17385}\", \"{69474}\",\n\t\"{17568}\", \"{19605}\", \"{8245}\", \"{6128}\", \"{43073}\", \"{26926}\", \"{16289}\", \"{1121}\", \"{13316}\", \"{18709}\",\n\t\"{68364}\", \"{146}\", \"{17882}\", \"{16278}\", \"{3046}\", \"{8459}\", \"{23848}\", \"{11271}\", \"{5143}\", \"{17599}\",\n\t\"{19019}\", \"{12406}\", \"{7638}\", \"{9555}\", \"{8942}\", \"{42763}\", \"{44920}\", \"{21075}\", \"{19244}\", \"{17129}\",\n\t\"{2317}\", \"{9708}\", \"{7288}\", \"{6872}\", \"{1560}\", \"{20179}\", \"{18348}\", \"{13757}\", \"{6569}\", \"{8604}\",\n\t\"{6384}\", \"{43432}\", \"{8018}\", \"{3407}\", \"{2810}\", \"{6598}\", \"{507}\", \"{40942}\", \"{5819}\", \"{1591}\",\n\t\"{6883}\", \"{7279}\", \"{42322}\", \"{7494}\", \"{21669}\", \"{5502}\", \"{4915}\", \"{19458}\", \"{931}\", \"{4838}\",\n\t\"{19398}\", \"{12787}\", \"{49610}\", \"{3831}\", \"{2426}\", \"{9039}\", \"{18479}\", \"{5934}\", \"{1251}\", \"{20648}\",\n\t\"{38380}\", \"{43303}\", \"{6258}\", \"{8135}\", \"{7853}\", \"{47897}\", \"{8729}\", \"{3336}\", \"{16108}\", \"{18265}\",\n\t\"{236}\", \"{18488}\", \"{42413}\", \"{39490}\", \"{9625}\", \"{7548}\", \"{1956}\", \"{19369}\", \"{21158}\", \"{5233}\",\n\t\"{9295}\", \"{22869}\", \"{9478}\", \"{2067}\", \"{17259}\", \"{5683}\", \"{21305}\", \"{69345}\", \"{8399}\", \"{3786}\",\n\t\"{8574}\", \"{6619}\", \"{686}\", \"{18038}\", \"{20209}\", \"{1610}\", \"{4193}\", \"{16549}\", \"{68455}\", \"{677}\",\n\t\"{48541}\", \"{8585}\", \"{3777}\", \"{8368}\", \"{19728}\", \"{12337}\", \"{5672}\", \"{21519}\", \"{2096}\", \"{9489}\",\n\t\"{7109}\", \"{9264}\", \"{17381}\", \"{69470}\", \"{40600}\", \"{19601}\", \"{10388}\", \"{2752}\", \"{10565}\", \"{49564}\",\n\t\"{44693}\", \"{1125}\", \"{13312}\", \"{68391}\", \"{8241}\", \"{29658}\", \"{11469}\", \"{26922}\", \"{3042}\", \"{11498}\",\n\t\"{48274}\", \"{11275}\", \"{1739}\", \"{142}\", \"{17886}\", \"{41110}\", \"{28148}\", \"{9551}\", \"{8946}\", \"{10379}\",\n\t\"{5147}\", \"{45183}\", \"{25949}\", \"{12402}\", \"{2313}\", \"{26078}\", \"{49125}\", \"{6876}\", \"{44924}\", \"{21071}\",\n\t\"{19240}\", \"{40241}\", \"{29219}\", \"{8600}\", \"{6380}\", \"{11028}\", \"{1564}\", \"{55293}\", \"{1389}\", \"{13753}\",\n\t\"{503}\", \"{1378}\", \"{41551}\", \"{1595}\", \"{27768}\", \"{3403}\", \"{2814}\", \"{48635}\", \"{54583}\", \"{5506}\",\n\t\"{4911}\", \"{818}\", \"{6887}\", \"{28509}\", \"{3918}\", \"{7490}\", \"{17891}\", \"{13519}\", \"{68377}\", \"{155}\",\n\t\"{10898}\", \"{11262}\", \"{3055}\", \"{43091}\", \"{69496}\", \"{12415}\", \"{5150}\", \"{24549}\", \"{8951}\", \"{42770}\",\n\t\"{2159}\", \"{9546}\", \"{10572}\", \"{27839}\", \"{42781}\", \"{2745}\", \"{12209}\", \"{19616}\", \"{17396}\", \"{69467}\",\n\t\"{43060}\", \"{26935}\", \"{8256}\", \"{3649}\", \"{13305}\", \"{68386}\", \"{749}\", \"{1132}\", \"{2803}\", \"{48622}\",\n\t\"{52491}\", \"{3414}\", \"{13158}\", \"{1582}\", \"{514}\", \"{40951}\", \"{29909}\", \"{7487}\", \"{6890}\", \"{49}\",\n\t\"{4906}\", \"{55983}\", \"{1899}\", \"{5511}\", \"{19257}\", \"{1868}\", \"{44933}\", \"{21066}\", \"{49132}\", \"{6861}\",\n\t\"{2304}\", \"{53381}\", \"{62922}\", \"{13744}\", \"{1573}\", \"{308}\", \"{6397}\", \"{43421}\", \"{3208}\", \"{8617}\",\n\t\"{221}\", \"{4328}\", \"{19888}\", \"{18272}\", \"{14089}\", \"{3321}\", \"{7844}\", \"{47880}\", \"{40592}\", \"{5224}\",\n\t\"{1941}\", \"{41985}\", \"{9632}\", \"{43813}\", \"{6948}\", \"{39487}\", \"{2431}\", \"{15799}\", \"{49607}\", \"{3826}\",\n\t\"{5438}\", \"{12790}\", \"{926}\", \"{40563}\", \"{47871}\", \"{8122}\", \"{38397}\", \"{14078}\", \"{1246}\", \"{19879}\",\n\t\"{67965}\", \"{5923}\", \"{3760}\", \"{22379}\", \"{48556}\", \"{8592}\", \"{4769}\", \"{660}\", \"{4184}\", \"{41632}\",\n\t\"{8889}\", \"{9273}\", \"{2081}\", \"{15329}\", \"{5665}\", \"{51192}\", \"{5088}\", \"{12320}\", \"{21312}\", \"{5079}\",\n\t\"{40122}\", \"{5694}\", \"{23469}\", \"{2070}\", \"{9282}\", \"{8878}\", \"{50682}\", \"{1607}\", \"{691}\", \"{4798}\",\n\t\"{8563}\", \"{82858}\", \"{14439}\", \"{3791}\", \"{9859}\", \"{11266}\", \"{3051}\", \"{22448}\", \"{17895}\", \"{41103}\",\n\t\"{4058}\", \"{151}\", \"{8955}\", \"{15418}\", \"{49586}\", \"{9542}\", \"{16999}\", \"{12411}\", \"{5154}\", \"{45190}\",\n\t\"{40613}\", \"{19612}\", \"{17392}\", \"{5748}\", \"{10576}\", \"{49577}\", \"{23358}\", \"{2741}\", \"{13301}\", \"{21938}\",\n\t\"{44680}\", \"{1136}\", \"{14308}\", \"{26931}\", \"{8252}\", \"{48296}\", \"{41542}\", \"{1586}\", \"{510}\", \"{4419}\",\n\t\"{2807}\", \"{48626}\", \"{7998}\", \"{3410}\", \"{4902}\", \"{55987}\", \"{18858}\", \"{5515}\", \"{15059}\", \"{7483}\",\n\t\"{6894}\", \"{46850}\", \"{49136}\", \"{6865}\", \"{2300}\", \"{23719}\", \"{19253}\", \"{40252}\", \"{5309}\", \"{21062}\",\n\t\"{6393}\", \"{7969}\", \"{42832}\", \"{8613}\", \"{62926}\", \"{13740}\", \"{1577}\", \"{55280}\", \"{39998}\", \"{3325}\",\n\t\"{7840}\", \"{47884}\", \"{225}\", \"{45912}\", \"{13669}\", \"{18276}\", \"{9636}\", \"{2229}\", \"{42400}\", \"{39483}\",\n\t\"{12188}\", \"{5220}\", \"{1945}\", \"{41981}\", \"{41970}\", \"{12794}\", \"{922}\", \"{12179}\", \"{2435}\", \"{47383}\",\n\t\"{49603}\", \"{3822}\", \"{1242}\", \"{439}\", \"{67961}\", \"{5927}\", \"{3539}\", \"{8126}\", \"{38393}\", \"{28928}\",\n\t\"{68446}\", \"{664}\", \"{4180}\", \"{13228}\", \"{3764}\", \"{49945}\", \"{3189}\", \"{8596}\", \"{5661}\", \"{24278}\",\n\t\"{101961}\", \"{12324}\", \"{2668}\", \"{9277}\", \"{2085}\", \"{42041}\", \"{56783}\", \"{2074}\", \"{9286}\", \"{2699}\",\n\t\"{21316}\", \"{69356}\", \"{12538}\", \"{5690}\", \"{8567}\", \"{3178}\", \"{43751}\", \"{3795}\", \"{25568}\", \"{1603}\",\n\t\"{695}\", \"{105903}\", \"{16995}\", \"{40003}\", \"{5158}\", \"{17582}\", \"{8959}\", \"{10366}\", \"{2151}\", \"{23548}\",\n\t\"{17899}\", \"{13511}\", \"{1726}\", \"{44090}\", \"{9855}\", \"{14518}\", \"{48486}\", \"{8442}\", \"{11476}\", \"{48477}\",\n\t\"{22258}\", \"{3641}\", \"{41713}\", \"{18712}\", \"{741}\", \"{4648}\", \"{15208}\", \"{27831}\", \"{9352}\", \"{49396}\",\n\t\"{12201}\", \"{20838}\", \"{45780}\", \"{5744}\", \"{3907}\", \"{49726}\", \"{6898}\", \"{41}\", \"{40442}\", \"{807}\",\n\t\"{1891}\", \"{5519}\", \"{14159}\", \"{6583}\", \"{7994}\", \"{47950}\", \"{5802}\", \"{54887}\", \"{19958}\", \"{1367}\",\n\t\"{1396}\", \"{41352}\", \"{4209}\", \"{300}\", \"{48036}\", \"{7965}\", \"{3200}\", \"{22619}\", \"{63826}\", \"{1860}\",\n\t\"{5305}\", \"{54380}\", \"{7293}\", \"{6869}\", \"{43932}\", \"{9713}\", \"{12184}\", \"{44812}\", \"{1949}\", \"{19376}\",\n\t\"{38898}\", \"{2225}\", \"{6940}\", \"{46984}\", \"{229}\", \"{1452}\", \"{13665}\", \"{40881}\", \"{8736}\", \"{3329}\",\n\t\"{43500}\", \"{38583}\", \"{3535}\", \"{46283}\", \"{48703}\", \"{2922}\", \"{40870}\", \"{435}\", \"{18466}\", \"{13079}\",\n\t\"{2439}\", \"{9026}\", \"{39293}\", \"{29828}\", \"{5430}\", \"{12798}\", \"{66861}\", \"{4827}\", \"{2664}\", \"{48845}\",\n\t\"{2089}\", \"{9496}\", \"{69546}\", \"{21506}\", \"{5080}\", \"{12328}\", \"{3768}\", \"{8377}\", \"{3185}\", \"{43141}\",\n\t\"{1013}\", \"{668}\", \"{100861}\", \"{13224}\", \"{20216}\", \"{68256}\", \"{699}\", \"{4790}\", \"{57683}\", \"{3174}\",\n\t\"{8386}\", \"{3799}\", \"{24468}\", \"{5071}\", \"{12534}\", \"{104803}\", \"{9467}\", \"{2078}\", \"{42651}\", \"{2695}\",\n\t\"{11998}\", \"{10362}\", \"{2155}\", \"{42191}\", \"{16991}\", \"{12419}\", \"{69277}\", \"{17586}\", \"{9851}\", \"{43670}\",\n\t\"{3059}\", \"{8446}\", \"{68596}\", \"{13515}\", \"{1722}\", \"{159}\", \"{13309}\", \"{18716}\", \"{745}\", \"{68567}\",\n\t\"{11472}\", \"{26939}\", \"{43681}\", \"{3645}\", \"{12205}\", \"{69286}\", \"{24359}\", \"{5740}\", \"{42160}\", \"{27835}\",\n\t\"{9356}\", \"{2749}\", \"{12058}\", \"{803}\", \"{1895}\", \"{41851}\", \"{3903}\", \"{49722}\", \"{53591}\", \"{45}\",\n\t\"{5806}\", \"{54883}\", \"{518}\", \"{1363}\", \"{28809}\", \"{6587}\", \"{7990}\", \"{3418}\", \"{48032}\", \"{7961}\",\n\t\"{3204}\", \"{52281}\", \"{1392}\", \"{13748}\", \"{45833}\", \"{304}\", \"{7297}\", \"{42521}\", \"{2308}\", \"{9717}\",\n\t\"{63822}\", \"{1864}\", \"{5301}\", \"{24718}\", \"{15189}\", \"{2221}\", \"{6944}\", \"{46980}\", \"{12180}\", \"{5228}\",\n\t\"{18988}\", \"{19372}\", \"{8732}\", \"{42913}\", \"{7848}\", \"{38587}\", \"{41492}\", \"{1456}\", \"{13661}\", \"{40885}\",\n\t\"{4538}\", \"{431}\", \"{18462}\", \"{41463}\", \"{3531}\", \"{14699}\", \"{48707}\", \"{2926}\", \"{5434}\", \"{18979}\",\n\t\"{66865}\", \"{4823}\", \"{46971}\", \"{9022}\", \"{39297}\", \"{15178}\", \"{5669}\", \"{21502}\", \"{5084}\", \"{40732}\",\n\t\"{2660}\", \"{23279}\", \"{49456}\", \"{9492}\", \"{1017}\", \"{50092}\", \"{4188}\", \"{13220}\", \"{9989}\", \"{8373}\",\n\t\"{3181}\", \"{14229}\", \"{22569}\", \"{3170}\", \"{8382}\", \"{9978}\", \"{20212}\", \"{4179}\", \"{41022}\", \"{4794}\",\n\t\"{9463}\", \"{83958}\", \"{15539}\", \"{2691}\", \"{51782}\", \"{5075}\", \"{12530}\", \"{5698}\", \"{356}\", \"{45861}\",\n\t\"{16068}\", \"{18305}\", \"{8649}\", \"{3256}\", \"{7933}\", \"{48060}\", \"{17789}\", \"{5353}\", \"{1836}\", \"{19209}\",\n\t\"{9745}\", \"{7428}\", \"{42573}\", \"{64879}\", \"{17}\", \"{9159}\", \"{43995}\", \"{3951}\", \"{41803}\", \"{17195}\",\n\t\"{851}\", \"{4958}\", \"{6338}\", \"{8055}\", \"{42899}\", \"{43263}\", \"{1331}\", \"{16099}\", \"{18519}\", \"{5854}\",\n\t\"{3617}\", \"{8208}\", \"{6788}\", \"{11420}\", \"{68535}\", \"{717}\", \"{1781}\", \"{16429}\", \"{7069}\", \"{9304}\",\n\t\"{7684}\", \"{42132}\", \"{5712}\", \"{21479}\", \"{19648}\", \"{12257}\", \"{21265}\", \"{69225}\", \"{17339}\", \"{19054}\",\n\t\"{9518}\", \"{2107}\", \"{10330}\", \"{7098}\", \"{20369}\", \"{1770}\", \"{13547}\", \"{18158}\", \"{8414}\", \"{6779}\",\n\t\"{43622}\", \"{6194}\", \"{18071}\", \"{41070}\", \"{1659}\", \"{13283}\", \"{38788}\", \"{11315}\", \"{3122}\", \"{27249}\",\n\t\"{25829}\", \"{12562}\", \"{5027}\", \"{40791}\", \"{8826}\", \"{10219}\", \"{28028}\", \"{9431}\", \"{10405}\", \"{39098}\",\n\t\"{26559}\", \"{2632}\", \"{40760}\", \"{13984}\", \"{12593}\", \"{34899}\", \"{11509}\", \"{15881}\", \"{8321}\", \"{29738}\",\n\t\"{13272}\", \"{12888}\", \"{41081}\", \"{1045}\", \"{2974}\", \"{29388}\", \"{27608}\", \"{3563}\", \"{41431}\", \"{4387}\",\n\t\"{463}\", \"{1218}\", \"{3878}\", \"{2282}\", \"{9070}\", \"{28469}\", \"{4871}\", \"{978}\", \"{51391}\", \"{5466}\",\n\t\"{5497}\", \"{40321}\", \"{989}\", \"{4880}\", \"{28498}\", \"{6916}\", \"{2273}\", \"{3889}\", \"{51896}\", \"{492}\",\n\t\"{1404}\", \"{50481}\", \"{3592}\", \"{11148}\", \"{29379}\", \"{2985}\", \"{11688}\", \"{3252}\", \"{7937}\", \"{48064}\",\n\t\"{352}\", \"{1529}\", \"{41300}\", \"{18301}\", \"{9741}\", \"{28358}\", \"{10169}\", \"{49168}\", \"{45393}\", \"{5357}\",\n\t\"{1832}\", \"{63874}\", \"{41807}\", \"{17191}\", \"{855}\", \"{40410}\", \"{13}\", \"{10198}\", \"{43991}\", \"{3955}\",\n\t\"{1335}\", \"{44483}\", \"{45894}\", \"{5850}\", \"{29448}\", \"{8051}\", \"{48678}\", \"{2859}\", \"{1168}\", \"{713}\",\n\t\"{1785}\", \"{41741}\", \"{3613}\", \"{27578}\", \"{48425}\", \"{11424}\", \"{5716}\", \"{54793}\", \"{78291}\", \"{12253}\",\n\t\"{28719}\", \"{9300}\", \"{7680}\", \"{10528}\", \"{26268}\", \"{2103}\", \"{10334}\", \"{49335}\", \"{21261}\", \"{13858}\",\n\t\"{40051}\", \"{19050}\", \"{8410}\", \"{29009}\", \"{11238}\", \"{6190}\", \"{55083}\", \"{1774}\", \"{13543}\", \"{1199}\",\n\t\"{23928}\", \"{11311}\", \"{3126}\", \"{8539}\", \"{18075}\", \"{16318}\", \"{18698}\", \"{13287}\", \"{8822}\", \"{42603}\",\n\t\"{7758}\", \"{9435}\", \"{19179}\", \"{12566}\", \"{5023}\", \"{21348}\", \"{17408}\", \"{13980}\", \"{12597}\", \"{19188}\",\n\t\"{10401}\", \"{14989}\", \"{9229}\", \"{2636}\", \"{13276}\", \"{18669}\", \"{20458}\", \"{1041}\", \"{43113}\", \"{15885}\",\n\t\"{8325}\", \"{6048}\", \"{5979}\", \"{4383}\", \"{467}\", \"{40822}\", \"{2970}\", \"{48751}\", \"{8178}\", \"{3567}\",\n\t\"{4875}\", \"{19538}\", \"{21709}\", \"{5462}\", \"{9699}\", \"{2286}\", \"{9074}\", \"{7319}\", \"{49041}\", \"{6912}\",\n\t\"{2277}\", \"{9668}\", \"{5493}\", \"{17049}\", \"{44840}\", \"{4884}\", \"{3596}\", \"{8189}\", \"{6409}\", \"{2981}\",\n\t\"{18228}\", \"{496}\", \"{1400}\", \"{5988}\", \"{17781}\", \"{44965}\", \"{40200}\", \"{19201}\", \"{10788}\", \"{2352}\",\n\t\"{6837}\", \"{49164}\", \"{44293}\", \"{1525}\", \"{13712}\", \"{62974}\", \"{8641}\", \"{29258}\", \"{11069}\", \"{48068}\",\n\t\"{3442}\", \"{11098}\", \"{42891}\", \"{2855}\", \"{1339}\", \"{542}\", \"{18511}\", \"{41510}\", \"{28548}\", \"{9151}\",\n\t\"{49778}\", \"{3959}\", \"{5547}\", \"{45583}\", \"{859}\", \"{4950}\", \"{2713}\", \"{26478}\", \"{49525}\", \"{10524}\",\n\t\"{69431}\", \"{21471}\", \"{19640}\", \"{40641}\", \"{29619}\", \"{8200}\", \"{6780}\", \"{11428}\", \"{1164}\", \"{55693}\",\n\t\"{1789}\", \"{13353}\", \"{103}\", \"{1778}\", \"{41151}\", \"{1195}\", \"{27368}\", \"{3003}\", \"{11234}\", \"{48235}\",\n\t\"{54183}\", \"{5106}\", \"{12443}\", \"{25908}\", \"{9510}\", \"{28109}\", \"{10338}\", \"{7090}\", \"{19175}\", \"{17218}\",\n\t\"{19798}\", \"{12387}\", \"{22828}\", \"{10211}\", \"{2026}\", \"{9439}\", \"{18079}\", \"{13466}\", \"{1651}\", \"{20248}\",\n\t\"{9922}\", \"{43703}\", \"{6658}\", \"{8535}\", \"{11501}\", \"{15889}\", \"{8329}\", \"{3736}\", \"{16508}\", \"{12880}\",\n\t\"{636}\", \"{18088}\", \"{42013}\", \"{14985}\", \"{9225}\", \"{7148}\", \"{12376}\", \"{19769}\", \"{21558}\", \"{5633}\",\n\t\"{3870}\", \"{49651}\", \"{9078}\", \"{2467}\", \"{4879}\", \"{970}\", \"{18923}\", \"{41922}\", \"{8799}\", \"{3386}\",\n\t\"{8174}\", \"{6219}\", \"{286}\", \"{18438}\", \"{20609}\", \"{1210}\", \"{4593}\", \"{16149}\", \"{45940}\", \"{277}\",\n\t\"{48141}\", \"{7812}\", \"{3377}\", \"{8768}\", \"{19328}\", \"{1917}\", \"{981}\", \"{4888}\", \"{2496}\", \"{9089}\",\n\t\"{7509}\", \"{3881}\", \"{9749}\", \"{2356}\", \"{6833}\", \"{49160}\", \"{17785}\", \"{44961}\", \"{17168}\", \"{19205}\",\n\t\"{8645}\", \"{6528}\", \"{43473}\", \"{65979}\", \"{16689}\", \"{1521}\", \"{13716}\", \"{18309}\", \"{40903}\", \"{546}\",\n\t\"{18515}\", \"{5858}\", \"{3446}\", \"{8059}\", \"{42895}\", \"{2851}\", \"{5543}\", \"{17199}\", \"{19419}\", \"{4954}\",\n\t\"{7238}\", \"{9155}\", \"{43999}\", \"{42363}\", \"{69435}\", \"{21475}\", \"{19644}\", \"{17529}\", \"{2717}\", \"{9308}\",\n\t\"{7688}\", \"{10520}\", \"{1160}\", \"{20579}\", \"{18748}\", \"{13357}\", \"{6169}\", \"{8204}\", \"{6784}\", \"{43032}\",\n\t\"{8418}\", \"{3007}\", \"{11230}\", \"{6198}\", \"{107}\", \"{68325}\", \"{16239}\", \"{1191}\", \"{9514}\", \"{7679}\",\n\t\"{42722}\", \"{7094}\", \"{21269}\", \"{5102}\", \"{12447}\", \"{19058}\", \"{39688}\", \"{10215}\", \"{2022}\", \"{26349}\",\n\t\"{19171}\", \"{40170}\", \"{13979}\", \"{12383}\", \"{9926}\", \"{11319}\", \"{29128}\", \"{8531}\", \"{24929}\", \"{13462}\",\n\t\"{1655}\", \"{41691}\", \"{41660}\", \"{12884}\", \"{632}\", \"{1049}\", \"{11505}\", \"{38198}\", \"{27459}\", \"{3732}\",\n\t\"{12372}\", \"{13988}\", \"{40181}\", \"{5637}\", \"{10409}\", \"{14981}\", \"{9221}\", \"{28638}\", \"{40531}\", \"{974}\",\n\t\"{18927}\", \"{41926}\", \"{3874}\", \"{28288}\", \"{26708}\", \"{2463}\", \"{282}\", \"{67937}\", \"{50291}\", \"{1214}\",\n\t\"{2978}\", \"{3382}\", \"{8170}\", \"{29569}\", \"{29598}\", \"{7816}\", \"{3373}\", \"{2989}\", \"{4597}\", \"{41221}\",\n\t\"{1408}\", \"{273}\", \"{2492}\", \"{10048}\", \"{28279}\", \"{3885}\", \"{50996}\", \"{1913}\", \"{985}\", \"{51581}\",\n\t\"{46093}\", \"{3725}\", \"{11512}\", \"{26859}\", \"{625}\", \"{68407}\", \"{13269}\", \"{12893}\", \"{9236}\", \"{2629}\",\n\t\"{42000}\", \"{14996}\", \"{12588}\", \"{5620}\", \"{12365}\", \"{101920}\", \"{69317}\", \"{12394}\", \"{19166}\", \"{12579}\",\n\t\"{2035}\", \"{47783}\", \"{49203}\", \"{10202}\", \"{1642}\", \"{13298}\", \"{105942}\", \"{13475}\", \"{3139}\", \"{8526}\",\n\t\"{9931}\", \"{43710}\", \"{45953}\", \"{264}\", \"{4580}\", \"{489}\", \"{3364}\", \"{28998}\", \"{3589}\", \"{7801}\",\n\t\"{992}\", \"{24678}\", \"{50981}\", \"{1904}\", \"{2268}\", \"{3892}\", \"{2485}\", \"{42441}\", \"{56383}\", \"{2474}\",\n\t\"{3863}\", \"{2299}\", \"{18930}\", \"{41931}\", \"{12138}\", \"{963}\", \"{8167}\", \"{3578}\", \"{28969}\", \"{3395}\",\n\t\"{478}\", \"{1203}\", \"{295}\", \"{67920}\", \"{42882}\", \"{2846}\", \"{3451}\", \"{22048}\", \"{18502}\", \"{41503}\",\n\t\"{4458}\", \"{551}\", \"{89471}\", \"{15018}\", \"{46811}\", \"{9142}\", \"{44987}\", \"{4943}\", \"{5554}\", \"{18819}\",\n\t\"{40213}\", \"{19212}\", \"{17792}\", \"{5348}\", \"{6824}\", \"{49177}\", \"{23758}\", \"{2341}\", \"{13701}\", \"{62967}\",\n\t\"{44280}\", \"{1536}\", \"{7928}\", \"{88361}\", \"{8652}\", \"{42873}\", \"{41142}\", \"{1186}\", \"{110}\", \"{4019}\",\n\t\"{11227}\", \"{9818}\", \"{22409}\", \"{3010}\", \"{12450}\", \"{78492}\", \"{54190}\", \"{5115}\", \"{15459}\", \"{7083}\",\n\t\"{9503}\", \"{58586}\", \"{49536}\", \"{10537}\", \"{2700}\", \"{23319}\", \"{19653}\", \"{40652}\", \"{5709}\", \"{21462}\",\n\t\"{6793}\", \"{14349}\", \"{59296}\", \"{8213}\", \"{21979}\", \"{13340}\", \"{1177}\", \"{55680}\", \"{621}\", \"{4728}\",\n\t\"{41673}\", \"{12897}\", \"{14489}\", \"{3721}\", \"{11516}\", \"{48517}\", \"{40192}\", \"{5624}\", \"{12361}\", \"{20958}\",\n\t\"{9232}\", \"{109657}\", \"{15368}\", \"{14992}\", \"{2031}\", \"{15399}\", \"{8839}\", \"{10206}\", \"{5038}\", \"{12390}\",\n\t\"{19162}\", \"{40163}\", \"{82819}\", \"{8522}\", \"{9935}\", \"{14478}\", \"{1646}\", \"{41682}\", \"{105946}\", \"{13471}\",\n\t\"{3360}\", \"{22779}\", \"{48156}\", \"{7805}\", \"{4369}\", \"{260}\", \"{4584}\", \"{41232}\", \"{43852}\", \"{3896}\",\n\t\"{2481}\", \"{6909}\", \"{996}\", \"{51592}\", \"{5488}\", \"{1900}\", \"{18934}\", \"{5479}\", \"{40522}\", \"{967}\",\n\t\"{23069}\", \"{2470}\", \"{3867}\", \"{49646}\", \"{19838}\", \"{1207}\", \"{291}\", \"{4398}\", \"{8163}\", \"{47830}\",\n\t\"{14039}\", \"{3391}\", \"{18506}\", \"{13119}\", \"{40910}\", \"{555}\", \"{42886}\", \"{2842}\", \"{3455}\", \"{43491}\",\n\t\"{44983}\", \"{4947}\", \"{5550}\", \"{24149}\", \"{61908}\", \"{29948}\", \"{2559}\", \"{9146}\", \"{6820}\", \"{49173}\",\n\t\"{42381}\", \"{2345}\", \"{1829}\", \"{19216}\", \"{17796}\", \"{44972}\", \"{43460}\", \"{88365}\", \"{8656}\", \"{3249}\",\n\t\"{13705}\", \"{62963}\", \"{349}\", \"{1532}\", \"{11223}\", \"{48222}\", \"{52091}\", \"{3014}\", \"{13558}\", \"{1182}\",\n\t\"{114}\", \"{68336}\", \"{42731}\", \"{7087}\", \"{9507}\", \"{2118}\", \"{12454}\", \"{78496}\", \"{24508}\", \"{5111}\",\n\t\"{19657}\", \"{12248}\", \"{69426}\", \"{21466}\", \"{27878}\", \"{10533}\", \"{2704}\", \"{48925}\", \"{79386}\", \"{13344}\",\n\t\"{1173}\", \"{708}\", \"{6797}\", \"{43021}\", \"{3608}\", \"{8217}\", \"{15589}\", \"{2621}\", \"{10416}\", \"{49417}\",\n\t\"{12580}\", \"{5628}\", \"{40773}\", \"{13997}\", \"{8332}\", \"{108757}\", \"{14268}\", \"{15892}\", \"{41092}\", \"{1056}\",\n\t\"{13261}\", \"{21858}\", \"{4138}\", \"{13290}\", \"{18062}\", \"{41063}\", \"{3131}\", \"{14299}\", \"{9939}\", \"{11306}\",\n\t\"{5034}\", \"{40782}\", \"{104846}\", \"{12571}\", \"{83919}\", \"{9422}\", \"{8835}\", \"{15578}\", \"{5269}\", \"{4893}\",\n\t\"{5484}\", \"{40332}\", \"{2260}\", \"{23679}\", \"{49056}\", \"{6905}\", \"{1417}\", \"{50492}\", \"{4588}\", \"{481}\",\n\t\"{42952}\", \"{2996}\", \"{3581}\", \"{7809}\", \"{22169}\", \"{3570}\", \"{2967}\", \"{48746}\", \"{470}\", \"{4579}\",\n\t\"{41422}\", \"{4394}\", \"{9063}\", \"{46930}\", \"{15139}\", \"{2291}\", \"{18938}\", \"{5475}\", \"{4862}\", \"{5298}\",\n\t\"{43986}\", \"{3942}\", \"{2555}\", \"{42591}\", \"{842}\", \"{12019}\", \"{41810}\", \"{17186}\", \"{60808}\", \"{28848}\",\n\t\"{3459}\", \"{8046}\", \"{45883}\", \"{5847}\", \"{1322}\", \"{559}\", \"{13709}\", \"{18316}\", \"{345}\", \"{45872}\",\n\t\"{7920}\", \"{48073}\", \"{43281}\", \"{3245}\", \"{1825}\", \"{63863}\", \"{24759}\", \"{5340}\", \"{42560}\", \"{89265}\",\n\t\"{9756}\", \"{2349}\", \"{12458}\", \"{19047}\", \"{21276}\", \"{69236}\", \"{10323}\", \"{49322}\", \"{53191}\", \"{2114}\",\n\t\"{13554}\", \"{79596}\", \"{118}\", \"{1763}\", \"{43631}\", \"{6187}\", \"{8407}\", \"{3018}\", \"{26978}\", \"{11433}\",\n\t\"{3604}\", \"{49825}\", \"{1792}\", \"{13348}\", \"{68526}\", \"{704}\", \"{7697}\", \"{42121}\", \"{2708}\", \"{9317}\",\n\t\"{78286}\", \"{12244}\", \"{5701}\", \"{24318}\", \"{12584}\", \"{69507}\", \"{12369}\", \"{13993}\", \"{47193}\", \"{2625}\",\n\t\"{10412}\", \"{27959}\", \"{629}\", \"{1052}\", \"{13265}\", \"{100820}\", \"{8336}\", \"{3729}\", \"{43100}\", \"{15896}\",\n\t\"{3135}\", \"{46683}\", \"{48303}\", \"{11302}\", \"{68217}\", \"{13294}\", \"{18066}\", \"{13479}\", \"{2039}\", \"{9426}\",\n\t\"{8831}\", \"{42610}\", \"{5030}\", \"{12398}\", \"{104842}\", \"{12575}\", \"{2264}\", \"{29898}\", \"{2489}\", \"{6901}\",\n\t\"{44853}\", \"{4897}\", \"{5480}\", \"{1908}\", \"{3368}\", \"{2992}\", \"{3585}\", \"{43541}\", \"{1413}\", \"{268}\",\n\t\"{51881}\", \"{485}\", \"{474}\", \"{40831}\", \"{299}\", \"{4390}\", \"{57283}\", \"{3574}\", \"{2963}\", \"{3399}\",\n\t\"{24068}\", \"{5471}\", \"{4866}\", \"{66820}\", \"{9067}\", \"{2478}\", \"{29869}\", \"{2295}\", \"{846}\", \"{40403}\",\n\t\"{5558}\", \"{17182}\", \"{43982}\", \"{3946}\", \"{2551}\", \"{23148}\", \"{45887}\", \"{5843}\", \"{1326}\", \"{19919}\",\n\t\"{88571}\", \"{14118}\", \"{47911}\", \"{8042}\", \"{7924}\", \"{48077}\", \"{22658}\", \"{3241}\", \"{41313}\", \"{18312}\",\n\t\"{341}\", \"{4248}\", \"{6828}\", \"{89261}\", \"{9752}\", \"{43973}\", \"{1821}\", \"{63867}\", \"{45380}\", \"{5344}\",\n\t\"{10327}\", \"{8918}\", \"{23509}\", \"{2110}\", \"{40042}\", \"{19043}\", \"{21272}\", \"{5119}\", \"{14559}\", \"{6183}\",\n\t\"{8403}\", \"{59486}\", \"{13550}\", \"{79592}\", \"{55090}\", \"{1767}\", \"{1796}\", \"{41752}\", \"{4609}\", \"{700}\",\n\t\"{48436}\", \"{11437}\", \"{3600}\", \"{22219}\", \"{20879}\", \"{12240}\", \"{5705}\", \"{54780}\", \"{7693}\", \"{15249}\",\n\t\"{58396}\", \"{9313}\", \"{45971}\", \"{246}\", \"{18215}\", \"{16178}\", \"{3346}\", \"{8759}\", \"{48170}\", \"{7823}\",\n\t\"{5243}\", \"{17699}\", \"{19319}\", \"{1926}\", \"{7538}\", \"{9655}\", \"{64969}\", \"{42463}\", \"{9049}\", \"{2456}\",\n\t\"{3841}\", \"{43885}\", \"{17085}\", \"{41913}\", \"{4848}\", \"{941}\", \"{8145}\", \"{6228}\", \"{43373}\", \"{42989}\",\n\t\"{16189}\", \"{1221}\", \"{5944}\", \"{18409}\", \"{8318}\", \"{3707}\", \"{11530}\", \"{6698}\", \"{607}\", \"{68425}\",\n\t\"{16539}\", \"{1691}\", \"{9214}\", \"{7179}\", \"{42022}\", \"{7794}\", \"{21569}\", \"{5602}\", \"{12347}\", \"{19758}\",\n\t\"{69335}\", \"{21375}\", \"{19144}\", \"{17229}\", \"{2017}\", \"{9408}\", \"{7188}\", \"{10220}\", \"{1660}\", \"{20279}\",\n\t\"{18048}\", \"{13457}\", \"{6669}\", \"{8504}\", \"{6084}\", \"{43732}\", \"{41160}\", \"{18161}\", \"{132}\", \"{1749}\",\n\t\"{11205}\", \"{38698}\", \"{27359}\", \"{3032}\", \"{12472}\", \"{25939}\", \"{40681}\", \"{5137}\", \"{10309}\", \"{8936}\",\n\t\"{9521}\", \"{28138}\", \"{39188}\", \"{10515}\", \"{2722}\", \"{26449}\", \"{13894}\", \"{40670}\", \"{34989}\", \"{12483}\",\n\t\"{15991}\", \"{11419}\", \"{29628}\", \"{8231}\", \"{12998}\", \"{13362}\", \"{1155}\", \"{41191}\", \"{29298}\", \"{2864}\",\n\t\"{3473}\", \"{27718}\", \"{4297}\", \"{41521}\", \"{1308}\", \"{573}\", \"{2392}\", \"{3968}\", \"{28579}\", \"{9160}\",\n\t\"{868}\", \"{4961}\", \"{5576}\", \"{51281}\", \"{40231}\", \"{5587}\", \"{4990}\", \"{899}\", \"{6806}\", \"{28588}\",\n\t\"{3999}\", \"{2363}\", \"{582}\", \"{51986}\", \"{50591}\", \"{1514}\", \"{11058}\", \"{3482}\", \"{2895}\", \"{29269}\",\n\t\"{3342}\", \"{11798}\", \"{48174}\", \"{7827}\", \"{1439}\", \"{242}\", \"{18211}\", \"{41210}\", \"{28248}\", \"{9651}\",\n\t\"{49078}\", \"{10079}\", \"{5247}\", \"{45283}\", \"{63964}\", \"{1922}\", \"{17081}\", \"{41917}\", \"{40500}\", \"{945}\",\n\t\"{10088}\", \"{2452}\", \"{3845}\", \"{43881}\", \"{44593}\", \"{1225}\", \"{5940}\", \"{45984}\", \"{8141}\", \"{29558}\",\n\t\"{2949}\", \"{48768}\", \"{603}\", \"{1078}\", \"{41651}\", \"{1695}\", \"{27468}\", \"{3703}\", \"{11534}\", \"{48535}\",\n\t\"{54683}\", \"{5606}\", \"{12343}\", \"{78381}\", \"{9210}\", \"{28609}\", \"{10438}\", \"{7790}\", \"{2013}\", \"{26378}\",\n\t\"{49225}\", \"{10224}\", \"{13948}\", \"{21371}\", \"{19140}\", \"{40141}\", \"{29119}\", \"{8500}\", \"{6080}\", \"{11328}\",\n\t\"{1664}\", \"{55193}\", \"{1089}\", \"{13453}\", \"{11201}\", \"{23838}\", \"{8429}\", \"{3036}\", \"{16208}\", \"{18165}\",\n\t\"{136}\", \"{18788}\", \"{42713}\", \"{8932}\", \"{9525}\", \"{7648}\", \"{12476}\", \"{19069}\", \"{21258}\", \"{5133}\",\n\t\"{13890}\", \"{17518}\", \"{19098}\", \"{12487}\", \"{14899}\", \"{10511}\", \"{2726}\", \"{9339}\", \"{18779}\", \"{13366}\",\n\t\"{1151}\", \"{20548}\", \"{15995}\", \"{43003}\", \"{6158}\", \"{8235}\", \"{4293}\", \"{5869}\", \"{40932}\", \"{577}\",\n\t\"{48641}\", \"{2860}\", \"{3477}\", \"{8068}\", \"{19428}\", \"{4965}\", \"{5572}\", \"{21619}\", \"{2396}\", \"{9789}\",\n\t\"{7209}\", \"{9164}\", \"{6802}\", \"{49151}\", \"{9778}\", \"{2367}\", \"{17159}\", \"{5583}\", \"{4994}\", \"{44950}\",\n\t\"{8099}\", \"{3486}\", \"{2891}\", \"{6519}\", \"{586}\", \"{18338}\", \"{5898}\", \"{1510}\", \"{44875}\", \"{17691}\",\n\t\"{19311}\", \"{40310}\", \"{2242}\", \"{10698}\", \"{49074}\", \"{6927}\", \"{1435}\", \"{44383}\", \"{62864}\", \"{13602}\",\n\t\"{29348}\", \"{8751}\", \"{48178}\", \"{11179}\", \"{11188}\", \"{3552}\", \"{2945}\", \"{42981}\", \"{452}\", \"{1229}\",\n\t\"{41400}\", \"{18401}\", \"{9041}\", \"{28458}\", \"{3849}\", \"{49668}\", \"{45493}\", \"{5457}\", \"{4840}\", \"{949}\",\n\t\"{26568}\", \"{2603}\", \"{10434}\", \"{49435}\", \"{21561}\", \"{69521}\", \"{40751}\", \"{19750}\", \"{8310}\", \"{29709}\",\n\t\"{11538}\", \"{6690}\", \"{55783}\", \"{1074}\", \"{13243}\", \"{1699}\", \"{1668}\", \"{20271}\", \"{1085}\", \"{41041}\",\n\t\"{3113}\", \"{27278}\", \"{48325}\", \"{11324}\", \"{5016}\", \"{54093}\", \"{25818}\", \"{12553}\", \"{28019}\", \"{9400}\",\n\t\"{7180}\", \"{10228}\", \"{17308}\", \"{19065}\", \"{12297}\", \"{19688}\", \"{10301}\", \"{22938}\", \"{9529}\", \"{2136}\",\n\t\"{13576}\", \"{18169}\", \"{20358}\", \"{1741}\", \"{43613}\", \"{9832}\", \"{8425}\", \"{6748}\", \"{15999}\", \"{11411}\",\n\t\"{3626}\", \"{8239}\", \"{12990}\", \"{16418}\", \"{18198}\", \"{726}\", \"{14895}\", \"{42103}\", \"{7058}\", \"{9335}\",\n\t\"{19679}\", \"{12266}\", \"{5723}\", \"{21448}\", \"{49741}\", \"{3960}\", \"{26}\", \"{9168}\", \"{860}\", \"{4969}\",\n\t\"{41832}\", \"{18833}\", \"{3296}\", \"{8689}\", \"{6309}\", \"{8064}\", \"{18528}\", \"{396}\", \"{1300}\", \"{20719}\",\n\t\"{16059}\", \"{4483}\", \"{367}\", \"{45850}\", \"{7902}\", \"{48051}\", \"{8678}\", \"{3267}\", \"{1807}\", \"{19238}\",\n\t\"{4998}\", \"{891}\", \"{9199}\", \"{2586}\", \"{3991}\", \"{7419}\", \"{2246}\", \"{9659}\", \"{49070}\", \"{6923}\",\n\t\"{44871}\", \"{17695}\", \"{19315}\", \"{17078}\", \"{6438}\", \"{8755}\", \"{65869}\", \"{43563}\", \"{1431}\", \"{16799}\",\n\t\"{18219}\", \"{13606}\", \"{456}\", \"{40813}\", \"{5948}\", \"{18405}\", \"{8149}\", \"{3556}\", \"{2941}\", \"{42985}\",\n\t\"{17089}\", \"{5453}\", \"{4844}\", \"{19509}\", \"{9045}\", \"{7328}\", \"{42273}\", \"{43889}\", \"{21565}\", \"{69525}\",\n\t\"{17439}\", \"{19754}\", \"{9218}\", \"{2607}\", \"{10430}\", \"{7798}\", \"{20469}\", \"{1070}\", \"{13247}\", \"{18658}\",\n\t\"{8314}\", \"{6079}\", \"{43122}\", \"{6694}\", \"{3117}\", \"{8508}\", \"{6088}\", \"{11320}\", \"{68235}\", \"{20275}\",\n\t\"{1081}\", \"{16329}\", \"{7769}\", \"{9404}\", \"{7184}\", \"{42632}\", \"{5012}\", \"{21379}\", \"{19148}\", \"{12557}\",\n\t\"{10305}\", \"{39798}\", \"{26259}\", \"{2132}\", \"{40060}\", \"{19061}\", \"{12293}\", \"{13869}\", \"{11209}\", \"{9836}\",\n\t\"{8421}\", \"{29038}\", \"{13572}\", \"{24839}\", \"{41781}\", \"{1745}\", \"{12994}\", \"{41770}\", \"{1159}\", \"{722}\",\n\t\"{38088}\", \"{11415}\", \"{3622}\", \"{27549}\", \"{13898}\", \"{12262}\", \"{5727}\", \"{40091}\", \"{14891}\", \"{10519}\",\n\t\"{28728}\", \"{9331}\", \"{864}\", \"{40421}\", \"{41836}\", \"{18837}\", \"{28398}\", \"{3964}\", \"{22}\", \"{26618}\",\n\t\"{67827}\", \"{392}\", \"{1304}\", \"{50381}\", \"{3292}\", \"{2868}\", \"{29479}\", \"{8060}\", \"{7906}\", \"{29488}\",\n\t\"{2899}\", \"{3263}\", \"{41331}\", \"{4487}\", \"{363}\", \"{1518}\", \"{10158}\", \"{2582}\", \"{3995}\", \"{28369}\",\n\t\"{1803}\", \"{50886}\", \"{51491}\", \"{895}\", \"{3635}\", \"{46183}\", \"{26949}\", \"{11402}\", \"{68517}\", \"{735}\",\n\t\"{12983}\", \"{13379}\", \"{2739}\", \"{9326}\", \"{14886}\", \"{42110}\", \"{5730}\", \"{12498}\", \"{101830}\", \"{12275}\",\n\t\"{12284}\", \"{69207}\", \"{12469}\", \"{19076}\", \"{47693}\", \"{2125}\", \"{10312}\", \"{49313}\", \"{129}\", \"{1752}\",\n\t\"{13565}\", \"{105852}\", \"{8436}\", \"{3029}\", \"{43600}\", \"{9821}\", \"{374}\", \"{45843}\", \"{599}\", \"{4490}\",\n\t\"{28888}\", \"{3274}\", \"{7911}\", \"{3499}\", \"{24768}\", \"{882}\", \"{1814}\", \"{50891}\", \"{3982}\", \"{2378}\",\n\t\"{42551}\", \"{2595}\", \"{35}\", \"{56293}\", \"{2389}\", \"{3973}\", \"{41821}\", \"{18820}\", \"{873}\", \"{12028}\",\n\t\"{3468}\", \"{8077}\", \"{3285}\", \"{28879}\", \"{1313}\", \"{568}\", \"{67830}\", \"{385}\", \"{2956}\", \"{42992}\",\n\t\"{22158}\", \"{3541}\", \"{41413}\", \"{18412}\", \"{441}\", \"{4548}\", \"{15108}\", \"{89561}\", \"{9052}\", \"{46901}\",\n\t\"{4853}\", \"{44897}\", \"{18909}\", \"{5444}\", \"{19302}\", \"{40303}\", \"{5258}\", \"{0}\", \"{49067}\", \"{6934}\",\n\t\"{2251}\", \"{23648}\", \"{62877}\", \"{13611}\", \"{1426}\", \"{44390}\", \"{88271}\", \"{7838}\", \"{42963}\", \"{8742}\",\n\t\"{1096}\", \"{41052}\", \"{4109}\", \"{20262}\", \"{9908}\", \"{11337}\", \"{3100}\", \"{22519}\", \"{78582}\", \"{12540}\",\n\t\"{5005}\", \"{54080}\", \"{7193}\", \"{15549}\", \"{58496}\", \"{9413}\", \"{10427}\", \"{49426}\", \"{23209}\", \"{2610}\",\n\t\"{40742}\", \"{19743}\", \"{21572}\", \"{5619}\", \"{14259}\", \"{6683}\", \"{8303}\", \"{59386}\", \"{13250}\", \"{21869}\",\n\t\"{55790}\", \"{1067}\", \"{4638}\", \"{731}\", \"{12987}\", \"{41763}\", \"{3631}\", \"{14599}\", \"{48407}\", \"{11406}\",\n\t\"{5734}\", \"{40082}\", \"{20848}\", \"{12271}\", \"{109747}\", \"{9322}\", \"{14882}\", \"{15278}\", \"{15289}\", \"{2121}\",\n\t\"{10316}\", \"{8929}\", \"{12280}\", \"{5128}\", \"{40073}\", \"{19072}\", \"{8432}\", \"{82909}\", \"{14568}\", \"{9825}\",\n\t\"{41792}\", \"{1756}\", \"{13561}\", \"{105856}\", \"{22669}\", \"{3270}\", \"{7915}\", \"{48046}\", \"{370}\", \"{4279}\",\n\t\"{41322}\", \"{4494}\", \"{3986}\", \"{43942}\", \"{6819}\", \"{2591}\", \"{51482}\", \"{886}\", \"{1810}\", \"{5598}\",\n\t\"{5569}\", \"{18824}\", \"{877}\", \"{40432}\", \"{31}\", \"{23179}\", \"{49756}\", \"{3977}\", \"{1317}\", \"{19928}\",\n\t\"{4288}\", \"{381}\", \"{47920}\", \"{8073}\", \"{3281}\", \"{14129}\", \"{13009}\", \"{18416}\", \"{445}\", \"{40800}\",\n\t\"{2952}\", \"{42996}\", \"{43581}\", \"{3545}\", \"{4857}\", \"{44893}\", \"{24059}\", \"{5440}\", \"{29858}\", \"{61818}\",\n\t\"{9056}\", \"{2449}\", \"{49063}\", \"{6930}\", \"{2255}\", \"{42291}\", \"{19306}\", \"{1939}\", \"{44862}\", \"{4}\",\n\t\"{88275}\", \"{43570}\", \"{3359}\", \"{8746}\", \"{62873}\", \"{13615}\", \"{1422}\", \"{259}\", \"{48332}\", \"{11333}\",\n\t\"{3104}\", \"{52181}\", \"{1092}\", \"{13448}\", \"{68226}\", \"{20266}\", \"{7197}\", \"{42621}\", \"{2008}\", \"{9417}\",\n\t\"{78586}\", \"{12544}\", \"{5001}\", \"{24418}\", \"{12358}\", \"{19747}\", \"{21576}\", \"{69536}\", \"{10423}\", \"{27968}\",\n\t\"{48835}\", \"{2614}\", \"{13254}\", \"{79296}\", \"{618}\", \"{1063}\", \"{43131}\", \"{6687}\", \"{8307}\", \"{3718}\",\n\t\"{2731}\", \"{15499}\", \"{49507}\", \"{10506}\", \"{5738}\", \"{12490}\", \"{13887}\", \"{40663}\", \"{108647}\", \"{8222}\",\n\t\"{15982}\", \"{14378}\", \"{1146}\", \"{41182}\", \"{21948}\", \"{13371}\", \"{121}\", \"{4028}\", \"{41173}\", \"{18172}\",\n\t\"{14389}\", \"{3021}\", \"{11216}\", \"{9829}\", \"{40692}\", \"{5124}\", \"{12461}\", \"{104956}\", \"{9532}\", \"{83809}\",\n\t\"{15468}\", \"{8925}\", \"{4983}\", \"{5379}\", \"{40222}\", \"{5594}\", \"{23769}\", \"{2370}\", \"{6815}\", \"{49146}\",\n\t\"{50582}\", \"{1507}\", \"{591}\", \"{4498}\", \"{2886}\", \"{42842}\", \"{7919}\", \"{3491}\", \"{3460}\", \"{22079}\",\n\t\"{48656}\", \"{2877}\", \"{4469}\", \"{560}\", \"{4284}\", \"{41532}\", \"{46820}\", \"{9173}\", \"{2381}\", \"{15029}\",\n\t\"{5565}\", \"{18828}\", \"{5388}\", \"{4972}\", \"{3852}\", \"{43896}\", \"{42481}\", \"{2445}\", \"{12109}\", \"{952}\",\n\t\"{17096}\", \"{41900}\", \"{28958}\", \"{60918}\", \"{8156}\", \"{3549}\", \"{5957}\", \"{45993}\", \"{449}\", \"{1232}\",\n\t\"{18206}\", \"{13619}\", \"{45962}\", \"{255}\", \"{48163}\", \"{7830}\", \"{3355}\", \"{43391}\", \"{63973}\", \"{1935}\",\n\t\"{5250}\", \"{8}\", \"{89375}\", \"{42470}\", \"{2259}\", \"{9646}\", \"{19157}\", \"{12548}\", \"{69326}\", \"{21366}\",\n\t\"{49232}\", \"{10233}\", \"{2004}\", \"{53081}\", \"{79486}\", \"{13444}\", \"{1673}\", \"{25518}\", \"{6097}\", \"{43721}\",\n\t\"{3108}\", \"{8517}\", \"{11523}\", \"{26868}\", \"{49935}\", \"{3714}\", \"{13258}\", \"{1682}\", \"{614}\", \"{68436}\",\n\t\"{42031}\", \"{7787}\", \"{9207}\", \"{2618}\", \"{12354}\", \"{78396}\", \"{24208}\", \"{5611}\", \"{69417}\", \"{12494}\",\n\t\"{13883}\", \"{12279}\", \"{2735}\", \"{47083}\", \"{27849}\", \"{10502}\", \"{1142}\", \"{739}\", \"{100930}\", \"{13375}\",\n\t\"{3639}\", \"{8226}\", \"{15986}\", \"{43010}\", \"{46793}\", \"{3025}\", \"{11212}\", \"{48213}\", \"{125}\", \"{68307}\",\n\t\"{13569}\", \"{18176}\", \"{9536}\", \"{2129}\", \"{42700}\", \"{8921}\", \"{12288}\", \"{5120}\", \"{12465}\", \"{104952}\",\n\t\"{29988}\", \"{2374}\", \"{6811}\", \"{2599}\", \"{4987}\", \"{44943}\", \"{1818}\", \"{5590}\", \"{2882}\", \"{3278}\",\n\t\"{43451}\", \"{3495}\", \"{378}\", \"{1503}\", \"{595}\", \"{51991}\", \"{40921}\", \"{564}\", \"{4280}\", \"{389}\",\n\t\"{3464}\", \"{57393}\", \"{3289}\", \"{2873}\", \"{5561}\", \"{24178}\", \"{66930}\", \"{4976}\", \"{39}\", \"{9177}\",\n\t\"{2385}\", \"{29979}\", \"{40513}\", \"{956}\", \"{17092}\", \"{5448}\", \"{3856}\", \"{43892}\", \"{23058}\", \"{2441}\",\n\t\"{5953}\", \"{45997}\", \"{19809}\", \"{1236}\", \"{14008}\", \"{88461}\", \"{8152}\", \"{47801}\", \"{48167}\", \"{7834}\",\n\t\"{3351}\", \"{22748}\", \"{18202}\", \"{41203}\", \"{4358}\", \"{251}\", \"{89371}\", \"{6938}\", \"{43863}\", \"{9642}\",\n\t\"{63977}\", \"{1931}\", \"{5254}\", \"{45290}\", \"{8808}\", \"{10237}\", \"{2000}\", \"{23419}\", \"{19153}\", \"{40152}\",\n\t\"{5009}\", \"{21362}\", \"{6093}\", \"{14449}\", \"{59596}\", \"{8513}\", \"{79482}\", \"{13440}\", \"{1677}\", \"{55180}\",\n\t\"{41642}\", \"{1686}\", \"{610}\", \"{4719}\", \"{11527}\", \"{48526}\", \"{22309}\", \"{3710}\", \"{12350}\", \"{20969}\",\n\t\"{54690}\", \"{5615}\", \"{15359}\", \"{7783}\", \"{9203}\", \"{58286}\", \"{16248}\", \"{4692}\", \"{176}\", \"{68354}\",\n\t\"{8284}\", \"{23878}\", \"{8469}\", \"{3076}\", \"{12436}\", \"{19029}\", \"{21218}\", \"{5173}\", \"{9388}\", \"{2797}\",\n\t\"{9565}\", \"{7608}\", \"{49550}\", \"{9594}\", \"{2766}\", \"{9379}\", \"{5182}\", \"{17558}\", \"{69444}\", \"{21404}\",\n\t\"{3087}\", \"{8498}\", \"{6118}\", \"{8275}\", \"{18739}\", \"{187}\", \"{1111}\", \"{20508}\", \"{48601}\", \"{2820}\",\n\t\"{3437}\", \"{8028}\", \"{18564}\", \"{5829}\", \"{18389}\", \"{537}\", \"{39391}\", \"{42312}\", \"{7249}\", \"{9124}\",\n\t\"{19468}\", \"{4925}\", \"{5532}\", \"{21659}\", \"{17119}\", \"{19274}\", \"{12086}\", \"{19499}\", \"{6842}\", \"{46886}\",\n\t\"{9738}\", \"{2327}\", \"{13767}\", \"{18378}\", \"{20149}\", \"{1550}\", \"{43402}\", \"{38481}\", \"{8634}\", \"{6559}\",\n\t\"{1479}\", \"{202}\", \"{1294}\", \"{41250}\", \"{3302}\", \"{27069}\", \"{48134}\", \"{7867}\", \"{5207}\", \"{54282}\",\n\t\"{63924}\", \"{1962}\", \"{28208}\", \"{9611}\", \"{7391}\", \"{10039}\", \"{26779}\", \"{2412}\", \"{3805}\", \"{49624}\",\n\t\"{1993}\", \"{41957}\", \"{40540}\", \"{905}\", \"{7896}\", \"{29518}\", \"{2909}\", \"{6481}\", \"{55592}\", \"{1265}\",\n\t\"{5900}\", \"{1488}\", \"{11399}\", \"{3743}\", \"{11574}\", \"{48575}\", \"{643}\", \"{1038}\", \"{41611}\", \"{18610}\",\n\t\"{9250}\", \"{28649}\", \"{10478}\", \"{27933}\", \"{45682}\", \"{5646}\", \"{12303}\", \"{69380}\", \"{13908}\", \"{17480}\",\n\t\"{16897}\", \"{40101}\", \"{2053}\", \"{10489}\", \"{49265}\", \"{10264}\", \"{1624}\", \"{44192}\", \"{24958}\", \"{13413}\",\n\t\"{29159}\", \"{8540}\", \"{9957}\", \"{11368}\", \"{8280}\", \"{29699}\", \"{27319}\", \"{3072}\", \"{41120}\", \"{4696}\",\n\t\"{172}\", \"{1709}\", \"{10349}\", \"{2793}\", \"{9561}\", \"{28178}\", \"{12432}\", \"{25979}\", \"{51680}\", \"{5177}\",\n\t\"{5186}\", \"{40630}\", \"{25988}\", \"{21400}\", \"{28189}\", \"{9590}\", \"{2762}\", \"{26409}\", \"{100967}\", \"{183}\",\n\t\"{1115}\", \"{50190}\", \"{3083}\", \"{11459}\", \"{29668}\", \"{8271}\", \"{18560}\", \"{41561}\", \"{1348}\", \"{533}\",\n\t\"{38299}\", \"{2824}\", \"{3433}\", \"{27758}\", \"{828}\", \"{4921}\", \"{5536}\", \"{40280}\", \"{39395}\", \"{3928}\",\n\t\"{28539}\", \"{9120}\", \"{6846}\", \"{39589}\", \"{26048}\", \"{2323}\", \"{40271}\", \"{19270}\", \"{12082}\", \"{44914}\",\n\t\"{11018}\", \"{38485}\", \"{8630}\", \"{29229}\", \"{13763}\", \"{40987}\", \"{41590}\", \"{1554}\", \"{3306}\", \"{8719}\",\n\t\"{6299}\", \"{7863}\", \"{45931}\", \"{206}\", \"{1290}\", \"{16138}\", \"{7578}\", \"{9615}\", \"{7395}\", \"{42423}\",\n\t\"{5203}\", \"{21168}\", \"{19359}\", \"{1966}\", \"{1997}\", \"{41953}\", \"{4808}\", \"{901}\", \"{9009}\", \"{2416}\",\n\t\"{3801}\", \"{7589}\", \"{20678}\", \"{1261}\", \"{5904}\", \"{18449}\", \"{7892}\", \"{6268}\", \"{43333}\", \"{6485}\",\n\t\"{647}\", \"{68465}\", \"{16579}\", \"{18614}\", \"{8358}\", \"{3747}\", \"{11570}\", \"{48571}\", \"{17298}\", \"{5642}\",\n\t\"{12307}\", \"{19718}\", \"{9254}\", \"{7139}\", \"{42062}\", \"{27937}\", \"{2057}\", \"{9448}\", \"{22859}\", \"{10260}\",\n\t\"{69375}\", \"{17484}\", \"{16893}\", \"{17269}\", \"{6629}\", \"{8544}\", \"{9953}\", \"{43772}\", \"{1620}\", \"{16588}\",\n\t\"{18008}\", \"{13417}\", \"{40020}\", \"{5796}\", \"{21210}\", \"{13829}\", \"{9380}\", \"{28799}\", \"{26219}\", \"{2172}\",\n\t\"{793}\", \"{24879}\", \"{50780}\", \"{1705}\", \"{11249}\", \"{3693}\", \"{8461}\", \"{29078}\", \"{29089}\", \"{8490}\",\n\t\"{3662}\", \"{27509}\", \"{4086}\", \"{41730}\", \"{1119}\", \"{762}\", \"{2183}\", \"{10559}\", \"{28768}\", \"{9371}\",\n\t\"{101867}\", \"{12222}\", \"{5767}\", \"{51090}\", \"{39399}\", \"{3924}\", \"{62}\", \"{26658}\", \"{824}\", \"{40461}\",\n\t\"{41876}\", \"{12692}\", \"{38295}\", \"{2828}\", \"{29439}\", \"{8020}\", \"{67867}\", \"{5821}\", \"{1344}\", \"{41380}\",\n\t\"{41371}\", \"{18370}\", \"{323}\", \"{1558}\", \"{7946}\", \"{38489}\", \"{27148}\", \"{3223}\", \"{1843}\", \"{41887}\",\n\t\"{40490}\", \"{5326}\", \"{10118}\", \"{93}\", \"{9730}\", \"{28329}\", \"{44831}\", \"{21164}\", \"{19355}\", \"{17038}\",\n\t\"{2206}\", \"{9619}\", \"{7399}\", \"{6963}\", \"{1471}\", \"{20068}\", \"{18259}\", \"{13646}\", \"{6478}\", \"{8715}\",\n\t\"{6295}\", \"{43523}\", \"{8109}\", \"{3516}\", \"{2901}\", \"{6489}\", \"{416}\", \"{40853}\", \"{5908}\", \"{1480}\",\n\t\"{6992}\", \"{7368}\", \"{42233}\", \"{7585}\", \"{21778}\", \"{5413}\", \"{4804}\", \"{19549}\", \"{9258}\", \"{2647}\",\n\t\"{10470}\", \"{49471}\", \"{17294}\", \"{69565}\", \"{17479}\", \"{19714}\", \"{8354}\", \"{6039}\", \"{43162}\", \"{26837}\",\n\t\"{16398}\", \"{1030}\", \"{13207}\", \"{18618}\", \"{68275}\", \"{16584}\", \"{17993}\", \"{16369}\", \"{3157}\", \"{8548}\",\n\t\"{23959}\", \"{11360}\", \"{5052}\", \"{17488}\", \"{19108}\", \"{12517}\", \"{7729}\", \"{9444}\", \"{8853}\", \"{42672}\",\n\t\"{9384}\", \"{22978}\", \"{9569}\", \"{2176}\", \"{17348}\", \"{5792}\", \"{21214}\", \"{69254}\", \"{8288}\", \"{3697}\",\n\t\"{8465}\", \"{6708}\", \"{797}\", \"{18129}\", \"{20318}\", \"{1701}\", \"{4082}\", \"{16458}\", \"{68544}\", \"{766}\",\n\t\"{48450}\", \"{8494}\", \"{3666}\", \"{8279}\", \"{19639}\", \"{12226}\", \"{5763}\", \"{21408}\", \"{2187}\", \"{9598}\",\n\t\"{7018}\", \"{9375}\", \"{820}\", \"{4929}\", \"{19289}\", \"{12696}\", \"{49701}\", \"{3920}\", \"{66}\", \"{9128}\",\n\t\"{18568}\", \"{5825}\", \"{1340}\", \"{20759}\", \"{38291}\", \"{43212}\", \"{6349}\", \"{8024}\", \"{7942}\", \"{47986}\",\n\t\"{8638}\", \"{3227}\", \"{16019}\", \"{18374}\", \"{327}\", \"{18599}\", \"{42502}\", \"{97}\", \"{9734}\", \"{7459}\",\n\t\"{1847}\", \"{19278}\", \"{21049}\", \"{5322}\", \"{2202}\", \"{26169}\", \"{49034}\", \"{6967}\", \"{44835}\", \"{21160}\",\n\t\"{19351}\", \"{40350}\", \"{29308}\", \"{8711}\", \"{6291}\", \"{11139}\", \"{1475}\", \"{55382}\", \"{1298}\", \"{13642}\",\n\t\"{412}\", \"{1269}\", \"{41440}\", \"{1484}\", \"{27679}\", \"{3512}\", \"{2905}\", \"{48724}\", \"{54492}\", \"{5417}\",\n\t\"{4800}\", \"{909}\", \"{6996}\", \"{28418}\", \"{3809}\", \"{7581}\", \"{17290}\", \"{69561}\", \"{40711}\", \"{19710}\",\n\t\"{10299}\", \"{2643}\", \"{10474}\", \"{49475}\", \"{44782}\", \"{1034}\", \"{13203}\", \"{68280}\", \"{8350}\", \"{29749}\",\n\t\"{11578}\", \"{26833}\", \"{3153}\", \"{11589}\", \"{48365}\", \"{11364}\", \"{1628}\", \"{16580}\", \"{17997}\", \"{41001}\",\n\t\"{28059}\", \"{9440}\", \"{8857}\", \"{10268}\", \"{5056}\", \"{45092}\", \"{25858}\", \"{12513}\", \"{2912}\", \"{48733}\",\n\t\"{52580}\", \"{3505}\", \"{13049}\", \"{1493}\", \"{405}\", \"{40840}\", \"{29818}\", \"{7596}\", \"{6981}\", \"{2409}\",\n\t\"{4817}\", \"{55892}\", \"{1988}\", \"{5400}\", \"{19346}\", \"{1979}\", \"{44822}\", \"{21177}\", \"{49023}\", \"{6970}\",\n\t\"{2215}\", \"{53290}\", \"{62833}\", \"{13655}\", \"{1462}\", \"{219}\", \"{6286}\", \"{43530}\", \"{3319}\", \"{8706}\",\n\t\"{17980}\", \"{13408}\", \"{68266}\", \"{16597}\", \"{10989}\", \"{11373}\", \"{3144}\", \"{43180}\", \"{69587}\", \"{12504}\",\n\t\"{5041}\", \"{24458}\", \"{8840}\", \"{42661}\", \"{2048}\", \"{9457}\", \"{10463}\", \"{27928}\", \"{42690}\", \"{2654}\",\n\t\"{12318}\", \"{19707}\", \"{17287}\", \"{69576}\", \"{43171}\", \"{26824}\", \"{8347}\", \"{3758}\", \"{13214}\", \"{68297}\",\n\t\"{658}\", \"{1023}\", \"{3671}\", \"{22268}\", \"{48447}\", \"{8483}\", \"{4678}\", \"{771}\", \"{4095}\", \"{41723}\",\n\t\"{8998}\", \"{9362}\", \"{2190}\", \"{15238}\", \"{5774}\", \"{51083}\", \"{5199}\", \"{12231}\", \"{21203}\", \"{5168}\",\n\t\"{40033}\", \"{5785}\", \"{23578}\", \"{2161}\", \"{9393}\", \"{8969}\", \"{50793}\", \"{1716}\", \"{780}\", \"{4689}\",\n\t\"{8472}\", \"{82949}\", \"{14528}\", \"{3680}\", \"{330}\", \"{4239}\", \"{19999}\", \"{18363}\", \"{14198}\", \"{3230}\",\n\t\"{7955}\", \"{47991}\", \"{40483}\", \"{5335}\", \"{1850}\", \"{41894}\", \"{9723}\", \"{43902}\", \"{6859}\", \"{80}\",\n\t\"{71}\", \"{15688}\", \"{49716}\", \"{3937}\", \"{5529}\", \"{12681}\", \"{837}\", \"{40472}\", \"{47960}\", \"{8033}\",\n\t\"{38286}\", \"{14169}\", \"{1357}\", \"{19968}\", \"{67874}\", \"{5832}\", \"{41453}\", \"{1497}\", \"{401}\", \"{4508}\",\n\t\"{2916}\", \"{48737}\", \"{7889}\", \"{3501}\", \"{4813}\", \"{55896}\", \"{18949}\", \"{5404}\", \"{15148}\", \"{7592}\",\n\t\"{6985}\", \"{46941}\", \"{49027}\", \"{6974}\", \"{2211}\", \"{23608}\", \"{19342}\", \"{40343}\", \"{5218}\", \"{21173}\",\n\t\"{6282}\", \"{7878}\", \"{42923}\", \"{8702}\", \"{62837}\", \"{13651}\", \"{1466}\", \"{55391}\", \"{9948}\", \"{11377}\",\n\t\"{3140}\", \"{22559}\", \"{17984}\", \"{41012}\", \"{4149}\", \"{16593}\", \"{8844}\", \"{15509}\", \"{49497}\", \"{9453}\",\n\t\"{16888}\", \"{12500}\", \"{5045}\", \"{45081}\", \"{40702}\", \"{19703}\", \"{17283}\", \"{5659}\", \"{10467}\", \"{49466}\",\n\t\"{23249}\", \"{2650}\", \"{13210}\", \"{21829}\", \"{44791}\", \"{1027}\", \"{14219}\", \"{26820}\", \"{8343}\", \"{48387}\",\n\t\"{68557}\", \"{775}\", \"{4091}\", \"{198}\", \"{3675}\", \"{49854}\", \"{3098}\", \"{8487}\", \"{5770}\", \"{24369}\",\n\t\"{101870}\", \"{12235}\", \"{2779}\", \"{9366}\", \"{2194}\", \"{42150}\", \"{56692}\", \"{2165}\", \"{9397}\", \"{2788}\",\n\t\"{21207}\", \"{69247}\", \"{12429}\", \"{5781}\", \"{8476}\", \"{3069}\", \"{43640}\", \"{3684}\", \"{169}\", \"{1712}\",\n\t\"{784}\", \"{105812}\", \"{39889}\", \"{3234}\", \"{7951}\", \"{47995}\", \"{334}\", \"{45803}\", \"{13778}\", \"{18367}\",\n\t\"{9727}\", \"{2338}\", \"{42511}\", \"{84}\", \"{12099}\", \"{5331}\", \"{1854}\", \"{41890}\", \"{41861}\", \"{12685}\",\n\t\"{833}\", \"{12068}\", \"{75}\", \"{47292}\", \"{49712}\", \"{3933}\", \"{1353}\", \"{528}\", \"{67870}\", \"{5836}\",\n\t\"{3428}\", \"{8037}\", \"{38282}\", \"{28839}\", \"{3816}\", \"{49637}\", \"{6989}\", \"{2401}\", \"{40553}\", \"{916}\",\n\t\"{1980}\", \"{5408}\", \"{14048}\", \"{6492}\", \"{7885}\", \"{47841}\", \"{5913}\", \"{54996}\", \"{19849}\", \"{1276}\",\n\t\"{1287}\", \"{41243}\", \"{4318}\", \"{211}\", \"{48127}\", \"{7874}\", \"{3311}\", \"{22708}\", \"{63937}\", \"{1971}\",\n\t\"{5214}\", \"{54291}\", \"{7382}\", \"{6978}\", \"{43823}\", \"{9602}\", \"{16884}\", \"{40112}\", \"{5049}\", \"{17493}\",\n\t\"{8848}\", \"{10277}\", \"{2040}\", \"{23459}\", \"{17988}\", \"{13400}\", \"{1637}\", \"{44181}\", \"{9944}\", \"{14409}\",\n\t\"{48597}\", \"{8553}\", \"{11567}\", \"{48566}\", \"{22349}\", \"{3750}\", \"{41602}\", \"{18603}\", \"{650}\", \"{4759}\",\n\t\"{15319}\", \"{27920}\", \"{9243}\", \"{49287}\", \"{12310}\", \"{20929}\", \"{45691}\", \"{5655}\", \"{2775}\", \"{48954}\",\n\t\"{2198}\", \"{9587}\", \"{69457}\", \"{21417}\", \"{5191}\", \"{12239}\", \"{3679}\", \"{8266}\", \"{3094}\", \"{43050}\",\n\t\"{1102}\", \"{779}\", \"{100970}\", \"{194}\", \"{165}\", \"{68347}\", \"{788}\", \"{4681}\", \"{57792}\", \"{3065}\",\n\t\"{8297}\", \"{3688}\", \"{24579}\", \"{5160}\", \"{12425}\", \"{104912}\", \"{9576}\", \"{2169}\", \"{42740}\", \"{2784}\",\n\t\"{12095}\", \"{44903}\", \"{1858}\", \"{19267}\", \"{38989}\", \"{2334}\", \"{6851}\", \"{88}\", \"{338}\", \"{1543}\",\n\t\"{13774}\", \"{40990}\", \"{8627}\", \"{3238}\", \"{43411}\", \"{38492}\", \"{3424}\", \"{46392}\", \"{48612}\", \"{2833}\",\n\t\"{40961}\", \"{524}\", \"{18577}\", \"{13168}\", \"{79}\", \"{9137}\", \"{39382}\", \"{29939}\", \"{5521}\", \"{12689}\",\n\t\"{66970}\", \"{4936}\", \"{12149}\", \"{912}\", \"{1984}\", \"{41940}\", \"{3812}\", \"{49633}\", \"{53480}\", \"{2405}\",\n\t\"{5917}\", \"{54992}\", \"{409}\", \"{1272}\", \"{28918}\", \"{6496}\", \"{7881}\", \"{3509}\", \"{48123}\", \"{7870}\",\n\t\"{3315}\", \"{52390}\", \"{1283}\", \"{13659}\", \"{45922}\", \"{215}\", \"{7386}\", \"{42430}\", \"{2219}\", \"{9606}\",\n\t\"{63933}\", \"{1975}\", \"{5210}\", \"{24609}\", \"{11889}\", \"{10273}\", \"{2044}\", \"{42080}\", \"{16880}\", \"{12508}\",\n\t\"{69366}\", \"{17497}\", \"{9940}\", \"{43761}\", \"{3148}\", \"{8557}\", \"{68487}\", \"{13404}\", \"{1633}\", \"{25558}\",\n\t\"{13218}\", \"{18607}\", \"{654}\", \"{68476}\", \"{11563}\", \"{26828}\", \"{43790}\", \"{3754}\", \"{12314}\", \"{69397}\",\n\t\"{24248}\", \"{5651}\", \"{42071}\", \"{27924}\", \"{9247}\", \"{2658}\", \"{5778}\", \"{21413}\", \"{5195}\", \"{40623}\",\n\t\"{2771}\", \"{23368}\", \"{49547}\", \"{9583}\", \"{1106}\", \"{50183}\", \"{4099}\", \"{190}\", \"{9898}\", \"{8262}\",\n\t\"{3090}\", \"{14338}\", \"{22478}\", \"{3061}\", \"{8293}\", \"{9869}\", \"{161}\", \"{4068}\", \"{41133}\", \"{4685}\",\n\t\"{9572}\", \"{83849}\", \"{15428}\", \"{2780}\", \"{51693}\", \"{5164}\", \"{12421}\", \"{5789}\", \"{15098}\", \"{2330}\",\n\t\"{6855}\", \"{46891}\", \"{12091}\", \"{5339}\", \"{18899}\", \"{19263}\", \"{8623}\", \"{42802}\", \"{7959}\", \"{38496}\",\n\t\"{41583}\", \"{1547}\", \"{13770}\", \"{40994}\", \"{4429}\", \"{520}\", \"{18573}\", \"{41572}\", \"{3420}\", \"{14788}\",\n\t\"{48616}\", \"{2837}\", \"{5525}\", \"{18868}\", \"{66974}\", \"{4932}\", \"{46860}\", \"{9133}\", \"{39386}\", \"{15069}\",\n\t\"{4782}\", \"{16358}\", \"{68244}\", \"{20204}\", \"{23968}\", \"{8394}\", \"{3166}\", \"{8579}\", \"{19139}\", \"{12526}\",\n\t\"{5063}\", \"{21308}\", \"{2687}\", \"{9298}\", \"{7718}\", \"{9475}\", \"{9484}\", \"{49440}\", \"{9269}\", \"{2676}\",\n\t\"{17448}\", \"{5092}\", \"{21514}\", \"{69554}\", \"{8588}\", \"{3197}\", \"{8365}\", \"{6008}\", \"{13236}\", \"{18629}\",\n\t\"{20418}\", \"{1001}\", \"{2930}\", \"{48711}\", \"{8138}\", \"{3527}\", \"{5939}\", \"{18474}\", \"{427}\", \"{18299}\",\n\t\"{42202}\", \"{39281}\", \"{9034}\", \"{7359}\", \"{4835}\", \"{19578}\", \"{21749}\", \"{5422}\", \"{19364}\", \"{17009}\",\n\t\"{19589}\", \"{12196}\", \"{46996}\", \"{6952}\", \"{2237}\", \"{9628}\", \"{18268}\", \"{13677}\", \"{1440}\", \"{20059}\",\n\t\"{38591}\", \"{43512}\", \"{6449}\", \"{8724}\", \"{312}\", \"{1569}\", \"{41340}\", \"{1384}\", \"{27179}\", \"{3212}\",\n\t\"{7977}\", \"{48024}\", \"{54392}\", \"{5317}\", \"{1872}\", \"{63834}\", \"{9701}\", \"{28318}\", \"{10129}\", \"{7281}\",\n\t\"{53}\", \"{26669}\", \"{49734}\", \"{3915}\", \"{41847}\", \"{1883}\", \"{815}\", \"{40450}\", \"{29408}\", \"{7986}\",\n\t\"{6591}\", \"{2819}\", \"{1375}\", \"{55482}\", \"{1598}\", \"{5810}\", \"{3653}\", \"{11289}\", \"{48465}\", \"{11464}\",\n\t\"{1128}\", \"{753}\", \"{18700}\", \"{41701}\", \"{28759}\", \"{9340}\", \"{27823}\", \"{10568}\", \"{5756}\", \"{45792}\",\n\t\"{69290}\", \"{12213}\", \"{17590}\", \"{13818}\", \"{40011}\", \"{16987}\", \"{10599}\", \"{2143}\", \"{10374}\", \"{49375}\",\n\t\"{44082}\", \"{1734}\", \"{13503}\", \"{24848}\", \"{8450}\", \"{29049}\", \"{11278}\", \"{9847}\", \"{29789}\", \"{8390}\",\n\t\"{3162}\", \"{27209}\", \"{4786}\", \"{41030}\", \"{1619}\", \"{20200}\", \"{2683}\", \"{10259}\", \"{28068}\", \"{9471}\",\n\t\"{25869}\", \"{12522}\", \"{5067}\", \"{51790}\", \"{40720}\", \"{5096}\", \"{21510}\", \"{25898}\", \"{9480}\", \"{28099}\",\n\t\"{26519}\", \"{2672}\", \"{13232}\", \"{100877}\", \"{50080}\", \"{1005}\", \"{11549}\", \"{3193}\", \"{8361}\", \"{29778}\",\n\t\"{41471}\", \"{18470}\", \"{423}\", \"{1258}\", \"{2934}\", \"{38389}\", \"{27648}\", \"{3523}\", \"{4831}\", \"{938}\",\n\t\"{40390}\", \"{5426}\", \"{3838}\", \"{39285}\", \"{9030}\", \"{28429}\", \"{39499}\", \"{6956}\", \"{2233}\", \"{26158}\",\n\t\"{19360}\", \"{40361}\", \"{44804}\", \"{12192}\", \"{38595}\", \"{11108}\", \"{29339}\", \"{8720}\", \"{40897}\", \"{13673}\",\n\t\"{1444}\", \"{41480}\", \"{8609}\", \"{3216}\", \"{7973}\", \"{6389}\", \"{316}\", \"{45821}\", \"{16028}\", \"{1380}\",\n\t\"{9705}\", \"{7468}\", \"{42533}\", \"{7285}\", \"{21078}\", \"{5313}\", \"{1876}\", \"{19249}\", \"{41843}\", \"{1887}\",\n\t\"{811}\", \"{4918}\", \"{57}\", \"{9119}\", \"{7499}\", \"{3911}\", \"{1371}\", \"{20768}\", \"{18559}\", \"{5814}\",\n\t\"{6378}\", \"{7982}\", \"{6595}\", \"{43223}\", \"{68575}\", \"{757}\", \"{18704}\", \"{16469}\", \"{3657}\", \"{8248}\",\n\t\"{48461}\", \"{11460}\", \"{5752}\", \"{17388}\", \"{19608}\", \"{12217}\", \"{7029}\", \"{9344}\", \"{27827}\", \"{42172}\",\n\t\"{9558}\", \"{2147}\", \"{10370}\", \"{22949}\", \"{17594}\", \"{69265}\", \"{17379}\", \"{16983}\", \"{8454}\", \"{6739}\",\n\t\"{43662}\", \"{9843}\", \"{16498}\", \"{1730}\", \"{13507}\", \"{18118}\", \"{5686}\", \"{40130}\", \"{13939}\", \"{21300}\",\n\t\"{28689}\", \"{9290}\", \"{2062}\", \"{26309}\", \"{24969}\", \"{683}\", \"{1615}\", \"{50690}\", \"{3783}\", \"{11359}\",\n\t\"{29168}\", \"{8571}\", \"{8580}\", \"{29199}\", \"{27419}\", \"{3772}\", \"{41620}\", \"{4196}\", \"{672}\", \"{1009}\",\n\t\"{10449}\", \"{2093}\", \"{9261}\", \"{28678}\", \"{12332}\", \"{101977}\", \"{51180}\", \"{5677}\", \"{3834}\", \"{39289}\",\n\t\"{26748}\", \"{2423}\", \"{40571}\", \"{934}\", \"{12782}\", \"{41966}\", \"{2938}\", \"{38385}\", \"{8130}\", \"{29529}\",\n\t\"{5931}\", \"{67977}\", \"{41290}\", \"{1254}\", \"{18260}\", \"{41261}\", \"{1448}\", \"{233}\", \"{38599}\", \"{7856}\",\n\t\"{3333}\", \"{27058}\", \"{41997}\", \"{1953}\", \"{5236}\", \"{40580}\", \"{39495}\", \"{10008}\", \"{28239}\", \"{9620}\",\n\t\"{21074}\", \"{44921}\", \"{17128}\", \"{19245}\", \"{9709}\", \"{2316}\", \"{6873}\", \"{7289}\", \"{20178}\", \"{1561}\",\n\t\"{13756}\", \"{18349}\", \"{8605}\", \"{6568}\", \"{43433}\", \"{6385}\", \"{3406}\", \"{8019}\", \"{6599}\", \"{2811}\",\n\t\"{40943}\", \"{506}\", \"{1590}\", \"{5818}\", \"{7278}\", \"{6882}\", \"{7495}\", \"{42323}\", \"{5503}\", \"{21668}\",\n\t\"{19459}\", \"{4914}\", \"{2757}\", \"{9348}\", \"{49561}\", \"{10560}\", \"{69475}\", \"{17384}\", \"{19604}\", \"{17569}\",\n\t\"{6129}\", \"{8244}\", \"{26927}\", \"{43072}\", \"{1120}\", \"{16288}\", \"{18708}\", \"{13317}\", \"{147}\", \"{68365}\",\n\t\"{16279}\", \"{17883}\", \"{8458}\", \"{3047}\", \"{11270}\", \"{23849}\", \"{17598}\", \"{5142}\", \"{12407}\", \"{19018}\",\n\t\"{9554}\", \"{7639}\", \"{42762}\", \"{8943}\", \"{22868}\", \"{9294}\", \"{2066}\", \"{9479}\", \"{5682}\", \"{17258}\",\n\t\"{69344}\", \"{21304}\", \"{3787}\", \"{8398}\", \"{6618}\", \"{8575}\", \"{18039}\", \"{687}\", \"{1611}\", \"{20208}\",\n\t\"{16548}\", \"{4192}\", \"{676}\", \"{68454}\", \"{8584}\", \"{48540}\", \"{8369}\", \"{3776}\", \"{12336}\", \"{19729}\",\n\t\"{21518}\", \"{5673}\", \"{9488}\", \"{2097}\", \"{9265}\", \"{7108}\", \"{4839}\", \"{930}\", \"{12786}\", \"{19399}\",\n\t\"{3830}\", \"{49611}\", \"{9038}\", \"{2427}\", \"{5935}\", \"{18478}\", \"{20649}\", \"{1250}\", \"{43302}\", \"{38381}\",\n\t\"{8134}\", \"{6259}\", \"{47896}\", \"{7852}\", \"{3337}\", \"{8728}\", \"{18264}\", \"{16109}\", \"{18489}\", \"{237}\",\n\t\"{39491}\", \"{42412}\", \"{7549}\", \"{9624}\", \"{19368}\", \"{1957}\", \"{5232}\", \"{21159}\", \"{26079}\", \"{2312}\",\n\t\"{6877}\", \"{49124}\", \"{21070}\", \"{44925}\", \"{40240}\", \"{19241}\", \"{8601}\", \"{29218}\", \"{11029}\", \"{6381}\",\n\t\"{55292}\", \"{1565}\", \"{13752}\", \"{1388}\", \"{1379}\", \"{502}\", \"{1594}\", \"{41550}\", \"{3402}\", \"{27769}\",\n\t\"{48634}\", \"{2815}\", \"{5507}\", \"{54582}\", \"{819}\", \"{4910}\", \"{28508}\", \"{6886}\", \"{7491}\", \"{3919}\",\n\t\"{69471}\", \"{17380}\", \"{19600}\", \"{40601}\", \"{2753}\", \"{10389}\", \"{49565}\", \"{10564}\", \"{1124}\", \"{44692}\",\n\t\"{68390}\", \"{13313}\", \"{29659}\", \"{8240}\", \"{26923}\", \"{11468}\", \"{11499}\", \"{3043}\", \"{11274}\", \"{48275}\",\n\t\"{143}\", \"{1738}\", \"{41111}\", \"{17887}\", \"{9550}\", \"{28149}\", \"{10378}\", \"{8947}\", \"{45182}\", \"{5146}\",\n\t\"{12403}\", \"{25948}\", \"{48623}\", \"{2802}\", \"{3415}\", \"{52490}\", \"{1583}\", \"{13159}\", \"{40950}\", \"{515}\",\n\t\"{7486}\", \"{29908}\", \"{48}\", \"{6891}\", \"{55982}\", \"{4907}\", \"{5510}\", \"{1898}\", \"{1869}\", \"{19256}\",\n\t\"{21067}\", \"{44932}\", \"{6860}\", \"{49133}\", \"{53380}\", \"{2305}\", \"{13745}\", \"{62923}\", \"{309}\", \"{1572}\",\n\t\"{43420}\", \"{6396}\", \"{8616}\", \"{3209}\", \"{13518}\", \"{17890}\", \"{154}\", \"{68376}\", \"{11263}\", \"{10899}\",\n\t\"{43090}\", \"{3054}\", \"{12414}\", \"{69497}\", \"{24548}\", \"{5151}\", \"{42771}\", \"{8950}\", \"{9547}\", \"{2158}\",\n\t\"{27838}\", \"{10573}\", \"{2744}\", \"{42780}\", \"{19617}\", \"{12208}\", \"{69466}\", \"{17397}\", \"{26934}\", \"{43061}\",\n\t\"{3648}\", \"{8257}\", \"{68387}\", \"{13304}\", \"{1133}\", \"{748}\", \"{22378}\", \"{3761}\", \"{8593}\", \"{48557}\",\n\t\"{661}\", \"{4768}\", \"{41633}\", \"{4185}\", \"{9272}\", \"{8888}\", \"{15328}\", \"{2080}\", \"{51193}\", \"{5664}\",\n\t\"{12321}\", \"{5089}\", \"{5078}\", \"{21313}\", \"{5695}\", \"{40123}\", \"{2071}\", \"{23468}\", \"{8879}\", \"{9283}\",\n\t\"{1606}\", \"{50683}\", \"{4799}\", \"{690}\", \"{82859}\", \"{8562}\", \"{3790}\", \"{14438}\", \"{4329}\", \"{220}\",\n\t\"{18273}\", \"{19889}\", \"{3320}\", \"{14088}\", \"{47881}\", \"{7845}\", \"{5225}\", \"{40593}\", \"{41984}\", \"{1940}\",\n\t\"{43812}\", \"{9633}\", \"{39486}\", \"{6949}\", \"{15798}\", \"{2430}\", \"{3827}\", \"{49606}\", \"{12791}\", \"{5439}\",\n\t\"{40562}\", \"{927}\", \"{8123}\", \"{47870}\", \"{14079}\", \"{38396}\", \"{19878}\", \"{1247}\", \"{5922}\", \"{67964}\",\n\t\"{1587}\", \"{41543}\", \"{4418}\", \"{511}\", \"{48627}\", \"{2806}\", \"{3411}\", \"{7999}\", \"{55986}\", \"{4903}\",\n\t\"{5514}\", \"{18859}\", \"{7482}\", \"{15058}\", \"{46851}\", \"{6895}\", \"{6864}\", \"{49137}\", \"{23718}\", \"{2301}\",\n\t\"{40253}\", \"{19252}\", \"{21063}\", \"{5308}\", \"{7968}\", \"{6392}\", \"{8612}\", \"{42833}\", \"{13741}\", \"{62927}\",\n\t\"{55281}\", \"{1576}\", \"{11267}\", \"{9858}\", \"{22449}\", \"{3050}\", \"{41102}\", \"{17894}\", \"{150}\", \"{4059}\",\n\t\"{15419}\", \"{8954}\", \"{9543}\", \"{49587}\", \"{12410}\", \"{16998}\", \"{45191}\", \"{5155}\", \"{19613}\", \"{40612}\",\n\t\"{5749}\", \"{17393}\", \"{49576}\", \"{10577}\", \"{2740}\", \"{23359}\", \"{21939}\", \"{13300}\", \"{1137}\", \"{44681}\",\n\t\"{26930}\", \"{14309}\", \"{48297}\", \"{8253}\", \"{665}\", \"{68447}\", \"{13229}\", \"{4181}\", \"{49944}\", \"{3765}\",\n\t\"{8597}\", \"{3188}\", \"{24279}\", \"{5660}\", \"{12325}\", \"{101960}\", \"{9276}\", \"{2669}\", \"{42040}\", \"{2084}\",\n\t\"{2075}\", \"{56782}\", \"{2698}\", \"{9287}\", \"{69357}\", \"{21317}\", \"{5691}\", \"{12539}\", \"{3179}\", \"{8566}\",\n\t\"{3794}\", \"{43750}\", \"{1602}\", \"{25569}\", \"{105902}\", \"{694}\", \"{3324}\", \"{39999}\", \"{47885}\", \"{7841}\",\n\t\"{45913}\", \"{224}\", \"{18277}\", \"{13668}\", \"{2228}\", \"{9637}\", \"{39482}\", \"{42401}\", \"{5221}\", \"{12189}\",\n\t\"{41980}\", \"{1944}\", \"{12795}\", \"{41971}\", \"{12178}\", \"{923}\", \"{47382}\", \"{2434}\", \"{3823}\", \"{49602}\",\n\t\"{438}\", \"{1243}\", \"{5926}\", \"{67960}\", \"{8127}\", \"{3538}\", \"{28929}\", \"{38392}\", \"{49727}\", \"{3906}\",\n\t\"{40}\", \"{6899}\", \"{806}\", \"{40443}\", \"{5518}\", \"{1890}\", \"{6582}\", \"{14158}\", \"{47951}\", \"{7995}\",\n\t\"{54886}\", \"{5803}\", \"{1366}\", \"{19959}\", \"{41353}\", \"{1397}\", \"{301}\", \"{4208}\", \"{7964}\", \"{48037}\",\n\t\"{22618}\", \"{3201}\", \"{1861}\", \"{63827}\", \"{54381}\", \"{5304}\", \"{6868}\", \"{7292}\", \"{9712}\", \"{43933}\",\n\t\"{40002}\", \"{16994}\", \"{17583}\", \"{5159}\", \"{10367}\", \"{8958}\", \"{23549}\", \"{2150}\", \"{13510}\", \"{17898}\",\n\t\"{44091}\", \"{1727}\", \"{14519}\", \"{9854}\", \"{8443}\", \"{48487}\", \"{48476}\", \"{11477}\", \"{3640}\", \"{22259}\",\n\t\"{18713}\", \"{41712}\", \"{4649}\", \"{740}\", \"{27830}\", \"{15209}\", \"{49397}\", \"{9353}\", \"{20839}\", \"{12200}\",\n\t\"{5745}\", \"{45781}\", \"{48844}\", \"{2665}\", \"{9497}\", \"{2088}\", \"{21507}\", \"{69547}\", \"{12329}\", \"{5081}\",\n\t\"{8376}\", \"{3769}\", \"{43140}\", \"{3184}\", \"{669}\", \"{1012}\", \"{13225}\", \"{100860}\", \"{68257}\", \"{20217}\",\n\t\"{4791}\", \"{698}\", \"{3175}\", \"{57682}\", \"{3798}\", \"{8387}\", \"{5070}\", \"{24469}\", \"{104802}\", \"{12535}\",\n\t\"{2079}\", \"{9466}\", \"{2694}\", \"{42650}\", \"{44813}\", \"{12185}\", \"{19377}\", \"{1948}\", \"{2224}\", \"{38899}\",\n\t\"{46985}\", \"{6941}\", \"{1453}\", \"{228}\", \"{40880}\", \"{13664}\", \"{3328}\", \"{8737}\", \"{38582}\", \"{43501}\",\n\t\"{46282}\", \"{3534}\", \"{2923}\", \"{48702}\", \"{434}\", \"{40871}\", \"{13078}\", \"{18467}\", \"{9027}\", \"{2438}\",\n\t\"{29829}\", \"{39292}\", \"{12799}\", \"{5431}\", \"{4826}\", \"{66860}\", \"{802}\", \"{12059}\", \"{41850}\", \"{1894}\",\n\t\"{49723}\", \"{3902}\", \"{44}\", \"{53590}\", \"{54882}\", \"{5807}\", \"{1362}\", \"{519}\", \"{6586}\", \"{28808}\",\n\t\"{3419}\", \"{7991}\", \"{7960}\", \"{48033}\", \"{52280}\", \"{3205}\", \"{13749}\", \"{1393}\", \"{305}\", \"{45832}\",\n\t\"{42520}\", \"{7296}\", \"{9716}\", \"{2309}\", \"{1865}\", \"{63823}\", \"{24719}\", \"{5300}\", \"{10363}\", \"{11999}\",\n\t\"{42190}\", \"{2154}\", \"{12418}\", \"{16990}\", \"{17587}\", \"{69276}\", \"{43671}\", \"{9850}\", \"{8447}\", \"{3058}\",\n\t\"{13514}\", \"{68597}\", \"{158}\", \"{1723}\", \"{18717}\", \"{13308}\", \"{68566}\", \"{744}\", \"{26938}\", \"{11473}\",\n\t\"{3644}\", \"{43680}\", \"{69287}\", \"{12204}\", \"{5741}\", \"{24358}\", \"{27834}\", \"{42161}\", \"{2748}\", \"{9357}\",\n\t\"{21503}\", \"{5668}\", \"{40733}\", \"{5085}\", \"{23278}\", \"{2661}\", \"{9493}\", \"{49457}\", \"{50093}\", \"{1016}\",\n\t\"{13221}\", \"{4189}\", \"{8372}\", \"{9988}\", \"{14228}\", \"{3180}\", \"{3171}\", \"{22568}\", \"{9979}\", \"{8383}\",\n\t\"{4178}\", \"{20213}\", \"{4795}\", \"{41023}\", \"{83959}\", \"{9462}\", \"{2690}\", \"{15538}\", \"{5074}\", \"{51783}\",\n\t\"{5699}\", \"{12531}\", \"{2220}\", \"{15188}\", \"{46981}\", \"{6945}\", \"{5229}\", \"{12181}\", \"{19373}\", \"{18989}\",\n\t\"{42912}\", \"{8733}\", \"{38586}\", \"{7849}\", \"{1457}\", \"{41493}\", \"{40884}\", \"{13660}\", \"{430}\", \"{4539}\",\n\t\"{41462}\", \"{18463}\", \"{14698}\", \"{3530}\", \"{2927}\", \"{48706}\", \"{18978}\", \"{5435}\", \"{4822}\", \"{66864}\",\n\t\"{9023}\", \"{46970}\", \"{15179}\", \"{39296}\",\n}\n"
  },
  {
    "path": "maintnotifications/e2e/test_mode_test.go",
    "content": "package e2e\n\nimport (\n\t\"os\"\n\t\"time\"\n)\n\n// TestMode represents the type of test environment\ntype TestMode int\n\nconst (\n\t// TestModeProxyMock uses the local proxy mock for fast testing\n\tTestModeProxyMock TestMode = iota\n\t// TestModeRealFaultInjector uses the real fault injector with actual Redis Enterprise\n\tTestModeRealFaultInjector\n)\n\n// TestModeConfig holds configuration for a specific test mode\ntype TestModeConfig struct {\n\tMode TestMode\n\n\t// Timing configuration\n\tNotificationDelay        time.Duration // How long to wait after injecting a notification\n\tActionWaitTimeout        time.Duration // How long to wait for fault injector actions\n\tActionPollInterval       time.Duration // How often to poll for action status\n\tDatabaseReadyDelay       time.Duration // How long to wait for database to be ready\n\tConnectionEstablishDelay time.Duration // How long to wait for connections to establish\n\n\t// Test behavior configuration\n\tMaxClients           int  // Maximum number of clients to create (proxy mock should use 1)\n\tSkipMultiClientTests bool // Whether to skip tests that require multiple clients\n}\n\n// GetTestMode detects the current test mode based on environment variables\nfunc GetTestMode() TestMode {\n\t// If REDIS_ENDPOINTS_CONFIG_PATH is set, we're using real fault injector\n\tif os.Getenv(\"REDIS_ENDPOINTS_CONFIG_PATH\") != \"\" {\n\t\treturn TestModeRealFaultInjector\n\t}\n\n\t// If FAULT_INJECTOR_URL is set, we're using real fault injector\n\tif os.Getenv(\"FAULT_INJECTOR_URL\") != \"\" {\n\t\treturn TestModeRealFaultInjector\n\t}\n\n\t// Otherwise, we're using proxy mock\n\treturn TestModeProxyMock\n}\n\n// GetTestModeConfig returns the configuration for the current test mode\nfunc GetTestModeConfig() *TestModeConfig {\n\tmode := GetTestMode()\n\n\tswitch mode {\n\tcase TestModeProxyMock:\n\t\treturn &TestModeConfig{\n\t\t\tMode:                     TestModeProxyMock,\n\t\t\tNotificationDelay:        1 * time.Second,\n\t\t\tActionWaitTimeout:        10 * time.Second,\n\t\t\tActionPollInterval:       500 * time.Millisecond,\n\t\t\tDatabaseReadyDelay:       1 * time.Second,\n\t\t\tConnectionEstablishDelay: 500 * time.Millisecond,\n\t\t\tMaxClients:               1,\n\t\t\tSkipMultiClientTests:     true,\n\t\t}\n\n\tcase TestModeRealFaultInjector:\n\t\treturn &TestModeConfig{\n\t\t\tMode:                     TestModeRealFaultInjector,\n\t\t\tNotificationDelay:        30 * time.Second,\n\t\t\tActionWaitTimeout:        5 * time.Minute, // Real fault injector can take up to 5 minutes\n\t\t\tActionPollInterval:       500 * time.Millisecond,\n\t\t\tDatabaseReadyDelay:       10 * time.Second,\n\t\t\tConnectionEstablishDelay: 2 * time.Second,\n\t\t\tMaxClients:               3,\n\t\t\tSkipMultiClientTests:     false,\n\t\t}\n\n\tdefault:\n\t\t// Default to proxy mock for safety\n\t\treturn &TestModeConfig{\n\t\t\tMode:                     TestModeProxyMock,\n\t\t\tNotificationDelay:        1 * time.Second,\n\t\t\tActionWaitTimeout:        10 * time.Second,\n\t\t\tActionPollInterval:       500 * time.Millisecond,\n\t\t\tDatabaseReadyDelay:       1 * time.Second,\n\t\t\tConnectionEstablishDelay: 500 * time.Millisecond,\n\t\t\tMaxClients:               1,\n\t\t\tSkipMultiClientTests:     true,\n\t\t}\n\t}\n}\n\n// IsProxyMock returns true if running in proxy mock mode\nfunc (c *TestModeConfig) IsProxyMock() bool {\n\treturn c.Mode == TestModeProxyMock\n}\n\n// IsRealFaultInjector returns true if running with real fault injector\nfunc (c *TestModeConfig) IsRealFaultInjector() bool {\n\treturn c.Mode == TestModeRealFaultInjector\n}\n\n// String returns a human-readable name for the test mode\nfunc (m TestMode) String() string {\n\tswitch m {\n\tcase TestModeProxyMock:\n\t\treturn \"ProxyMock\"\n\tcase TestModeRealFaultInjector:\n\t\treturn \"RealFaultInjector\"\n\tdefault:\n\t\treturn \"Unknown\"\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/e2e/utils_test.go",
    "content": "package e2e\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n)\n\nfunc isTimeout(errMsg string) bool {\n\treturn contains(errMsg, \"i/o timeout\") ||\n\t\tcontains(errMsg, \"deadline exceeded\") ||\n\t\tcontains(errMsg, \"context deadline exceeded\")\n}\n\n// isTimeoutError checks if an error is a timeout error\nfunc isTimeoutError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\t// Check for various timeout error types\n\terrStr := err.Error()\n\treturn isTimeout(errStr)\n}\n\n// contains checks if a string contains a substring (case-insensitive)\nfunc contains(s, substr string) bool {\n\treturn len(s) >= len(substr) &&\n\t\t(s == substr ||\n\t\t\t(len(s) > len(substr) &&\n\t\t\t\t(s[:len(substr)] == substr ||\n\t\t\t\t\ts[len(s)-len(substr):] == substr ||\n\t\t\t\t\tcontainsSubstring(s, substr))))\n}\n\nfunc containsSubstring(s, substr string) bool {\n\tfor i := 0; i <= len(s)-len(substr); i++ {\n\t\tif s[i:i+len(substr)] == substr {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc printLog(group string, isError bool, format string, args ...interface{}) {\n\t_, filename, line, _ := runtime.Caller(2)\n\tfilename = filepath.Base(filename)\n\tfinalFormat := \"%s:%d [%s][%s] \" + format + \"\\n\"\n\tif isError {\n\t\tfinalFormat = \"%s:%d [%s][%s][ERROR] \" + format + \"\\n\"\n\t}\n\tts := time.Now().Format(\"15:04:05.000\")\n\targs = append([]interface{}{filename, line, ts, group}, args...)\n\tfmt.Printf(finalFormat, args...)\n}\n\nfunc actionOutputIfFailed(status *ActionStatusResponse) string {\n\tif status.Status != StatusFailed {\n\t\treturn \"\"\n\t}\n\tif status.Error != nil {\n\t\treturn fmt.Sprintf(\"%v\", status.Error)\n\t}\n\tif status.Output == nil {\n\t\treturn \"\"\n\t}\n\treturn fmt.Sprintf(\"%+v\", status.Output)\n}\n"
  },
  {
    "path": "maintnotifications/errors.go",
    "content": "package maintnotifications\n\nimport (\n\t\"errors\"\n\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n)\n\n// Configuration errors\nvar (\n\tErrInvalidRelaxedTimeout             = errors.New(logs.InvalidRelaxedTimeoutError())\n\tErrInvalidHandoffTimeout             = errors.New(logs.InvalidHandoffTimeoutError())\n\tErrInvalidHandoffWorkers             = errors.New(logs.InvalidHandoffWorkersError())\n\tErrInvalidHandoffQueueSize           = errors.New(logs.InvalidHandoffQueueSizeError())\n\tErrInvalidPostHandoffRelaxedDuration = errors.New(logs.InvalidPostHandoffRelaxedDurationError())\n\tErrInvalidEndpointType               = errors.New(logs.InvalidEndpointTypeError())\n\tErrInvalidMaintNotifications         = errors.New(logs.InvalidMaintNotificationsError())\n\tErrMaxHandoffRetriesReached          = errors.New(logs.MaxHandoffRetriesReachedError())\n\n\t// Configuration validation errors\n\n\t// ErrInvalidHandoffRetries is returned when the number of handoff retries is invalid\n\tErrInvalidHandoffRetries = errors.New(logs.InvalidHandoffRetriesError())\n)\n\n// Integration errors\nvar (\n\t// ErrInvalidClient is returned when the client does not support push notifications\n\tErrInvalidClient = errors.New(logs.InvalidClientError())\n)\n\n// Handoff errors\nvar (\n\t// ErrHandoffQueueFull is returned when the handoff queue is full\n\tErrHandoffQueueFull = errors.New(logs.HandoffQueueFullError())\n)\n\n// Notification errors\nvar (\n\t// ErrInvalidNotification is returned when a notification is in an invalid format\n\tErrInvalidNotification = errors.New(logs.InvalidNotificationError())\n)\n\n// connection handoff errors\nvar (\n\t// ErrConnectionMarkedForHandoff is returned when a connection is marked for handoff\n\t// and should not be used until the handoff is complete\n\tErrConnectionMarkedForHandoff = errors.New(logs.ConnectionMarkedForHandoffErrorMessage)\n\t// ErrConnectionMarkedForHandoffWithState is returned when a connection is marked for handoff\n\t// and should not be used until the handoff is complete\n\tErrConnectionMarkedForHandoffWithState = errors.New(logs.ConnectionMarkedForHandoffErrorMessage + \" with state\")\n\t// ErrConnectionInvalidHandoffState is returned when a connection is in an invalid state for handoff\n\tErrConnectionInvalidHandoffState = errors.New(logs.ConnectionInvalidHandoffStateErrorMessage)\n)\n\n// shutdown errors\nvar (\n\t// ErrShutdown is returned when the maintnotifications manager is shutdown\n\tErrShutdown = errors.New(logs.ShutdownError())\n)\n\n// circuit breaker errors\nvar (\n\t// ErrCircuitBreakerOpen is returned when the circuit breaker is open\n\tErrCircuitBreakerOpen = errors.New(logs.CircuitBreakerOpenErrorMessage)\n)\n\n// circuit breaker configuration errors\nvar (\n\t// ErrInvalidCircuitBreakerFailureThreshold is returned when the circuit breaker failure threshold is invalid\n\tErrInvalidCircuitBreakerFailureThreshold = errors.New(logs.InvalidCircuitBreakerFailureThresholdError())\n\t// ErrInvalidCircuitBreakerResetTimeout is returned when the circuit breaker reset timeout is invalid\n\tErrInvalidCircuitBreakerResetTimeout = errors.New(logs.InvalidCircuitBreakerResetTimeoutError())\n\t// ErrInvalidCircuitBreakerMaxRequests is returned when the circuit breaker max requests is invalid\n\tErrInvalidCircuitBreakerMaxRequests = errors.New(logs.InvalidCircuitBreakerMaxRequestsError())\n)\n"
  },
  {
    "path": "maintnotifications/example_hooks.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// contextKey is a custom type for context keys to avoid collisions\ntype contextKey string\n\nconst (\n\tstartTimeKey contextKey = \"maint_notif_start_time\"\n)\n\n// MetricsHook collects metrics about notification processing.\ntype MetricsHook struct {\n\tNotificationCounts map[string]int64\n\tProcessingTimes    map[string]time.Duration\n\tErrorCounts        map[string]int64\n\tHandoffCounts      int64 // Total handoffs initiated\n\tHandoffSuccesses   int64 // Successful handoffs\n\tHandoffFailures    int64 // Failed handoffs\n}\n\n// NewMetricsHook creates a new metrics collection hook.\nfunc NewMetricsHook() *MetricsHook {\n\treturn &MetricsHook{\n\t\tNotificationCounts: make(map[string]int64),\n\t\tProcessingTimes:    make(map[string]time.Duration),\n\t\tErrorCounts:        make(map[string]int64),\n\t}\n}\n\n// PreHook records the start time for processing metrics.\nfunc (mh *MetricsHook) PreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) {\n\tmh.NotificationCounts[notificationType]++\n\n\t// Log connection information if available\n\tif conn, ok := notificationCtx.Conn.(*pool.Conn); ok {\n\t\tinternal.Logger.Printf(ctx, logs.MetricsHookProcessingNotification(notificationType, conn.GetID()))\n\t}\n\n\t// Store start time in context for duration calculation\n\tstartTime := time.Now()\n\t_ = context.WithValue(ctx, startTimeKey, startTime) // Context not used further\n\n\treturn notification, true\n}\n\n// PostHook records processing completion and any errors.\nfunc (mh *MetricsHook) PostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) {\n\t// Calculate processing duration\n\tif startTime, ok := ctx.Value(startTimeKey).(time.Time); ok {\n\t\tduration := time.Since(startTime)\n\t\tmh.ProcessingTimes[notificationType] = duration\n\t}\n\n\t// Record errors\n\tif result != nil {\n\t\tmh.ErrorCounts[notificationType]++\n\n\t\t// Log error details with connection information\n\t\tif conn, ok := notificationCtx.Conn.(*pool.Conn); ok {\n\t\t\tinternal.Logger.Printf(ctx, logs.MetricsHookRecordedError(notificationType, conn.GetID(), result))\n\t\t}\n\t}\n}\n\n// GetMetrics returns a summary of collected metrics.\nfunc (mh *MetricsHook) GetMetrics() map[string]interface{} {\n\treturn map[string]interface{}{\n\t\t\"notification_counts\": mh.NotificationCounts,\n\t\t\"processing_times\":    mh.ProcessingTimes,\n\t\t\"error_counts\":        mh.ErrorCounts,\n\t}\n}\n\n// ExampleCircuitBreakerMonitor demonstrates how to monitor circuit breaker status\nfunc ExampleCircuitBreakerMonitor(poolHook *PoolHook) {\n\t// Get circuit breaker statistics\n\tstats := poolHook.GetCircuitBreakerStats()\n\n\tfor _, stat := range stats {\n\t\tfmt.Printf(\"Circuit Breaker for %s:\\n\", stat.Endpoint)\n\t\tfmt.Printf(\"  State: %s\\n\", stat.State)\n\t\tfmt.Printf(\"  Failures: %d\\n\", stat.Failures)\n\t\tfmt.Printf(\"  Last Failure: %v\\n\", stat.LastFailureTime)\n\t\tfmt.Printf(\"  Last Success: %v\\n\", stat.LastSuccessTime)\n\n\t\t// Alert if circuit breaker is open\n\t\tif stat.State.String() == \"open\" {\n\t\t\tfmt.Printf(\"  ⚠️  ALERT: Circuit breaker is OPEN for %s\\n\", stat.Endpoint)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/handoff_worker.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// PoolNameMain is the name used for the main connection pool in metrics.\nconst PoolNameMain = \"main\"\n\n// handoffWorkerManager manages background workers and queue for connection handoffs\ntype handoffWorkerManager struct {\n\t// Event-driven handoff support\n\thandoffQueue chan HandoffRequest // Queue for handoff requests\n\tshutdown     chan struct{}       // Shutdown signal\n\tshutdownOnce sync.Once           // Ensure clean shutdown\n\tworkerWg     sync.WaitGroup      // Track worker goroutines\n\n\t// On-demand worker management\n\tmaxWorkers     int\n\tactiveWorkers  atomic.Int32\n\tworkerTimeout  time.Duration // How long workers wait for work before exiting\n\tworkersScaling atomic.Bool\n\n\t// Simple state tracking\n\tpending sync.Map // map[uint64]int64 (connID -> seqID)\n\n\t// Configuration for the maintenance notifications\n\tconfig *Config\n\n\t// Pool hook reference for handoff processing\n\tpoolHook *PoolHook\n\n\t// Circuit breaker manager for endpoint failure handling\n\tcircuitBreakerManager *CircuitBreakerManager\n}\n\n// newHandoffWorkerManager creates a new handoff worker manager\nfunc newHandoffWorkerManager(config *Config, poolHook *PoolHook) *handoffWorkerManager {\n\treturn &handoffWorkerManager{\n\t\thandoffQueue:          make(chan HandoffRequest, config.HandoffQueueSize),\n\t\tshutdown:              make(chan struct{}),\n\t\tmaxWorkers:            config.MaxWorkers,\n\t\tactiveWorkers:         atomic.Int32{},   // Start with no workers - create on demand\n\t\tworkerTimeout:         15 * time.Second, // Workers exit after 15s of inactivity\n\t\tconfig:                config,\n\t\tpoolHook:              poolHook,\n\t\tcircuitBreakerManager: newCircuitBreakerManager(config),\n\t}\n}\n\n// getCurrentWorkers returns the current number of active workers (for testing)\nfunc (hwm *handoffWorkerManager) getCurrentWorkers() int {\n\treturn int(hwm.activeWorkers.Load())\n}\n\n// getPendingMap returns the pending map for testing purposes\nfunc (hwm *handoffWorkerManager) getPendingMap() *sync.Map {\n\treturn &hwm.pending\n}\n\n// getMaxWorkers returns the max workers for testing purposes\nfunc (hwm *handoffWorkerManager) getMaxWorkers() int {\n\treturn hwm.maxWorkers\n}\n\n// getHandoffQueue returns the handoff queue for testing purposes\nfunc (hwm *handoffWorkerManager) getHandoffQueue() chan HandoffRequest {\n\treturn hwm.handoffQueue\n}\n\n// getCircuitBreakerStats returns circuit breaker statistics for monitoring\nfunc (hwm *handoffWorkerManager) getCircuitBreakerStats() []CircuitBreakerStats {\n\treturn hwm.circuitBreakerManager.GetAllStats()\n}\n\n// resetCircuitBreakers resets all circuit breakers (useful for testing)\nfunc (hwm *handoffWorkerManager) resetCircuitBreakers() {\n\thwm.circuitBreakerManager.Reset()\n}\n\n// isHandoffPending returns true if the given connection has a pending handoff\nfunc (hwm *handoffWorkerManager) isHandoffPending(conn *pool.Conn) bool {\n\t_, pending := hwm.pending.Load(conn.GetID())\n\treturn pending\n}\n\n// ensureWorkerAvailable ensures at least one worker is available to process requests\n// Creates a new worker if needed and under the max limit\nfunc (hwm *handoffWorkerManager) ensureWorkerAvailable() {\n\tselect {\n\tcase <-hwm.shutdown:\n\t\treturn\n\tdefault:\n\t\tif hwm.workersScaling.CompareAndSwap(false, true) {\n\t\t\tdefer hwm.workersScaling.Store(false)\n\t\t\t// Check if we need a new worker\n\t\t\tcurrentWorkers := hwm.activeWorkers.Load()\n\t\t\tworkersWas := currentWorkers\n\t\t\tfor currentWorkers < int32(hwm.maxWorkers) {\n\t\t\t\thwm.workerWg.Add(1)\n\t\t\t\tgo hwm.onDemandWorker()\n\t\t\t\tcurrentWorkers++\n\t\t\t}\n\t\t\t// workersWas is always <= currentWorkers\n\t\t\t// currentWorkers will be maxWorkers, but if we have a worker that was closed\n\t\t\t// while we were creating new workers, just add the difference between\n\t\t\t// the currentWorkers and the number of workers we observed initially (i.e. the number of workers we created)\n\t\t\thwm.activeWorkers.Add(currentWorkers - workersWas)\n\t\t}\n\t}\n}\n\n// onDemandWorker processes handoff requests and exits when idle\nfunc (hwm *handoffWorkerManager) onDemandWorker() {\n\tdefer func() {\n\t\t// Handle panics to ensure proper cleanup\n\t\tif r := recover(); r != nil {\n\t\t\tinternal.Logger.Printf(context.Background(), logs.WorkerPanicRecovered(r))\n\t\t}\n\n\t\t// Decrement active worker count when exiting\n\t\thwm.activeWorkers.Add(-1)\n\t\thwm.workerWg.Done()\n\t}()\n\n\t// Create reusable timer to prevent timer leaks\n\ttimer := time.NewTimer(hwm.workerTimeout)\n\tdefer timer.Stop()\n\n\tfor {\n\t\t// Reset timer for next iteration\n\t\tif !timer.Stop() {\n\t\t\tselect {\n\t\t\tcase <-timer.C:\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t\ttimer.Reset(hwm.workerTimeout)\n\n\t\tselect {\n\t\tcase <-hwm.shutdown:\n\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\tinternal.Logger.Printf(context.Background(), logs.WorkerExitingDueToShutdown())\n\t\t\t}\n\t\t\treturn\n\t\tcase <-timer.C:\n\t\t\t// Worker has been idle for too long, exit to save resources\n\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\tinternal.Logger.Printf(context.Background(), logs.WorkerExitingDueToInactivityTimeout(hwm.workerTimeout))\n\t\t\t}\n\t\t\treturn\n\t\tcase request := <-hwm.handoffQueue:\n\t\t\t// Check for shutdown before processing\n\t\t\tselect {\n\t\t\tcase <-hwm.shutdown:\n\t\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.WorkerExitingDueToShutdownWhileProcessing())\n\t\t\t\t}\n\t\t\t\t// Clean up the request before exiting\n\t\t\t\thwm.pending.Delete(request.ConnID)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\t// Process the request\n\t\t\t\thwm.processHandoffRequest(request)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// processHandoffRequest processes a single handoff request\nfunc (hwm *handoffWorkerManager) processHandoffRequest(request HandoffRequest) {\n\tif internal.LogLevel.InfoOrAbove() {\n\t\tinternal.Logger.Printf(context.Background(), logs.HandoffStarted(request.Conn.GetID(), request.Endpoint))\n\t}\n\n\t// Create a context with handoff timeout from config\n\thandoffTimeout := 15 * time.Second // Default timeout\n\tif hwm.config != nil && hwm.config.HandoffTimeout > 0 {\n\t\thandoffTimeout = hwm.config.HandoffTimeout\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), handoffTimeout)\n\tdefer cancel()\n\n\t// Create a context that also respects the shutdown signal\n\tshutdownCtx, shutdownCancel := context.WithCancel(ctx)\n\tdefer shutdownCancel()\n\n\t// Monitor shutdown signal in a separate goroutine\n\tgo func() {\n\t\tselect {\n\t\tcase <-hwm.shutdown:\n\t\t\tshutdownCancel()\n\t\tcase <-shutdownCtx.Done():\n\t\t}\n\t}()\n\n\t// Perform the handoff with cancellable context\n\tshouldRetry, err := hwm.performConnectionHandoff(shutdownCtx, request.Conn)\n\tminRetryBackoff := 500 * time.Millisecond\n\tif err != nil {\n\t\tif shouldRetry {\n\t\t\tnow := time.Now()\n\t\t\tdeadline, ok := shutdownCtx.Deadline()\n\t\t\tthirdOfTimeout := handoffTimeout / 3\n\t\t\tif !ok || deadline.Before(now) {\n\t\t\t\t// wait half the timeout before retrying if no deadline or deadline has passed\n\t\t\t\tdeadline = now.Add(thirdOfTimeout)\n\t\t\t}\n\t\t\tafterTime := deadline.Sub(now)\n\t\t\tif afterTime < minRetryBackoff {\n\t\t\t\tafterTime = minRetryBackoff\n\t\t\t}\n\n\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\t// Get current retry count for better logging\n\t\t\t\tcurrentRetries := request.Conn.HandoffRetries()\n\t\t\t\tmaxRetries := 3 // Default fallback\n\t\t\t\tif hwm.config != nil {\n\t\t\t\t\tmaxRetries = hwm.config.MaxHandoffRetries\n\t\t\t\t}\n\t\t\t\tinternal.Logger.Printf(context.Background(), logs.HandoffFailed(request.ConnID, request.Endpoint, currentRetries, maxRetries, err))\n\t\t\t}\n\t\t\t// Schedule retry - keep connection in pending map until retry is queued\n\t\t\ttime.AfterFunc(afterTime, func() {\n\t\t\t\tif err := hwm.queueHandoff(request.Conn); err != nil {\n\t\t\t\t\tif internal.LogLevel.WarnOrAbove() {\n\t\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.CannotQueueHandoffForRetry(err))\n\t\t\t\t\t}\n\t\t\t\t\t// Failed to queue retry - remove from pending and close connection\n\t\t\t\t\thwm.pending.Delete(request.Conn.GetID())\n\t\t\t\t\thwm.closeConnFromRequest(context.Background(), request, err)\n\t\t\t\t} else {\n\t\t\t\t\t// Successfully queued retry - remove from pending (will be re-added by queueHandoff)\n\t\t\t\t\thwm.pending.Delete(request.Conn.GetID())\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn\n\t\t} else {\n\t\t\t// Won't retry - remove from pending and close connection\n\t\t\thwm.pending.Delete(request.Conn.GetID())\n\t\t\tgo hwm.closeConnFromRequest(ctx, request, err)\n\t\t}\n\n\t\t// Clear handoff state if not returned for retry\n\t\tseqID := request.Conn.GetMovingSeqID()\n\t\tconnID := request.Conn.GetID()\n\t\tif hwm.poolHook.operationsManager != nil {\n\t\t\thwm.poolHook.operationsManager.UntrackOperationWithConnID(seqID, connID)\n\t\t}\n\t} else {\n\t\t// Success - remove from pending map\n\t\thwm.pending.Delete(request.Conn.GetID())\n\t}\n}\n\n// queueHandoff queues a handoff request for processing\n// if err is returned, connection will be removed from pool\nfunc (hwm *handoffWorkerManager) queueHandoff(conn *pool.Conn) error {\n\t// Get handoff info atomically to prevent race conditions\n\tshouldHandoff, endpoint, seqID := conn.GetHandoffInfo()\n\n\t// on retries the connection will not be marked for handoff, but it will have retries > 0\n\t// if shouldHandoff is false and retries is 0, then we are not retrying and not do a handoff\n\tif !shouldHandoff && conn.HandoffRetries() == 0 {\n\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\tinternal.Logger.Printf(context.Background(), logs.ConnectionNotMarkedForHandoff(conn.GetID()))\n\t\t}\n\t\treturn errors.New(logs.ConnectionNotMarkedForHandoffError(conn.GetID()))\n\t}\n\n\t// Create handoff request with atomically retrieved data\n\trequest := HandoffRequest{\n\t\tConn:     conn,\n\t\tConnID:   conn.GetID(),\n\t\tEndpoint: endpoint,\n\t\tSeqID:    seqID,\n\t\tPool:     hwm.poolHook.pool, // Include pool for connection removal on failure\n\t}\n\n\tselect {\n\t// priority to shutdown\n\tcase <-hwm.shutdown:\n\t\treturn ErrShutdown\n\tdefault:\n\t\tselect {\n\t\tcase <-hwm.shutdown:\n\t\t\treturn ErrShutdown\n\t\tcase hwm.handoffQueue <- request:\n\t\t\t// Store in pending map\n\t\t\thwm.pending.Store(request.ConnID, request.SeqID)\n\t\t\t// Ensure we have a worker to process this request\n\t\t\thwm.ensureWorkerAvailable()\n\t\t\treturn nil\n\t\tdefault:\n\t\t\tselect {\n\t\t\tcase <-hwm.shutdown:\n\t\t\t\treturn ErrShutdown\n\t\t\tcase hwm.handoffQueue <- request:\n\t\t\t\t// Store in pending map\n\t\t\t\thwm.pending.Store(request.ConnID, request.SeqID)\n\t\t\t\t// Ensure we have a worker to process this request\n\t\t\t\thwm.ensureWorkerAvailable()\n\t\t\t\treturn nil\n\t\t\tcase <-time.After(100 * time.Millisecond): // give workers a chance to process\n\t\t\t\t// Queue is full - log and attempt scaling\n\t\t\t\tqueueLen := len(hwm.handoffQueue)\n\t\t\t\tqueueCap := cap(hwm.handoffQueue)\n\t\t\t\tif internal.LogLevel.WarnOrAbove() {\n\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.HandoffQueueFull(queueLen, queueCap))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Ensure we have workers available to handle the load\n\thwm.ensureWorkerAvailable()\n\treturn ErrHandoffQueueFull\n}\n\n// shutdownWorkers gracefully shuts down the worker manager, waiting for workers to complete\nfunc (hwm *handoffWorkerManager) shutdownWorkers(ctx context.Context) error {\n\thwm.shutdownOnce.Do(func() {\n\t\tclose(hwm.shutdown)\n\t\t// workers will exit when they finish their current request\n\n\t\t// Shutdown circuit breaker manager cleanup goroutine\n\t\tif hwm.circuitBreakerManager != nil {\n\t\t\thwm.circuitBreakerManager.Shutdown()\n\t\t}\n\t})\n\n\t// Wait for workers to complete\n\tdone := make(chan struct{})\n\tgo func() {\n\t\thwm.workerWg.Wait()\n\t\tclose(done)\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}\n\n// performConnectionHandoff performs the actual connection handoff\n// When error is returned, the connection handoff should be retried if err is not ErrMaxHandoffRetriesReached\nfunc (hwm *handoffWorkerManager) performConnectionHandoff(ctx context.Context, conn *pool.Conn) (shouldRetry bool, err error) {\n\t// Clear handoff state after successful handoff\n\tconnID := conn.GetID()\n\n\tnewEndpoint := conn.GetHandoffEndpoint()\n\tif newEndpoint == \"\" {\n\t\treturn false, ErrConnectionInvalidHandoffState\n\t}\n\n\t// Use circuit breaker to protect against failing endpoints\n\tcircuitBreaker := hwm.circuitBreakerManager.GetCircuitBreaker(newEndpoint)\n\n\t// Check if circuit breaker is open before attempting handoff\n\tif circuitBreaker.IsOpen() {\n\t\tinternal.Logger.Printf(ctx, logs.CircuitBreakerOpen(connID, newEndpoint))\n\t\treturn false, ErrCircuitBreakerOpen // Don't retry when circuit breaker is open\n\t}\n\n\t// Perform the handoff\n\tshouldRetry, err = hwm.performHandoffInternal(ctx, conn, newEndpoint, connID)\n\n\t// Update circuit breaker based on result\n\tif err != nil {\n\t\t// Only track dial/network errors in circuit breaker, not initialization errors\n\t\tif shouldRetry {\n\t\t\tcircuitBreaker.recordFailure()\n\t\t}\n\t\treturn shouldRetry, err\n\t}\n\n\t// Success - record in circuit breaker\n\tcircuitBreaker.recordSuccess()\n\treturn false, nil\n}\n\n// performHandoffInternal performs the actual handoff logic (extracted for circuit breaker integration)\nfunc (hwm *handoffWorkerManager) performHandoffInternal(\n\tctx context.Context,\n\tconn *pool.Conn,\n\tnewEndpoint string,\n\tconnID uint64,\n) (shouldRetry bool, err error) {\n\tretries := conn.IncrementAndGetHandoffRetries(1)\n\tinternal.Logger.Printf(ctx, logs.HandoffRetryAttempt(connID, retries, newEndpoint, conn.RemoteAddr().String()))\n\tmaxRetries := 3 // Default fallback\n\tif hwm.config != nil {\n\t\tmaxRetries = hwm.config.MaxHandoffRetries\n\t}\n\n\tif retries > maxRetries {\n\t\tif internal.LogLevel.WarnOrAbove() {\n\t\t\tinternal.Logger.Printf(ctx, logs.ReachedMaxHandoffRetries(connID, newEndpoint, maxRetries))\n\t\t}\n\t\t// won't retry on ErrMaxHandoffRetriesReached\n\t\treturn false, ErrMaxHandoffRetriesReached\n\t}\n\n\t// Create endpoint-specific dialer\n\tendpointDialer := hwm.createEndpointDialer(newEndpoint)\n\n\t// Create new connection to the new endpoint\n\tnewNetConn, err := endpointDialer(ctx)\n\tif err != nil {\n\t\tinternal.Logger.Printf(ctx, logs.FailedToDialNewEndpoint(connID, newEndpoint, err))\n\t\t// will retry\n\t\t// Maybe a network error - retry after a delay\n\t\treturn true, err\n\t}\n\n\t// Get the old connection\n\toldConn := conn.GetNetConn()\n\n\t// Apply relaxed timeout to the new connection for the configured post-handoff duration\n\t// This gives the new connection more time to handle operations during cluster transition\n\t// Setting this here (before initing the connection) ensures that the connection is going\n\t// to use the relaxed timeout for the first operation (auth/ACL select)\n\tif hwm.config != nil && hwm.config.PostHandoffRelaxedDuration > 0 {\n\t\trelaxedTimeout := hwm.config.RelaxedTimeout\n\t\t// Set relaxed timeout with deadline - no background goroutine needed\n\t\tdeadline := time.Now().Add(hwm.config.PostHandoffRelaxedDuration)\n\t\tconn.SetRelaxedTimeoutWithDeadline(relaxedTimeout, relaxedTimeout, deadline)\n\n\t\t// Record relaxed timeout metric (post-handoff)\n\t\tif relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil {\n\t\t\trelaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, \"HANDOFF\")\n\t\t}\n\n\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\tinternal.Logger.Printf(context.Background(), logs.ApplyingRelaxedTimeoutDueToPostHandoff(connID, relaxedTimeout, deadline.Format(\"15:04:05.000\")))\n\t\t}\n\t}\n\n\t// Replace the connection and execute initialization\n\terr = conn.SetNetConnAndInitConn(ctx, newNetConn)\n\tif err != nil {\n\t\t// won't retry\n\t\t// Initialization failed - remove the connection\n\t\treturn false, err\n\t}\n\tdefer func() {\n\t\tif oldConn != nil {\n\t\t\toldConn.Close()\n\t\t}\n\t}()\n\n\t// Clear handoff state will:\n\t// - set the connection as usable again\n\t// - clear the handoff state (shouldHandoff, endpoint, seqID)\n\t// - reset the handoff retries to 0\n\t// Note: Theoretically there may be a short window where the connection is in the pool\n\t// and IDLE (initConn completed) but still has handoff state set.\n\tconn.ClearHandoffState()\n\tinternal.Logger.Printf(ctx, logs.HandoffSucceeded(connID, newEndpoint))\n\n\t// successfully completed the handoff, no retry needed and no error\n\t// Notify metrics: connection handoff succeeded\n\tif handoffCallback := pool.GetMetricConnectionHandoffCallback(); handoffCallback != nil {\n\t\thandoffCallback(ctx, conn, PoolNameMain)\n\t}\n\n\treturn false, nil\n}\n\n// createEndpointDialer creates a dialer function that connects to a specific endpoint\nfunc (hwm *handoffWorkerManager) createEndpointDialer(endpoint string) func(context.Context) (net.Conn, error) {\n\treturn func(ctx context.Context) (net.Conn, error) {\n\t\t// Parse endpoint to extract host and port\n\t\thost, port, err := net.SplitHostPort(endpoint)\n\t\tif err != nil {\n\t\t\t// If no port specified, assume default Redis port\n\t\t\thost = endpoint\n\t\t\tif port == \"\" {\n\t\t\t\tport = \"6379\"\n\t\t\t}\n\t\t}\n\n\t\t// Use the base dialer to connect to the new endpoint\n\t\treturn hwm.poolHook.baseDialer(ctx, hwm.poolHook.network, net.JoinHostPort(host, port))\n\t}\n}\n\n// closeConnFromRequest closes the connection and logs the reason\nfunc (hwm *handoffWorkerManager) closeConnFromRequest(ctx context.Context, request HandoffRequest, err error) {\n\tpooler := request.Pool\n\tconn := request.Conn\n\n\t// Clear handoff state before closing\n\tconn.ClearHandoffState()\n\n\tif pooler != nil {\n\t\t// Use RemoveWithoutTurn instead of Remove to avoid freeing a turn that we don't have.\n\t\t// The handoff worker doesn't call Get(), so it doesn't have a turn to free.\n\t\t// Remove() is meant to be called after Get() and frees a turn.\n\t\t// RemoveWithoutTurn() removes and closes the connection without affecting the queue.\n\t\tpooler.RemoveWithoutTurn(ctx, conn, err)\n\t\tif internal.LogLevel.WarnOrAbove() {\n\t\t\tinternal.Logger.Printf(ctx, logs.RemovingConnectionFromPool(conn.GetID(), err))\n\t\t}\n\t} else {\n\t\terrClose := conn.Close() // Close the connection if no pool provided\n\t\tif errClose != nil {\n\t\t\tinternal.Logger.Printf(ctx, \"redis: failed to close connection: %v\", errClose)\n\t\t}\n\t\tif internal.LogLevel.WarnOrAbove() {\n\t\t\tinternal.Logger.Printf(ctx, logs.NoPoolProvidedCannotRemove(conn.GetID(), err))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/hooks.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"slices\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// LoggingHook is an example hook implementation that logs all notifications.\ntype LoggingHook struct {\n\tLogLevel int // 0=Error, 1=Warn, 2=Info, 3=Debug\n}\n\n// PreHook logs the notification before processing and allows modification.\nfunc (lh *LoggingHook) PreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) {\n\tif lh.LogLevel >= 2 { // Info level\n\t\t// Log the notification type and content\n\t\tconnID := uint64(0)\n\t\tif conn, ok := notificationCtx.Conn.(*pool.Conn); ok {\n\t\t\tconnID = conn.GetID()\n\t\t}\n\t\tseqID := int64(0)\n\t\tif slices.Contains(maintenanceNotificationTypes, notificationType) {\n\t\t\t// seqID is the second element in the notification array\n\t\t\tif len(notification) > 1 {\n\t\t\t\tif parsedSeqID, ok := notification[1].(int64); !ok {\n\t\t\t\t\tseqID = 0\n\t\t\t\t} else {\n\t\t\t\t\tseqID = parsedSeqID\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t\tinternal.Logger.Printf(ctx, logs.ProcessingNotification(connID, seqID, notificationType, notification))\n\t}\n\treturn notification, true // Continue processing with unmodified notification\n}\n\n// PostHook logs the result after processing.\nfunc (lh *LoggingHook) PostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) {\n\tconnID := uint64(0)\n\tif conn, ok := notificationCtx.Conn.(*pool.Conn); ok {\n\t\tconnID = conn.GetID()\n\t}\n\tif result != nil && lh.LogLevel >= 1 { // Warning level\n\t\tinternal.Logger.Printf(ctx, logs.ProcessingNotificationFailed(connID, notificationType, result, notification))\n\t} else if lh.LogLevel >= 3 { // Debug level\n\t\tinternal.Logger.Printf(ctx, logs.ProcessingNotificationSucceeded(connID, notificationType))\n\t}\n}\n\n// NewLoggingHook creates a new logging hook with the specified log level.\n// Log levels: 0=Error, 1=Warn, 2=Info, 3=Debug\nfunc NewLoggingHook(logLevel int) *LoggingHook {\n\treturn &LoggingHook{LogLevel: logLevel}\n}\n"
  },
  {
    "path": "maintnotifications/manager.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/interfaces\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// Push notification type constants for maintenance\nconst (\n\tNotificationMoving      = \"MOVING\"       // Per-connection handoff notification\n\tNotificationMigrating   = \"MIGRATING\"    // Per-connection migration start notification - relaxes timeouts\n\tNotificationMigrated    = \"MIGRATED\"     // Per-connection migration complete notification - clears relaxed timeouts\n\tNotificationFailingOver = \"FAILING_OVER\" // Per-connection failover start notification - relaxes timeouts\n\tNotificationFailedOver  = \"FAILED_OVER\"  // Per-connection failover complete notification - clears relaxed timeouts\n\tNotificationSMigrating  = \"SMIGRATING\"   // Cluster slot migrating notification - relaxes timeouts\n\tNotificationSMigrated   = \"SMIGRATED\"    // Cluster slot migrated notification -  unrelaxes timeouts and triggers cluster state reload\n)\n\n// maintenanceNotificationTypes contains all notification types that maintenance handles\nvar maintenanceNotificationTypes = []string{\n\tNotificationMoving,\n\tNotificationMigrating,\n\tNotificationMigrated,\n\tNotificationFailingOver,\n\tNotificationFailedOver,\n\tNotificationSMigrating,\n\tNotificationSMigrated,\n}\n\n// NotificationHook is called before and after notification processing\n// PreHook can modify the notification and return false to skip processing\n// PostHook is called after successful processing\ntype NotificationHook interface {\n\tPreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool)\n\tPostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error)\n}\n\n// MovingOperationKey provides a unique key for tracking MOVING operations\n// that combines sequence ID with connection identifier to handle duplicate\n// sequence IDs across multiple connections to the same node.\ntype MovingOperationKey struct {\n\tSeqID  int64  // Sequence ID from MOVING notification\n\tConnID uint64 // Unique connection identifier\n}\n\n// String returns a string representation of the key for debugging\nfunc (k MovingOperationKey) String() string {\n\treturn fmt.Sprintf(\"seq:%d-conn:%d\", k.SeqID, k.ConnID)\n}\n\n// Manager provides a simplified upgrade functionality with hooks and atomic state.\ntype Manager struct {\n\tclient  interfaces.ClientInterface\n\tconfig  *Config\n\toptions interfaces.OptionsInterface\n\tpool    pool.Pooler\n\n\t// MOVING operation tracking - using sync.Map for better concurrent performance\n\tactiveMovingOps sync.Map // map[MovingOperationKey]*MovingOperation\n\n\t// SMIGRATED notification deduplication - tracks processed SeqIDs\n\t// Multiple connections may receive the same SMIGRATED notification\n\tprocessedSMigratedSeqIDs sync.Map // map[int64]bool\n\n\t// Atomic state tracking - no locks needed for state queries\n\tactiveOperationCount atomic.Int64 // Number of active operations\n\tclosed               atomic.Bool  // Manager closed state\n\n\t// Notification hooks for extensibility\n\thooks        []NotificationHook\n\thooksMu      sync.RWMutex // Protects hooks slice\n\tpoolHooksRef *PoolHook\n\n\t// Cluster state reload callback for SMIGRATED notifications\n\tclusterStateReloadCallback ClusterStateReloadCallback\n}\n\n// MovingOperation tracks an active MOVING operation.\ntype MovingOperation struct {\n\tSeqID       int64\n\tNewEndpoint string\n\tStartTime   time.Time\n\tDeadline    time.Time\n}\n\n// ClusterStateReloadCallback is a callback function that triggers cluster state reload.\n// This is used by node clients to notify their parent ClusterClient about SMIGRATED notifications.\n// The hostPort parameter indicates the destination node (e.g., \"127.0.0.1:6379\").\n// The slotRanges parameter contains the migrated slots (e.g., [\"1234\", \"5000-6000\"]).\n// Currently, implementations typically reload the entire cluster state, but in the future\n// this could be optimized to reload only the specific slots.\ntype ClusterStateReloadCallback func(ctx context.Context, hostPort string, slotRanges []string)\n\n// NewManager creates a new simplified manager.\nfunc NewManager(client interfaces.ClientInterface, pool pool.Pooler, config *Config) (*Manager, error) {\n\tif client == nil {\n\t\treturn nil, ErrInvalidClient\n\t}\n\n\thm := &Manager{\n\t\tclient:  client,\n\t\tpool:    pool,\n\t\toptions: client.GetOptions(),\n\t\tconfig:  config.Clone(),\n\t\thooks:   make([]NotificationHook, 0),\n\t}\n\n\t// Set up push notification handling\n\tif err := hm.setupPushNotifications(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn hm, nil\n}\n\n// GetPoolHook creates a pool hook with a custom dialer.\nfunc (hm *Manager) InitPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error)) {\n\tpoolHook := hm.createPoolHook(baseDialer)\n\thm.pool.AddPoolHook(poolHook)\n}\n\n// setupPushNotifications sets up push notification handling by registering with the client's processor.\nfunc (hm *Manager) setupPushNotifications() error {\n\tprocessor := hm.client.GetPushProcessor()\n\tif processor == nil {\n\t\treturn ErrInvalidClient // Client doesn't support push notifications\n\t}\n\n\t// Create our notification handler\n\thandler := &NotificationHandler{manager: hm, operationsManager: hm}\n\n\t// Register handlers for all upgrade notifications with the client's processor\n\tfor _, notificationType := range maintenanceNotificationTypes {\n\t\tif err := processor.RegisterHandler(notificationType, handler, true); err != nil {\n\t\t\treturn errors.New(logs.FailedToRegisterHandler(notificationType, err))\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// TrackMovingOperationWithConnID starts a new MOVING operation with a specific connection ID.\nfunc (hm *Manager) TrackMovingOperationWithConnID(ctx context.Context, newEndpoint string, deadline time.Time, seqID int64, connID uint64) error {\n\t// Create composite key\n\tkey := MovingOperationKey{\n\t\tSeqID:  seqID,\n\t\tConnID: connID,\n\t}\n\n\t// Create MOVING operation record\n\tmovingOp := &MovingOperation{\n\t\tSeqID:       seqID,\n\t\tNewEndpoint: newEndpoint,\n\t\tStartTime:   time.Now(),\n\t\tDeadline:    deadline,\n\t}\n\n\t// Use LoadOrStore for atomic check-and-set operation\n\tif _, loaded := hm.activeMovingOps.LoadOrStore(key, movingOp); loaded {\n\t\t// Duplicate MOVING notification, ignore\n\t\tif internal.LogLevel.DebugOrAbove() { // Debug level\n\t\t\tinternal.Logger.Printf(context.Background(), logs.DuplicateMovingOperation(connID, newEndpoint, seqID))\n\t\t}\n\t\treturn nil\n\t}\n\tif internal.LogLevel.DebugOrAbove() { // Debug level\n\t\tinternal.Logger.Printf(context.Background(), logs.TrackingMovingOperation(connID, newEndpoint, seqID))\n\t}\n\n\t// Increment active operation count atomically\n\thm.activeOperationCount.Add(1)\n\n\treturn nil\n}\n\n// UntrackOperationWithConnID completes a MOVING operation with a specific connection ID.\nfunc (hm *Manager) UntrackOperationWithConnID(seqID int64, connID uint64) {\n\t// Create composite key\n\tkey := MovingOperationKey{\n\t\tSeqID:  seqID,\n\t\tConnID: connID,\n\t}\n\n\t// Remove from active operations atomically\n\tif _, loaded := hm.activeMovingOps.LoadAndDelete(key); loaded {\n\t\tif internal.LogLevel.DebugOrAbove() { // Debug level\n\t\t\tinternal.Logger.Printf(context.Background(), logs.UntrackingMovingOperation(connID, seqID))\n\t\t}\n\t\t// Decrement active operation count only if operation existed\n\t\thm.activeOperationCount.Add(-1)\n\t} else {\n\t\tif internal.LogLevel.DebugOrAbove() { // Debug level\n\t\t\tinternal.Logger.Printf(context.Background(), logs.OperationNotTracked(connID, seqID))\n\t\t}\n\t}\n}\n\n// GetActiveMovingOperations returns active operations with composite keys.\n// WARNING: This method creates a new map and copies all operations on every call.\n// Use sparingly, especially in hot paths or high-frequency logging.\nfunc (hm *Manager) GetActiveMovingOperations() map[MovingOperationKey]*MovingOperation {\n\tresult := make(map[MovingOperationKey]*MovingOperation)\n\n\t// Iterate over sync.Map to build result\n\thm.activeMovingOps.Range(func(key, value interface{}) bool {\n\t\tk := key.(MovingOperationKey)\n\t\top := value.(*MovingOperation)\n\n\t\t// Create a copy to avoid sharing references\n\t\tresult[k] = &MovingOperation{\n\t\t\tSeqID:       op.SeqID,\n\t\t\tNewEndpoint: op.NewEndpoint,\n\t\t\tStartTime:   op.StartTime,\n\t\t\tDeadline:    op.Deadline,\n\t\t}\n\t\treturn true // Continue iteration\n\t})\n\n\treturn result\n}\n\n// IsHandoffInProgress returns true if any handoff is in progress.\n// Uses atomic counter for lock-free operation.\nfunc (hm *Manager) IsHandoffInProgress() bool {\n\treturn hm.activeOperationCount.Load() > 0\n}\n\n// GetActiveOperationCount returns the number of active operations.\n// Uses atomic counter for lock-free operation.\nfunc (hm *Manager) GetActiveOperationCount() int64 {\n\treturn hm.activeOperationCount.Load()\n}\n\n// MarkSMigratedSeqIDProcessed attempts to mark a SMIGRATED SeqID as processed.\n// Returns true if this is the first time processing this SeqID (should process),\n// false if it was already processed (should skip).\n// This prevents duplicate processing when multiple connections receive the same notification.\nfunc (hm *Manager) MarkSMigratedSeqIDProcessed(seqID int64) bool {\n\t_, alreadyProcessed := hm.processedSMigratedSeqIDs.LoadOrStore(seqID, true)\n\treturn !alreadyProcessed // Return true if NOT already processed\n}\n\n// Close closes the manager.\nfunc (hm *Manager) Close() error {\n\t// Use atomic operation for thread-safe close check\n\tif !hm.closed.CompareAndSwap(false, true) {\n\t\treturn nil // Already closed\n\t}\n\n\t// Shutdown the pool hook if it exists\n\tif hm.poolHooksRef != nil {\n\t\t// Use a timeout to prevent hanging indefinitely\n\t\tshutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\n\t\terr := hm.poolHooksRef.Shutdown(shutdownCtx)\n\t\tif err != nil {\n\t\t\t// was not able to close pool hook, keep closed state false\n\t\t\thm.closed.Store(false)\n\t\t\treturn err\n\t\t}\n\t\t// Remove the pool hook from the pool\n\t\tif hm.pool != nil {\n\t\t\thm.pool.RemovePoolHook(hm.poolHooksRef)\n\t\t}\n\t}\n\n\t// Clear all active operations\n\thm.activeMovingOps.Range(func(key, value interface{}) bool {\n\t\thm.activeMovingOps.Delete(key)\n\t\treturn true\n\t})\n\n\t// Reset counter\n\thm.activeOperationCount.Store(0)\n\n\treturn nil\n}\n\n// GetState returns current state using atomic counter for lock-free operation.\nfunc (hm *Manager) GetState() State {\n\tif hm.activeOperationCount.Load() > 0 {\n\t\treturn StateMoving\n\t}\n\treturn StateIdle\n}\n\n// processPreHooks calls all pre-hooks and returns the modified notification and whether to continue processing.\nfunc (hm *Manager) processPreHooks(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) {\n\thm.hooksMu.RLock()\n\tdefer hm.hooksMu.RUnlock()\n\n\tcurrentNotification := notification\n\n\tfor _, hook := range hm.hooks {\n\t\tmodifiedNotification, shouldContinue := hook.PreHook(ctx, notificationCtx, notificationType, currentNotification)\n\t\tif !shouldContinue {\n\t\t\treturn modifiedNotification, false\n\t\t}\n\t\tcurrentNotification = modifiedNotification\n\t}\n\n\treturn currentNotification, true\n}\n\n// processPostHooks calls all post-hooks with the processing result.\nfunc (hm *Manager) processPostHooks(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) {\n\thm.hooksMu.RLock()\n\tdefer hm.hooksMu.RUnlock()\n\n\tfor _, hook := range hm.hooks {\n\t\thook.PostHook(ctx, notificationCtx, notificationType, notification, result)\n\t}\n}\n\n// createPoolHook creates a pool hook with this manager already set.\nfunc (hm *Manager) createPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error)) *PoolHook {\n\tif hm.poolHooksRef != nil {\n\t\treturn hm.poolHooksRef\n\t}\n\t// Get pool size from client options for better worker defaults\n\tpoolSize := 0\n\tif hm.options != nil {\n\t\tpoolSize = hm.options.GetPoolSize()\n\t}\n\n\thm.poolHooksRef = NewPoolHookWithPoolSize(baseDialer, hm.options.GetNetwork(), hm.config, hm, poolSize)\n\thm.poolHooksRef.SetPool(hm.pool)\n\n\treturn hm.poolHooksRef\n}\n\nfunc (hm *Manager) AddNotificationHook(notificationHook NotificationHook) {\n\thm.hooksMu.Lock()\n\tdefer hm.hooksMu.Unlock()\n\thm.hooks = append(hm.hooks, notificationHook)\n}\n\n// SetClusterStateReloadCallback sets the callback function that will be called when a SMIGRATED notification is received.\n// This allows node clients to notify their parent ClusterClient to reload cluster state.\nfunc (hm *Manager) SetClusterStateReloadCallback(callback ClusterStateReloadCallback) {\n\thm.clusterStateReloadCallback = callback\n}\n\n// TriggerClusterStateReload calls the cluster state reload callback if it's set.\n// This is called when a SMIGRATED notification is received.\nfunc (hm *Manager) TriggerClusterStateReload(ctx context.Context, hostPort string, slotRanges []string) {\n\tif hm.clusterStateReloadCallback != nil {\n\t\thm.clusterStateReloadCallback(ctx, hostPort, slotRanges)\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/manager_test.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/interfaces\"\n)\n\n// MockClient implements interfaces.ClientInterface for testing\ntype MockClient struct {\n\toptions interfaces.OptionsInterface\n}\n\nfunc (mc *MockClient) GetOptions() interfaces.OptionsInterface {\n\treturn mc.options\n}\n\nfunc (mc *MockClient) GetPushProcessor() interfaces.NotificationProcessor {\n\treturn &MockPushProcessor{}\n}\n\n// MockPushProcessor implements interfaces.NotificationProcessor for testing\ntype MockPushProcessor struct{}\n\nfunc (mpp *MockPushProcessor) RegisterHandler(notificationType string, handler interface{}, protected bool) error {\n\treturn nil\n}\n\nfunc (mpp *MockPushProcessor) UnregisterHandler(pushNotificationName string) error {\n\treturn nil\n}\n\nfunc (mpp *MockPushProcessor) GetHandler(pushNotificationName string) interface{} {\n\treturn nil\n}\n\n// MockOptions implements interfaces.OptionsInterface for testing\ntype MockOptions struct{}\n\nfunc (mo *MockOptions) GetReadTimeout() time.Duration {\n\treturn 5 * time.Second\n}\n\nfunc (mo *MockOptions) GetWriteTimeout() time.Duration {\n\treturn 5 * time.Second\n}\n\nfunc (mo *MockOptions) GetAddr() string {\n\treturn \"localhost:6379\"\n}\n\nfunc (mo *MockOptions) GetNodeAddress() string {\n\treturn \"localhost:6379\"\n}\n\nfunc (mo *MockOptions) IsTLSEnabled() bool {\n\treturn false\n}\n\nfunc (mo *MockOptions) GetProtocol() int {\n\treturn 3 // RESP3\n}\n\nfunc (mo *MockOptions) GetPoolSize() int {\n\treturn 10\n}\n\nfunc (mo *MockOptions) GetNetwork() string {\n\treturn \"tcp\"\n}\n\nfunc (mo *MockOptions) NewDialer() func(context.Context) (net.Conn, error) {\n\treturn func(ctx context.Context) (net.Conn, error) {\n\t\treturn nil, nil\n\t}\n}\n\nfunc TestManagerRefactoring(t *testing.T) {\n\tt.Run(\"AtomicStateTracking\", func(t *testing.T) {\n\t\tconfig := DefaultConfig()\n\t\tclient := &MockClient{options: &MockOptions{}}\n\n\t\tmanager, err := NewManager(client, nil, config)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to create maintnotifications manager: %v\", err)\n\t\t}\n\t\tdefer manager.Close()\n\n\t\t// Test initial state\n\t\tif manager.IsHandoffInProgress() {\n\t\t\tt.Error(\"Expected no handoff in progress initially\")\n\t\t}\n\n\t\tif manager.GetActiveOperationCount() != 0 {\n\t\t\tt.Errorf(\"Expected 0 active operations, got %d\", manager.GetActiveOperationCount())\n\t\t}\n\n\t\tif manager.GetState() != StateIdle {\n\t\t\tt.Errorf(\"Expected StateIdle, got %v\", manager.GetState())\n\t\t}\n\n\t\t// Add an operation\n\t\tctx := context.Background()\n\t\tdeadline := time.Now().Add(30 * time.Second)\n\t\terr = manager.TrackMovingOperationWithConnID(ctx, \"new-endpoint:6379\", deadline, 12345, 1)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to track operation: %v\", err)\n\t\t}\n\n\t\t// Test state after adding operation\n\t\tif !manager.IsHandoffInProgress() {\n\t\t\tt.Error(\"Expected handoff in progress after adding operation\")\n\t\t}\n\n\t\tif manager.GetActiveOperationCount() != 1 {\n\t\t\tt.Errorf(\"Expected 1 active operation, got %d\", manager.GetActiveOperationCount())\n\t\t}\n\n\t\tif manager.GetState() != StateMoving {\n\t\t\tt.Errorf(\"Expected StateMoving, got %v\", manager.GetState())\n\t\t}\n\n\t\t// Remove the operation\n\t\tmanager.UntrackOperationWithConnID(12345, 1)\n\n\t\t// Test state after removing operation\n\t\tif manager.IsHandoffInProgress() {\n\t\t\tt.Error(\"Expected no handoff in progress after removing operation\")\n\t\t}\n\n\t\tif manager.GetActiveOperationCount() != 0 {\n\t\t\tt.Errorf(\"Expected 0 active operations, got %d\", manager.GetActiveOperationCount())\n\t\t}\n\n\t\tif manager.GetState() != StateIdle {\n\t\t\tt.Errorf(\"Expected StateIdle, got %v\", manager.GetState())\n\t\t}\n\t})\n\n\tt.Run(\"SyncMapPerformance\", func(t *testing.T) {\n\t\tconfig := DefaultConfig()\n\t\tclient := &MockClient{options: &MockOptions{}}\n\n\t\tmanager, err := NewManager(client, nil, config)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to create maintnotifications manager: %v\", err)\n\t\t}\n\t\tdefer manager.Close()\n\n\t\tctx := context.Background()\n\t\tdeadline := time.Now().Add(30 * time.Second)\n\n\t\t// Test concurrent operations\n\t\tconst numOps = 100\n\t\tfor i := 0; i < numOps; i++ {\n\t\t\terr := manager.TrackMovingOperationWithConnID(ctx, \"endpoint:6379\", deadline, int64(i), uint64(i))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to track operation %d: %v\", i, err)\n\t\t\t}\n\t\t}\n\n\t\tif manager.GetActiveOperationCount() != numOps {\n\t\t\tt.Errorf(\"Expected %d active operations, got %d\", numOps, manager.GetActiveOperationCount())\n\t\t}\n\n\t\t// Test GetActiveMovingOperations\n\t\toperations := manager.GetActiveMovingOperations()\n\t\tif len(operations) != numOps {\n\t\t\tt.Errorf(\"Expected %d operations in map, got %d\", numOps, len(operations))\n\t\t}\n\n\t\t// Remove all operations\n\t\tfor i := 0; i < numOps; i++ {\n\t\t\tmanager.UntrackOperationWithConnID(int64(i), uint64(i))\n\t\t}\n\n\t\tif manager.GetActiveOperationCount() != 0 {\n\t\t\tt.Errorf(\"Expected 0 active operations after cleanup, got %d\", manager.GetActiveOperationCount())\n\t\t}\n\t})\n\n\tt.Run(\"DuplicateOperationHandling\", func(t *testing.T) {\n\t\tconfig := DefaultConfig()\n\t\tclient := &MockClient{options: &MockOptions{}}\n\n\t\tmanager, err := NewManager(client, nil, config)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to create maintnotifications manager: %v\", err)\n\t\t}\n\t\tdefer manager.Close()\n\n\t\tctx := context.Background()\n\t\tdeadline := time.Now().Add(30 * time.Second)\n\n\t\t// Add operation\n\t\terr = manager.TrackMovingOperationWithConnID(ctx, \"endpoint:6379\", deadline, 12345, 1)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to track operation: %v\", err)\n\t\t}\n\n\t\t// Try to add duplicate operation\n\t\terr = manager.TrackMovingOperationWithConnID(ctx, \"endpoint:6379\", deadline, 12345, 1)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Duplicate operation should not return error: %v\", err)\n\t\t}\n\n\t\t// Should still have only 1 operation\n\t\tif manager.GetActiveOperationCount() != 1 {\n\t\t\tt.Errorf(\"Expected 1 active operation after duplicate, got %d\", manager.GetActiveOperationCount())\n\t\t}\n\t})\n\n\tt.Run(\"NotificationTypeConstants\", func(t *testing.T) {\n\t\t// Test that constants are properly defined\n\t\texpectedTypes := []string{\n\t\t\tNotificationMoving,\n\t\t\tNotificationMigrating,\n\t\t\tNotificationMigrated,\n\t\t\tNotificationFailingOver,\n\t\t\tNotificationFailedOver,\n\t\t\tNotificationSMigrating,\n\t\t\tNotificationSMigrated,\n\t\t}\n\n\t\tif len(maintenanceNotificationTypes) != len(expectedTypes) {\n\t\t\tt.Errorf(\"Expected %d notification types, got %d\", len(expectedTypes), len(maintenanceNotificationTypes))\n\t\t}\n\n\t\t// Test that all expected types are present\n\t\ttypeMap := make(map[string]bool)\n\t\tfor _, t := range maintenanceNotificationTypes {\n\t\t\ttypeMap[t] = true\n\t\t}\n\n\t\tfor _, expected := range expectedTypes {\n\t\t\tif !typeMap[expected] {\n\t\t\t\tt.Errorf(\"Expected notification type %s not found in maintenanceNotificationTypes\", expected)\n\t\t\t}\n\t\t}\n\n\t\t// Test that maintenanceNotificationTypes contains all expected constants\n\t\texpectedConstants := []string{\n\t\t\tNotificationMoving,\n\t\t\tNotificationMigrating,\n\t\t\tNotificationMigrated,\n\t\t\tNotificationFailingOver,\n\t\t\tNotificationFailedOver,\n\t\t}\n\n\t\tfor _, expected := range expectedConstants {\n\t\t\tfound := false\n\t\t\tfor _, actual := range maintenanceNotificationTypes {\n\t\t\t\tif actual == expected {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tt.Errorf(\"Expected constant %s not found in maintenanceNotificationTypes\", expected)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "maintnotifications/pool_hook.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// OperationsManagerInterface defines the interface for completing handoff operations\ntype OperationsManagerInterface interface {\n\tTrackMovingOperationWithConnID(ctx context.Context, newEndpoint string, deadline time.Time, seqID int64, connID uint64) error\n\tUntrackOperationWithConnID(seqID int64, connID uint64)\n}\n\n// HandoffRequest represents a request to handoff a connection to a new endpoint\ntype HandoffRequest struct {\n\tConn     *pool.Conn\n\tConnID   uint64 // Unique connection identifier\n\tEndpoint string\n\tSeqID    int64\n\tPool     pool.Pooler // Pool to remove connection from on failure\n}\n\n// PoolHook implements pool.PoolHook for Redis-specific connection handling\n// with maintenance notifications support.\ntype PoolHook struct {\n\t// Base dialer for creating connections to new endpoints during handoffs\n\t// args are network and address\n\tbaseDialer func(context.Context, string, string) (net.Conn, error)\n\n\t// Network type (e.g., \"tcp\", \"unix\")\n\tnetwork string\n\n\t// Worker manager for background handoff processing\n\tworkerManager *handoffWorkerManager\n\n\t// Configuration for the maintenance notifications\n\tconfig *Config\n\n\t// Operations manager interface for operation completion tracking\n\toperationsManager OperationsManagerInterface\n\n\t// Pool interface for removing connections on handoff failure\n\tpool pool.Pooler\n}\n\n// NewPoolHook creates a new pool hook\nfunc NewPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error), network string, config *Config, operationsManager OperationsManagerInterface) *PoolHook {\n\treturn NewPoolHookWithPoolSize(baseDialer, network, config, operationsManager, 0)\n}\n\n// NewPoolHookWithPoolSize creates a new pool hook with pool size for better worker defaults\nfunc NewPoolHookWithPoolSize(baseDialer func(context.Context, string, string) (net.Conn, error), network string, config *Config, operationsManager OperationsManagerInterface, poolSize int) *PoolHook {\n\t// Apply defaults if config is nil or has zero values\n\tif config == nil {\n\t\tconfig = config.ApplyDefaultsWithPoolSize(poolSize)\n\t}\n\n\tph := &PoolHook{\n\t\t// baseDialer is used to create connections to new endpoints during handoffs\n\t\tbaseDialer:        baseDialer,\n\t\tnetwork:           network,\n\t\tconfig:            config,\n\t\toperationsManager: operationsManager,\n\t}\n\n\t// Create worker manager\n\tph.workerManager = newHandoffWorkerManager(config, ph)\n\n\treturn ph\n}\n\n// SetPool sets the pool interface for removing connections on handoff failure\nfunc (ph *PoolHook) SetPool(pooler pool.Pooler) {\n\tph.pool = pooler\n}\n\n// GetCurrentWorkers returns the current number of active workers (for testing)\nfunc (ph *PoolHook) GetCurrentWorkers() int {\n\treturn ph.workerManager.getCurrentWorkers()\n}\n\n// IsHandoffPending returns true if the given connection has a pending handoff\nfunc (ph *PoolHook) IsHandoffPending(conn *pool.Conn) bool {\n\treturn ph.workerManager.isHandoffPending(conn)\n}\n\n// GetPendingMap returns the pending map for testing purposes\nfunc (ph *PoolHook) GetPendingMap() *sync.Map {\n\treturn ph.workerManager.getPendingMap()\n}\n\n// GetMaxWorkers returns the max workers for testing purposes\nfunc (ph *PoolHook) GetMaxWorkers() int {\n\treturn ph.workerManager.getMaxWorkers()\n}\n\n// GetHandoffQueue returns the handoff queue for testing purposes\nfunc (ph *PoolHook) GetHandoffQueue() chan HandoffRequest {\n\treturn ph.workerManager.getHandoffQueue()\n}\n\n// GetCircuitBreakerStats returns circuit breaker statistics for monitoring\nfunc (ph *PoolHook) GetCircuitBreakerStats() []CircuitBreakerStats {\n\treturn ph.workerManager.getCircuitBreakerStats()\n}\n\n// ResetCircuitBreakers resets all circuit breakers (useful for testing)\nfunc (ph *PoolHook) ResetCircuitBreakers() {\n\tph.workerManager.resetCircuitBreakers()\n}\n\n// OnGet is called when a connection is retrieved from the pool\nfunc (ph *PoolHook) OnGet(_ context.Context, conn *pool.Conn, _ bool) (accept bool, err error) {\n\t// Check if connection is marked for handoff\n\t// This prevents using connections that have received MOVING notifications\n\tif conn.ShouldHandoff() {\n\t\treturn false, ErrConnectionMarkedForHandoffWithState\n\t}\n\n\t// Check if connection is usable (not in UNUSABLE or CLOSED state)\n\t// This ensures we don't return connections that are currently being handed off or re-authenticated.\n\tif !conn.IsUsable() {\n\t\treturn false, ErrConnectionMarkedForHandoff\n\t}\n\n\treturn true, nil\n}\n\n// OnPut is called when a connection is returned to the pool\nfunc (ph *PoolHook) OnPut(ctx context.Context, conn *pool.Conn) (shouldPool bool, shouldRemove bool, err error) {\n\t// first check if we should handoff for faster rejection\n\tif !conn.ShouldHandoff() {\n\t\t// Default behavior (no handoff): pool the connection\n\t\treturn true, false, nil\n\t}\n\n\t// check pending handoff to not queue the same connection twice\n\tif ph.workerManager.isHandoffPending(conn) {\n\t\t// Default behavior (pending handoff): pool the connection\n\t\treturn true, false, nil\n\t}\n\n\tif err := ph.workerManager.queueHandoff(conn); err != nil {\n\t\t// Failed to queue handoff, remove the connection\n\t\tinternal.Logger.Printf(ctx, logs.FailedToQueueHandoff(conn.GetID(), err))\n\t\t// Don't pool, remove connection, no error to caller\n\t\treturn false, true, nil\n\t}\n\n\t// Check if handoff was already processed by a worker before we can mark it as queued\n\tif !conn.ShouldHandoff() {\n\t\t// Handoff was already processed - this is normal and the connection should be pooled\n\t\treturn true, false, nil\n\t}\n\n\tif err := conn.MarkQueuedForHandoff(); err != nil {\n\t\t// If marking fails, check if handoff was processed in the meantime\n\t\tif !conn.ShouldHandoff() {\n\t\t\t// Handoff was processed - this is normal, pool the connection\n\t\t\treturn true, false, nil\n\t\t}\n\t\t// Other error - remove the connection\n\t\treturn false, true, nil\n\t}\n\tinternal.Logger.Printf(ctx, logs.MarkedForHandoff(conn.GetID()))\n\treturn true, false, nil\n}\n\nfunc (ph *PoolHook) OnRemove(_ context.Context, _ *pool.Conn, _ error) {\n\t// Not used\n}\n\n// Shutdown gracefully shuts down the processor, waiting for workers to complete\nfunc (ph *PoolHook) Shutdown(ctx context.Context) error {\n\treturn ph.workerManager.shutdownWorkers(ctx)\n}\n"
  },
  {
    "path": "maintnotifications/pool_hook_test.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// mockNetConn implements net.Conn for testing\ntype mockNetConn struct {\n\taddr           string\n\tshouldFailInit bool\n}\n\nfunc (m *mockNetConn) Read(b []byte) (n int, err error)   { return 0, nil }\nfunc (m *mockNetConn) Write(b []byte) (n int, err error)  { return len(b), nil }\nfunc (m *mockNetConn) Close() error                       { return nil }\nfunc (m *mockNetConn) LocalAddr() net.Addr                { return &mockAddr{m.addr} }\nfunc (m *mockNetConn) RemoteAddr() net.Addr               { return &mockAddr{m.addr} }\nfunc (m *mockNetConn) SetDeadline(t time.Time) error      { return nil }\nfunc (m *mockNetConn) SetReadDeadline(t time.Time) error  { return nil }\nfunc (m *mockNetConn) SetWriteDeadline(t time.Time) error { return nil }\n\ntype mockAddr struct {\n\taddr string\n}\n\nfunc (m *mockAddr) Network() string { return \"tcp\" }\nfunc (m *mockAddr) String() string  { return m.addr }\n\n// createMockPoolConnection creates a mock pool connection for testing\nfunc createMockPoolConnection() *pool.Conn {\n\tmockNetConn := &mockNetConn{addr: \"test:6379\"}\n\tconn := pool.NewConn(mockNetConn)\n\tconn.SetUsable(true) // Make connection usable for testing (transitions to IDLE)\n\t// Simulate real flow: connection is acquired (IDLE → IN_USE) before OnPut is called\n\tconn.SetUsed(true) // Transition to IN_USE state\n\treturn conn\n}\n\n// mockPool implements pool.Pooler for testing\ntype mockPool struct {\n\tremovedConnections map[uint64]bool\n\tmu                 sync.Mutex\n}\n\nfunc (mp *mockPool) NewConn(ctx context.Context) (*pool.Conn, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n\nfunc (mp *mockPool) CloseConn(ctx context.Context, conn *pool.Conn, reason string, fromState string) error {\n\treturn nil\n}\n\nfunc (mp *mockPool) Get(ctx context.Context) (*pool.Conn, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n\nfunc (mp *mockPool) Put(ctx context.Context, conn *pool.Conn) {\n\t// Not implemented for testing\n}\n\nfunc (mp *mockPool) Remove(ctx context.Context, conn *pool.Conn, reason error) {\n\tmp.mu.Lock()\n\tdefer mp.mu.Unlock()\n\n\t// Use pool.Conn directly - no adapter needed\n\tmp.removedConnections[conn.GetID()] = true\n}\n\nfunc (mp *mockPool) RemoveWithoutTurn(ctx context.Context, conn *pool.Conn, reason error) {\n\t// For mock pool, same behavior as Remove since we don't have a turn-based queue\n\tmp.Remove(ctx, conn, reason)\n}\n\n// WasRemoved safely checks if a connection was removed from the pool\nfunc (mp *mockPool) WasRemoved(connID uint64) bool {\n\tmp.mu.Lock()\n\tdefer mp.mu.Unlock()\n\treturn mp.removedConnections[connID]\n}\n\nfunc (mp *mockPool) Len() int {\n\treturn 0\n}\n\nfunc (mp *mockPool) IdleLen() int {\n\treturn 0\n}\n\nfunc (mp *mockPool) Stats() *pool.Stats {\n\treturn &pool.Stats{}\n}\n\nfunc (mp *mockPool) Size() int {\n\treturn 0\n}\n\nfunc (mp *mockPool) AddPoolHook(hook pool.PoolHook) {\n\t// Mock implementation - do nothing\n}\n\nfunc (mp *mockPool) RemovePoolHook(hook pool.PoolHook) {\n\t// Mock implementation - do nothing\n}\n\nfunc (mp *mockPool) Close() error {\n\treturn nil\n}\n\n// TestConnectionHook tests the Redis connection processor functionality\nfunc TestConnectionHook(t *testing.T) {\n\t// Create a base dialer for testing\n\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\treturn &mockNetConn{addr: addr}, nil\n\t}\n\n\tt.Run(\"SuccessfulEventDrivenHandoff\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMode:              ModeAuto,\n\t\t\tEndpointType:      EndpointTypeAuto,\n\t\t\tMaxWorkers:        1,  // Use only 1 worker to ensure synchronization\n\t\t\tHandoffQueueSize:  10, // Explicit queue size to avoid 0-size queue\n\t\t\tMaxHandoffRetries: 3,\n\t\t}\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\tconn := createMockPoolConnection()\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 12345); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\t// Verify connection is marked for handoff\n\t\tif !conn.ShouldHandoff() {\n\t\t\tt.Fatal(\"Connection should be marked for handoff\")\n\t\t}\n\t\t// Set a mock initialization function with synchronization\n\t\tinitConnCalled := make(chan bool, 1)\n\t\tproceedWithInit := make(chan bool, 1)\n\t\tinitConnFunc := func(ctx context.Context, cn *pool.Conn) error {\n\t\t\tselect {\n\t\t\tcase initConnCalled <- true:\n\t\t\tdefault:\n\t\t\t}\n\t\t\t// Wait for test to proceed\n\t\t\t<-proceedWithInit\n\t\t\treturn nil\n\t\t}\n\t\tconn.SetInitConnFunc(initConnFunc)\n\n\t\tctx := context.Background()\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should not error: %v\", err)\n\t\t}\n\n\t\t// Should pool the connection immediately (handoff queued)\n\t\tif !shouldPool {\n\t\t\tt.Error(\"Connection should be pooled immediately with event-driven handoff\")\n\t\t}\n\t\tif shouldRemove {\n\t\t\tt.Error(\"Connection should not be removed when queuing handoff\")\n\t\t}\n\n\t\t// Wait for initialization to be called (indicates handoff started)\n\t\tselect {\n\t\tcase <-initConnCalled:\n\t\t\t// Good, initialization was called\n\t\tcase <-time.After(5 * time.Second):\n\t\t\tt.Fatal(\"Timeout waiting for initialization function to be called\")\n\t\t}\n\n\t\t// Connection should be in pending map while initialization is blocked\n\t\tif _, pending := processor.GetPendingMap().Load(conn.GetID()); !pending {\n\t\t\tt.Error(\"Connection should be in pending handoffs map\")\n\t\t}\n\n\t\t// Allow initialization to proceed\n\t\tproceedWithInit <- true\n\n\t\t// Wait for handoff to complete with proper timeout and polling\n\t\ttimeout := time.After(2 * time.Second)\n\t\tticker := time.NewTicker(10 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\n\t\thandoffCompleted := false\n\t\tfor !handoffCompleted {\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\tt.Fatal(\"Timeout waiting for handoff to complete\")\n\t\t\tcase <-ticker.C:\n\t\t\t\tif _, pending := processor.GetPendingMap().Load(conn); !pending {\n\t\t\t\t\thandoffCompleted = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Verify handoff completed (removed from pending map)\n\t\tif _, pending := processor.GetPendingMap().Load(conn); pending {\n\t\t\tt.Error(\"Connection should be removed from pending map after handoff\")\n\t\t}\n\n\t\t// Verify connection is usable again\n\t\tif !conn.IsUsable() {\n\t\t\tt.Error(\"Connection should be usable after successful handoff\")\n\t\t}\n\n\t\t// Verify handoff state is cleared\n\t\tif conn.ShouldHandoff() {\n\t\t\tt.Error(\"Connection should not be marked for handoff after completion\")\n\t\t}\n\t})\n\n\tt.Run(\"HandoffNotNeeded\", func(t *testing.T) {\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tconn := createMockPoolConnection()\n\t\t// Don't mark for handoff\n\n\t\tctx := context.Background()\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should not error when handoff not needed: %v\", err)\n\t\t}\n\n\t\t// Should pool the connection normally\n\t\tif !shouldPool {\n\t\t\tt.Error(\"Connection should be pooled when no handoff needed\")\n\t\t}\n\t\tif shouldRemove {\n\t\t\tt.Error(\"Connection should not be removed when no handoff needed\")\n\t\t}\n\t})\n\tt.Run(\"EmptyEndpoint\", func(t *testing.T) {\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tconn := createMockPoolConnection()\n\t\tif err := conn.MarkForHandoff(\"\", 12345); err != nil { // Empty endpoint\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\t\tctx := context.Background()\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should not error with empty endpoint: %v\", err)\n\t\t}\n\n\t\t// Should pool the connection (empty endpoint clears state)\n\t\tif !shouldPool {\n\t\t\tt.Error(\"Connection should be pooled after clearing empty endpoint\")\n\t\t}\n\t\tif shouldRemove {\n\t\t\tt.Error(\"Connection should not be removed after clearing empty endpoint\")\n\t\t}\n\n\t\t// State should be cleared\n\t\tif conn.ShouldHandoff() {\n\t\t\tt.Error(\"Connection should not be marked for handoff after clearing empty endpoint\")\n\t\t}\n\t})\n\n\tt.Run(\"EventDrivenHandoffDialerError\", func(t *testing.T) {\n\t\t// Create a failing base dialer\n\t\tfailingDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn nil, errors.New(\"dial failed\")\n\t\t}\n\n\t\tconfig := &Config{\n\t\t\tMode:              ModeAuto,\n\t\t\tEndpointType:      EndpointTypeAuto,\n\t\t\tMaxWorkers:        2,\n\t\t\tHandoffQueueSize:  10,\n\t\t\tMaxHandoffRetries: 2,                      // Reduced retries for faster test\n\t\t\tHandoffTimeout:    500 * time.Millisecond, // Shorter timeout for faster test\n\t\t}\n\t\tprocessor := NewPoolHook(failingDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\tconn := createMockPoolConnection()\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 12345); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should not return error to caller: %v\", err)\n\t\t}\n\n\t\t// Should pool the connection initially (handoff queued)\n\t\tif !shouldPool {\n\t\t\tt.Error(\"Connection should be pooled initially with event-driven handoff\")\n\t\t}\n\t\tif shouldRemove {\n\t\t\tt.Error(\"Connection should not be removed when queuing handoff\")\n\t\t}\n\n\t\t// Wait for handoff to complete and fail with proper timeout and polling\n\t\ttimeout := time.After(3 * time.Second)\n\t\tticker := time.NewTicker(10 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\n\t\t// wait for handoff to start\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\thandoffCompleted := false\n\t\tfor !handoffCompleted {\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\tt.Fatal(\"Timeout waiting for failed handoff to complete\")\n\t\t\tcase <-ticker.C:\n\t\t\t\tif _, pending := processor.GetPendingMap().Load(conn.GetID()); !pending {\n\t\t\t\t\thandoffCompleted = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Connection should be removed from pending map after failed handoff\n\t\tif _, pending := processor.GetPendingMap().Load(conn.GetID()); pending {\n\t\t\tt.Error(\"Connection should be removed from pending map after failed handoff\")\n\t\t}\n\n\t\t// Wait for retries to complete (with MaxHandoffRetries=2, it will retry twice then give up)\n\t\t// Each retry has a delay of handoffTimeout/2 = 250ms, so wait for all retries to complete\n\t\ttime.Sleep(800 * time.Millisecond)\n\n\t\t// After max retries are reached, the connection should be removed from pool\n\t\t// and handoff state should be cleared\n\t\tif conn.ShouldHandoff() {\n\t\t\tt.Error(\"Connection should not be marked for handoff after max retries reached\")\n\t\t}\n\n\t\tt.Logf(\"EventDrivenHandoffDialerError test completed successfully\")\n\t})\n\n\tt.Run(\"BufferedDataRESP2\", func(t *testing.T) {\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tconn := createMockPoolConnection()\n\n\t\t// For this test, we'll just verify the logic works for connections without buffered data\n\t\t// The actual buffered data detection is handled by the pool's connection health check\n\t\t// which is outside the scope of the Redis connection processor\n\n\t\tctx := context.Background()\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should not error: %v\", err)\n\t\t}\n\n\t\t// Should pool the connection normally (no buffered data in mock)\n\t\tif !shouldPool {\n\t\t\tt.Error(\"Connection should be pooled when no buffered data\")\n\t\t}\n\t\tif shouldRemove {\n\t\t\tt.Error(\"Connection should not be removed when no buffered data\")\n\t\t}\n\t})\n\n\tt.Run(\"OnGet\", func(t *testing.T) {\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tconn := createMockPoolConnection()\n\n\t\tctx := context.Background()\n\t\tacceptCon, err := processor.OnGet(ctx, conn, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnGet should not error for normal connection: %v\", err)\n\t\t}\n\t\tif !acceptCon {\n\t\t\tt.Error(\"Connection should be accepted for normal connection\")\n\t\t}\n\t})\n\n\tt.Run(\"OnGetWithPendingHandoff\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMode:              ModeAuto,\n\t\t\tEndpointType:      EndpointTypeAuto,\n\t\t\tMaxWorkers:        2,\n\t\t\tHandoffQueueSize:  10,\n\t\t\tMaxHandoffRetries: 3, // Explicit queue size to avoid 0-size queue\n\t\t}\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\tconn := createMockPoolConnection()\n\n\t\t// Simulate a pending handoff by marking for handoff and queuing\n\t\tconn.MarkForHandoff(\"new-endpoint:6379\", 12345)\n\t\tprocessor.GetPendingMap().Store(conn.GetID(), int64(12345)) // Store connID -> seqID\n\t\tconn.MarkQueuedForHandoff()                                 // Mark as queued (sets ShouldHandoff=false, state=UNUSABLE)\n\n\t\tctx := context.Background()\n\t\tacceptCon, err := processor.OnGet(ctx, conn, false)\n\t\t// After MarkQueuedForHandoff, ShouldHandoff() returns false, so we get ErrConnectionMarkedForHandoff\n\t\t// (from IsUsable() check) instead of ErrConnectionMarkedForHandoffWithState\n\t\tif err != ErrConnectionMarkedForHandoff {\n\t\t\tt.Errorf(\"Expected ErrConnectionMarkedForHandoff, got %v\", err)\n\t\t}\n\t\tif acceptCon {\n\t\t\tt.Error(\"Connection should not be accepted when marked for handoff\")\n\t\t}\n\n\t\t// Clean up\n\t\tprocessor.GetPendingMap().Delete(conn)\n\t})\n\n\tt.Run(\"EventDrivenStateManagement\", func(t *testing.T) {\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", nil, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\tconn := createMockPoolConnection()\n\n\t\t// Test initial state - no pending handoffs\n\t\tif _, pending := processor.GetPendingMap().Load(conn); pending {\n\t\t\tt.Error(\"New connection should not have pending handoffs\")\n\t\t}\n\n\t\t// Test adding to pending map\n\t\tconn.MarkForHandoff(\"new-endpoint:6379\", 12345)\n\t\tprocessor.GetPendingMap().Store(conn.GetID(), int64(12345)) // Store connID -> seqID\n\t\tconn.MarkQueuedForHandoff()                                 // Mark as queued (sets ShouldHandoff=false, state=UNUSABLE)\n\n\t\tif _, pending := processor.GetPendingMap().Load(conn.GetID()); !pending {\n\t\t\tt.Error(\"Connection should be in pending map\")\n\t\t}\n\n\t\t// Test OnGet with pending handoff\n\t\tctx := context.Background()\n\t\tacceptCon, err := processor.OnGet(ctx, conn, false)\n\t\t// After MarkQueuedForHandoff, ShouldHandoff() returns false, so we get ErrConnectionMarkedForHandoff\n\t\tif err != ErrConnectionMarkedForHandoff {\n\t\t\tt.Errorf(\"Should return ErrConnectionMarkedForHandoff for pending connection, got %v\", err)\n\t\t}\n\t\tif acceptCon {\n\t\t\tt.Error(\"Should not accept connection with pending handoff\")\n\t\t}\n\n\t\t// Test removing from pending map and clearing handoff state\n\t\tprocessor.GetPendingMap().Delete(conn)\n\t\tif _, pending := processor.GetPendingMap().Load(conn); pending {\n\t\t\tt.Error(\"Connection should be removed from pending map\")\n\t\t}\n\n\t\t// Clear handoff state to simulate completed handoff\n\t\tconn.ClearHandoffState()\n\t\tconn.SetUsable(true) // Make connection usable again\n\n\t\t// Test OnGet without pending handoff\n\t\tacceptCon, err = processor.OnGet(ctx, conn, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Should not return error for non-pending connection: %v\", err)\n\t\t}\n\t\tif !acceptCon {\n\t\t\tt.Error(\"Should accept connection without pending handoff\")\n\t\t}\n\t})\n\n\tt.Run(\"EventDrivenQueueOptimization\", func(t *testing.T) {\n\t\t// Create processor with small queue to test optimization features\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:        3,\n\t\t\tHandoffQueueSize:  2,\n\t\t\tMaxHandoffRetries: 3, // Small queue to trigger optimizations\n\t\t}\n\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t// Add small delay to simulate network latency\n\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Create multiple connections that need handoff to fill the queue\n\t\tconnections := make([]*pool.Conn, 5)\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tconnections[i] = createMockPoolConnection()\n\t\t\tif err := connections[i].MarkForHandoff(\"new-endpoint:6379\", int64(i)); err != nil {\n\t\t\t\tt.Fatalf(\"Failed to mark conn[%d] for handoff: %v\", i, err)\n\t\t\t}\n\t\t\t// Set a mock initialization function\n\t\t\tconnections[i].SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\tctx := context.Background()\n\t\tsuccessCount := 0\n\n\t\t// Process connections - should trigger scaling and timeout logic\n\t\tfor _, conn := range connections {\n\t\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"OnPut returned error (expected with timeout): %v\", err)\n\t\t\t}\n\n\t\t\tif shouldPool && !shouldRemove {\n\t\t\t\tsuccessCount++\n\t\t\t}\n\t\t}\n\n\t\t// With timeout and scaling, most handoffs should eventually succeed\n\t\tif successCount == 0 {\n\t\t\tt.Error(\"Should have queued some handoffs with timeout and scaling\")\n\t\t}\n\n\t\tt.Logf(\"Successfully queued %d handoffs with optimization features\", successCount)\n\n\t\t// Give time for workers to process and scaling to occur\n\t\ttime.Sleep(100 * time.Millisecond)\n\t})\n\n\tt.Run(\"WorkerScalingBehavior\", func(t *testing.T) {\n\t\t// Create processor with small queue to test scaling behavior\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:        15, // Set to >= 10 to test explicit value preservation\n\t\t\tHandoffQueueSize:  1,\n\t\t\tMaxHandoffRetries: 3, // Very small queue to force scaling\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Verify initial worker count (should be 0 with on-demand workers)\n\t\tif processor.GetCurrentWorkers() != 0 {\n\t\t\tt.Errorf(\"Expected 0 initial workers with on-demand system, got %d\", processor.GetCurrentWorkers())\n\t\t}\n\t\tif processor.GetMaxWorkers() != 15 {\n\t\t\tt.Errorf(\"Expected maxWorkers=15, got %d\", processor.GetMaxWorkers())\n\t\t}\n\n\t\t// The on-demand worker behavior creates workers only when needed\n\t\t// This test just verifies the basic configuration is correct\n\t\tt.Logf(\"On-demand worker configuration verified - Max: %d, Current: %d\",\n\t\t\tprocessor.GetMaxWorkers(), processor.GetCurrentWorkers())\n\t})\n\n\tt.Run(\"PassiveTimeoutRestoration\", func(t *testing.T) {\n\t\t// Create processor with fast post-handoff duration for testing\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:                 2,\n\t\t\tHandoffQueueSize:           10,\n\t\t\tMaxHandoffRetries:          3,                      // Allow retries for successful handoff\n\t\t\tPostHandoffRelaxedDuration: 100 * time.Millisecond, // Fast expiration for testing\n\t\t\tRelaxedTimeout:             5 * time.Second,\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\tctx := context.Background()\n\n\t\t// Create a connection and trigger handoff\n\t\tconn := createMockPoolConnection()\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 1); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\t// Set a mock initialization function\n\t\tconn.SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\treturn nil\n\t\t})\n\n\t\t// Process the connection to trigger handoff\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Handoff should succeed: %v\", err)\n\t\t}\n\t\tif !shouldPool || shouldRemove {\n\t\t\tt.Error(\"Connection should be pooled after handoff\")\n\t\t}\n\n\t\t// Wait for handoff to complete with proper timeout and polling\n\t\ttimeout := time.After(1 * time.Second)\n\t\tticker := time.NewTicker(5 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\n\t\thandoffCompleted := false\n\t\tfor !handoffCompleted {\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\tt.Fatal(\"Timeout waiting for handoff to complete\")\n\t\t\tcase <-ticker.C:\n\t\t\t\tif _, pending := processor.GetPendingMap().Load(conn); !pending {\n\t\t\t\t\thandoffCompleted = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Verify relaxed timeout is set with deadline\n\t\tif !conn.HasRelaxedTimeout() {\n\t\t\tt.Error(\"Connection should have relaxed timeout after handoff\")\n\t\t}\n\n\t\t// Test that timeout is still active before deadline\n\t\t// We'll use HasRelaxedTimeout which internally checks the deadline\n\t\tif !conn.HasRelaxedTimeout() {\n\t\t\tt.Error(\"Connection should still have active relaxed timeout before deadline\")\n\t\t}\n\n\t\t// Wait for deadline to pass\n\t\ttime.Sleep(150 * time.Millisecond) // 100ms deadline + buffer\n\n\t\t// Test that timeout is automatically restored after deadline\n\t\t// HasRelaxedTimeout should return false after deadline passes\n\t\tif conn.HasRelaxedTimeout() {\n\t\t\tt.Error(\"Connection should not have active relaxed timeout after deadline\")\n\t\t}\n\n\t\t// Additional verification: calling HasRelaxedTimeout again should still return false\n\t\t// and should have cleared the internal timeout values\n\t\tif conn.HasRelaxedTimeout() {\n\t\t\tt.Error(\"Connection should not have relaxed timeout after deadline (second check)\")\n\t\t}\n\n\t\tt.Logf(\"Passive timeout restoration test completed successfully\")\n\t})\n\n\tt.Run(\"UsableFlagBehavior\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:        2,\n\t\t\tHandoffQueueSize:  10,\n\t\t\tMaxHandoffRetries: 3,\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\tctx := context.Background()\n\n\t\t// Create a new connection\n\t\tmockNetConn := &mockNetConn{addr: \"test:6379\"}\n\t\tconn := pool.NewConn(mockNetConn)\n\n\t\t// New connections in CREATED state are usable (they pass OnGet() before initialization)\n\t\t// The initialization happens AFTER OnGet() in the client code\n\t\tif !conn.IsUsable() {\n\t\t\tt.Error(\"New connection should be usable (CREATED state is usable)\")\n\t\t}\n\n\t\t// Simulate initialization by transitioning to IDLE\n\t\tconn.GetStateMachine().Transition(pool.StateIdle)\n\t\tif !conn.IsUsable() {\n\t\t\tt.Error(\"Connection should be usable after initialization (IDLE state)\")\n\t\t}\n\n\t\t// OnGet should succeed for usable connection\n\t\tacceptConn, err := processor.OnGet(ctx, conn, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnGet should succeed for usable connection: %v\", err)\n\t\t}\n\n\t\tif !acceptConn {\n\t\t\tt.Error(\"Connection should be accepted when usable\")\n\t\t}\n\n\t\t// Mark connection for handoff\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 1); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\t// Set a mock initialization function\n\t\tconn.SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\treturn nil\n\t\t})\n\n\t\t// Connection should still be usable until queued, but marked for handoff\n\t\tif !conn.IsUsable() {\n\t\t\tt.Error(\"Connection should still be usable after being marked for handoff (until queued)\")\n\t\t}\n\t\tif !conn.ShouldHandoff() {\n\t\t\tt.Error(\"Connection should be marked for handoff\")\n\t\t}\n\n\t\t// OnGet should FAIL for connection marked for handoff\n\t\t// Even though the connection is still in a usable state, the metadata indicates\n\t\t// it should be handed off, so we reject it to prevent using a connection that\n\t\t// will be moved to a different endpoint\n\t\tacceptConn, err = processor.OnGet(ctx, conn, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"OnGet should fail for connection marked for handoff\")\n\t\t}\n\t\tif err != ErrConnectionMarkedForHandoffWithState {\n\t\t\tt.Errorf(\"Expected ErrConnectionMarkedForHandoffWithState, got %v\", err)\n\t\t}\n\t\tif acceptConn {\n\t\t\tt.Error(\"Connection should not be accepted when marked for handoff\")\n\t\t}\n\n\t\t// Process the connection to trigger handoff\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should succeed: %v\", err)\n\t\t}\n\t\tif !shouldPool || shouldRemove {\n\t\t\tt.Errorf(\"Connection should be pooled after handoff (shouldPool=%v, shouldRemove=%v)\", shouldPool, shouldRemove)\n\t\t}\n\n\t\t// Wait for handoff to complete with polling instead of fixed sleep\n\t\t// This avoids flakiness on slow CI runners where 50ms may not be enough\n\t\tmaxWait := 500 * time.Millisecond\n\t\tpollInterval := 10 * time.Millisecond\n\t\tdeadline := time.Now().Add(maxWait)\n\n\t\thandoffCompleted := false\n\t\tfor time.Now().Before(deadline) {\n\t\t\tif conn.IsUsable() && !processor.IsHandoffPending(conn) {\n\t\t\t\thandoffCompleted = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ttime.Sleep(pollInterval)\n\t\t}\n\n\t\tif !handoffCompleted {\n\t\t\tt.Fatalf(\"Handoff did not complete within %v (IsUsable=%v, IsHandoffPending=%v)\",\n\t\t\t\tmaxWait, conn.IsUsable(), processor.IsHandoffPending(conn))\n\t\t}\n\t\t// After handoff completion, connection should be usable again\n\t\tif !conn.IsUsable() {\n\t\t\tt.Error(\"Connection should be usable after handoff completion\")\n\t\t}\n\n\t\t// OnGet should succeed again\n\t\tacceptConn, err = processor.OnGet(ctx, conn, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnGet should succeed after handoff completion: %v\", err)\n\t\t}\n\n\t\tif !acceptConn {\n\t\t\tt.Error(\"Connection should be accepted after handoff completion\")\n\t\t}\n\n\t\tt.Logf(\"Usable flag behavior test completed successfully\")\n\t})\n\n\tt.Run(\"StaticQueueBehavior\", func(t *testing.T) {\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:        3,\n\t\t\tHandoffQueueSize:  50,\n\t\t\tMaxHandoffRetries: 3, // Explicit static queue size\n\t\t}\n\n\t\tprocessor := NewPoolHookWithPoolSize(baseDialer, \"tcp\", config, nil, 100) // Pool size: 100\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Verify queue capacity matches configured size\n\t\tqueueCapacity := cap(processor.GetHandoffQueue())\n\t\tif queueCapacity != 50 {\n\t\t\tt.Errorf(\"Expected queue capacity 50, got %d\", queueCapacity)\n\t\t}\n\n\t\t// Test that queue size is static regardless of pool size\n\t\t// (No dynamic resizing should occur)\n\n\t\tctx := context.Background()\n\n\t\t// Fill part of the queue\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tconn := createMockPoolConnection()\n\t\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", int64(i+1)); err != nil {\n\t\t\t\tt.Fatalf(\"Failed to mark conn[%d] for handoff: %v\", i, err)\n\t\t\t}\n\t\t\t// Set a mock initialization function\n\t\t\tconn.SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Failed to queue handoff %d: %v\", i, err)\n\t\t\t}\n\n\t\t\tif !shouldPool || shouldRemove {\n\t\t\t\tt.Errorf(\"conn[%d] should be pooled after handoff (shouldPool=%v, shouldRemove=%v)\",\n\t\t\t\t\ti, shouldPool, shouldRemove)\n\t\t\t}\n\t\t}\n\n\t\t// Verify queue capacity remains static (the main purpose of this test)\n\t\tfinalCapacity := cap(processor.GetHandoffQueue())\n\n\t\tif finalCapacity != 50 {\n\t\t\tt.Errorf(\"Queue capacity should remain static at 50, got %d\", finalCapacity)\n\t\t}\n\n\t\t// Note: We don't check queue size here because workers process items quickly\n\t\t// The important thing is that the capacity remains static regardless of pool size\n\t})\n\n\tt.Run(\"ConnectionRemovalOnHandoffFailure\", func(t *testing.T) {\n\t\t// Create a failing dialer that will cause handoff initialization to fail\n\t\tfailingDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t// Return a connection that will fail during initialization\n\t\t\treturn &mockNetConn{addr: addr, shouldFailInit: true}, nil\n\t\t}\n\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:        2,\n\t\t\tHandoffQueueSize:  10,\n\t\t\tMaxHandoffRetries: 3,\n\t\t}\n\n\t\tprocessor := NewPoolHook(failingDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Create a mock pool that tracks removals\n\t\tmockPool := &mockPool{removedConnections: make(map[uint64]bool)}\n\t\tprocessor.SetPool(mockPool)\n\n\t\tctx := context.Background()\n\n\t\t// Create a connection and mark it for handoff\n\t\tconn := createMockPoolConnection()\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 1); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\t// Set a failing initialization function\n\t\tconn.SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\treturn fmt.Errorf(\"initialization failed\")\n\t\t})\n\n\t\t// Process the connection - handoff should fail and connection should be removed\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should not error: %v\", err)\n\t\t}\n\t\tif !shouldPool || shouldRemove {\n\t\t\tt.Error(\"Connection should be pooled after failed handoff attempt\")\n\t\t}\n\n\t\t// Wait for handoff to be attempted and fail\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Verify that the connection was removed from the pool\n\t\tif !mockPool.WasRemoved(conn.GetID()) {\n\t\t\tt.Errorf(\"conn[%d] should have been removed from pool after handoff failure\", conn.GetID())\n\t\t}\n\n\t\tt.Logf(\"Connection removal on handoff failure test completed successfully\")\n\t})\n\n\tt.Run(\"PostHandoffRelaxedTimeout\", func(t *testing.T) {\n\t\t// Create config with short post-handoff duration for testing\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:                 2,\n\t\t\tHandoffQueueSize:           10,\n\t\t\tMaxHandoffRetries:          3, // Allow retries for successful handoff\n\t\t\tRelaxedTimeout:             5 * time.Second,\n\t\t\tPostHandoffRelaxedDuration: 100 * time.Millisecond, // Short for testing\n\t\t}\n\n\t\tbaseDialer := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn &mockNetConn{addr: addr}, nil\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\tconn := createMockPoolConnection()\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 12345); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\t// Set a mock initialization function\n\t\tconn.SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\treturn nil\n\t\t})\n\n\t\tctx := context.Background()\n\t\tshouldPool, shouldRemove, err := processor.OnPut(ctx, conn)\n\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"OnPut failed: %v\", err)\n\t\t}\n\n\t\tif !shouldPool {\n\t\t\tt.Error(\"Connection should be pooled after successful handoff\")\n\t\t}\n\n\t\tif shouldRemove {\n\t\t\tt.Error(\"Connection should not be removed after successful handoff\")\n\t\t}\n\n\t\t// Wait for the handoff to complete (it happens asynchronously)\n\t\ttimeout := time.After(1 * time.Second)\n\t\tticker := time.NewTicker(5 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\n\t\thandoffCompleted := false\n\t\tfor !handoffCompleted {\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\tt.Fatal(\"Timeout waiting for handoff to complete\")\n\t\t\tcase <-ticker.C:\n\t\t\t\tif _, pending := processor.GetPendingMap().Load(conn); !pending {\n\t\t\t\t\thandoffCompleted = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Verify that relaxed timeout was applied to the new connection\n\t\tif !conn.HasRelaxedTimeout() {\n\t\t\tt.Error(\"New connection should have relaxed timeout applied after handoff\")\n\t\t}\n\n\t\t// Wait for the post-handoff duration to expire\n\t\ttime.Sleep(150 * time.Millisecond) // Slightly longer than PostHandoffRelaxedDuration\n\n\t\t// Verify that relaxed timeout was automatically cleared\n\t\tif conn.HasRelaxedTimeout() {\n\t\t\tt.Error(\"Relaxed timeout should be automatically cleared after post-handoff duration\")\n\t\t}\n\t})\n\n\tt.Run(\"MarkForHandoff returns error when already marked\", func(t *testing.T) {\n\t\tconn := createMockPoolConnection()\n\n\t\t// First mark should succeed\n\t\tif err := conn.MarkForHandoff(\"new-endpoint:6379\", 1); err != nil {\n\t\t\tt.Fatalf(\"First MarkForHandoff should succeed: %v\", err)\n\t\t}\n\n\t\t// Second mark should fail\n\t\tif err := conn.MarkForHandoff(\"another-endpoint:6379\", 2); err == nil {\n\t\t\tt.Fatal(\"Second MarkForHandoff should return error\")\n\t\t} else if err.Error() != \"connection is already marked for handoff\" {\n\t\t\tt.Fatalf(\"Expected specific error message, got: %v\", err)\n\t\t}\n\n\t\t// Verify original handoff data is preserved\n\t\tif !conn.ShouldHandoff() {\n\t\t\tt.Fatal(\"Connection should still be marked for handoff\")\n\t\t}\n\t\tif conn.GetHandoffEndpoint() != \"new-endpoint:6379\" {\n\t\t\tt.Fatalf(\"Expected original endpoint, got: %s\", conn.GetHandoffEndpoint())\n\t\t}\n\t\tif conn.GetMovingSeqID() != 1 {\n\t\t\tt.Fatalf(\"Expected original sequence ID, got: %d\", conn.GetMovingSeqID())\n\t\t}\n\t})\n\n\tt.Run(\"HandoffTimeoutConfiguration\", func(t *testing.T) {\n\t\t// Test that HandoffTimeout from config is actually used\n\t\tcustomTimeout := 2 * time.Second\n\t\tconfig := &Config{\n\t\t\tMaxWorkers:        2,\n\t\t\tHandoffQueueSize:  10,\n\t\t\tHandoffTimeout:    customTimeout, // Custom timeout\n\t\t\tMaxHandoffRetries: 1,             // Single retry to speed up test\n\t\t}\n\n\t\tprocessor := NewPoolHook(baseDialer, \"tcp\", config, nil)\n\t\tdefer processor.Shutdown(context.Background())\n\n\t\t// Create a connection that will test the timeout\n\t\tconn := createMockPoolConnection()\n\t\tif err := conn.MarkForHandoff(\"test-endpoint:6379\", 123); err != nil {\n\t\t\tt.Fatalf(\"Failed to mark connection for handoff: %v\", err)\n\t\t}\n\n\t\t// Set a dialer that will check the context timeout\n\t\tvar timeoutVerified int32 // Use atomic for thread safety\n\t\tconn.SetInitConnFunc(func(ctx context.Context, cn *pool.Conn) error {\n\t\t\t// Check that the context has the expected timeout\n\t\t\tdeadline, ok := ctx.Deadline()\n\t\t\tif !ok {\n\t\t\t\tt.Error(\"Context should have a deadline\")\n\t\t\t\treturn errors.New(\"no deadline\")\n\t\t\t}\n\n\t\t\t// The deadline should be approximately customTimeout from now\n\t\t\texpectedDeadline := time.Now().Add(customTimeout)\n\t\t\ttimeDiff := deadline.Sub(expectedDeadline)\n\t\t\tif timeDiff < -500*time.Millisecond || timeDiff > 500*time.Millisecond {\n\t\t\t\tt.Errorf(\"Context deadline not as expected. Expected around %v, got %v (diff: %v)\",\n\t\t\t\t\texpectedDeadline, deadline, timeDiff)\n\t\t\t} else {\n\t\t\t\tatomic.StoreInt32(&timeoutVerified, 1)\n\t\t\t}\n\n\t\t\treturn nil // Successful handoff\n\t\t})\n\n\t\t// Trigger handoff\n\t\tshouldPool, shouldRemove, err := processor.OnPut(context.Background(), conn)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"OnPut should not return error: %v\", err)\n\t\t}\n\n\t\t// Connection should be queued for handoff\n\t\tif !shouldPool || shouldRemove {\n\t\t\tt.Errorf(\"Connection should be pooled for handoff processing\")\n\t\t}\n\n\t\t// Wait for handoff to complete\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\tif atomic.LoadInt32(&timeoutVerified) == 0 {\n\t\t\tt.Error(\"HandoffTimeout was not properly applied to context\")\n\t\t}\n\n\t\tt.Logf(\"HandoffTimeout configuration test completed successfully\")\n\t})\n}\n"
  },
  {
    "path": "maintnotifications/push_notification_handler.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/maintnotifications/logs\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// NotificationHandler handles push notifications for the simplified manager.\ntype NotificationHandler struct {\n\tmanager           *Manager\n\toperationsManager OperationsManagerInterface\n}\n\n// HandlePushNotification processes push notifications with hook support.\nfunc (snh *NotificationHandler) HandlePushNotification(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\tif len(notification) == 0 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotificationFormat(notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tnotificationType, ok := notification[0].(string)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotificationTypeFormat(notification[0]))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Process pre-hooks - they can modify the notification or skip processing\n\tmodifiedNotification, shouldContinue := snh.manager.processPreHooks(ctx, handlerCtx, notificationType, notification)\n\tif !shouldContinue {\n\t\treturn nil // Hooks decided to skip processing\n\t}\n\n\tvar err error\n\tswitch notificationType {\n\tcase NotificationMoving:\n\t\terr = snh.handleMoving(ctx, handlerCtx, modifiedNotification)\n\tcase NotificationMigrating:\n\t\terr = snh.handleMigrating(ctx, handlerCtx, modifiedNotification)\n\tcase NotificationMigrated:\n\t\terr = snh.handleMigrated(ctx, handlerCtx, modifiedNotification)\n\tcase NotificationFailingOver:\n\t\terr = snh.handleFailingOver(ctx, handlerCtx, modifiedNotification)\n\tcase NotificationFailedOver:\n\t\terr = snh.handleFailedOver(ctx, handlerCtx, modifiedNotification)\n\tcase NotificationSMigrating:\n\t\terr = snh.handleSMigrating(ctx, handlerCtx, modifiedNotification)\n\tcase NotificationSMigrated:\n\t\terr = snh.handleSMigrated(ctx, handlerCtx, modifiedNotification)\n\tdefault:\n\t\t// Ignore other notification types (e.g., pub/sub messages)\n\t\terr = nil\n\t}\n\n\t// Record maintenance notification metric\n\tif maintenanceCallback := pool.GetMetricMaintenanceNotificationCallback(); maintenanceCallback != nil {\n\t\tif conn, ok := handlerCtx.Conn.(*pool.Conn); ok {\n\t\t\tmaintenanceCallback(ctx, conn, notificationType)\n\t\t}\n\t}\n\n\t// Process post-hooks with the result\n\tsnh.manager.processPostHooks(ctx, handlerCtx, notificationType, modifiedNotification, err)\n\n\treturn err\n}\n\n// handleMoving processes MOVING notifications.\n// MOVING indicates that a connection should be handed off to a new endpoint.\n// This is a per-connection notification that triggers connection handoff.\n// Expected format: [\"MOVING\", seqNum, timeS, endpoint]\nfunc (snh *NotificationHandler) handleMoving(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\tif len(notification) < 3 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"MOVING\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\tseqID, ok := notification[1].(int64)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidSeqIDInMovingNotification(notification[1]))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Extract timeS\n\ttimeS, ok := notification[2].(int64)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidTimeSInMovingNotification(notification[2]))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tnewEndpoint := \"\"\n\tif len(notification) > 3 {\n\t\t// Extract new endpoint\n\t\tnewEndpoint, ok = notification[3].(string)\n\t\tif !ok {\n\t\t\tstringified := fmt.Sprintf(\"%v\", notification[3])\n\t\t\t// this could be <nil> which is valid\n\t\t\tif notification[3] == nil || stringified == internal.RedisNull {\n\t\t\t\tnewEndpoint = \"\"\n\t\t\t} else {\n\t\t\t\tinternal.Logger.Printf(ctx, logs.InvalidNewEndpointInMovingNotification(notification[3]))\n\t\t\t\treturn ErrInvalidNotification\n\t\t\t}\n\t\t}\n\t}\n\n\t// Get the connection that received this notification\n\tconn := handlerCtx.Conn\n\tif conn == nil {\n\t\tinternal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext(\"MOVING\"))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Type assert to get the underlying pool connection\n\tvar poolConn *pool.Conn\n\tif pc, ok := conn.(*pool.Conn); ok {\n\t\tpoolConn = pc\n\t} else {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext(\"MOVING\", conn, handlerCtx))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// If the connection is closed or not pooled, we can ignore the notification\n\t// this connection won't be remembered by the pool and will be garbage collected\n\t// Keep pubsub connections around since they are not pooled but are long-lived\n\t// and should be allowed to handoff (the pubsub instance will reconnect and change\n\t// the underlying *pool.Conn)\n\tif (poolConn.IsClosed() || !poolConn.IsPooled()) && !poolConn.IsPubSub() {\n\t\treturn nil\n\t}\n\n\tdeadline := time.Now().Add(time.Duration(timeS) * time.Second)\n\t// If newEndpoint is empty, we should schedule a handoff to the current endpoint in timeS/2 seconds\n\tif newEndpoint == \"\" || newEndpoint == internal.RedisNull {\n\t\tif internal.LogLevel.DebugOrAbove() {\n\t\t\tinternal.Logger.Printf(ctx, logs.SchedulingHandoffToCurrentEndpoint(poolConn.GetID(), float64(timeS)/2))\n\t\t}\n\t\t// same as current endpoint\n\t\tnewEndpoint = snh.manager.options.GetAddr()\n\t\t// delay the handoff for timeS/2 seconds to the same endpoint\n\t\t// do this in a goroutine to avoid blocking the notification handler\n\t\t// NOTE: This timer is started while parsing the notification, so the connection is not marked for handoff\n\t\t// and there should be no possibility of a race condition or double handoff.\n\t\ttime.AfterFunc(time.Duration(timeS/2)*time.Second, func() {\n\t\t\tif poolConn == nil || poolConn.IsClosed() {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := snh.markConnForHandoff(poolConn, newEndpoint, seqID, deadline); err != nil {\n\t\t\t\t// Log error but don't fail the goroutine - use background context since original may be cancelled\n\t\t\t\tinternal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(poolConn.GetID(), err))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Queue the handoff immediately if the connection is idle in the pool.\n\t\t\t// If the connection is in use (StateInUse), it will be queued when returned to the pool via OnPut.\n\t\t\t// This handles the case where the connection is idle and might never be retrieved again.\n\t\t\tif poolConn.GetStateMachine().GetState() == pool.StateIdle {\n\t\t\t\tif snh.manager.poolHooksRef != nil && snh.manager.poolHooksRef.workerManager != nil {\n\t\t\t\t\tif err := snh.manager.poolHooksRef.workerManager.queueHandoff(poolConn); err != nil {\n\t\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.FailedToQueueHandoff(poolConn.GetID(), err))\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Mark the connection as queued for handoff to prevent it from being retrieved\n\t\t\t\t\t\t// This transitions the connection to StateUnusable\n\t\t\t\t\t\tif err := poolConn.MarkQueuedForHandoff(); err != nil {\n\t\t\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(poolConn.GetID(), err))\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tinternal.Logger.Printf(context.Background(), logs.MarkedForHandoff(poolConn.GetID()))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// If connection is StateInUse, the handoff will be queued when it's returned to the pool\n\t\t})\n\t\treturn nil\n\t}\n\n\treturn snh.markConnForHandoff(poolConn, newEndpoint, seqID, deadline)\n}\n\nfunc (snh *NotificationHandler) markConnForHandoff(conn *pool.Conn, newEndpoint string, seqID int64, deadline time.Time) error {\n\tif err := conn.MarkForHandoff(newEndpoint, seqID); err != nil {\n\t\tinternal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(conn.GetID(), err))\n\t\t// Connection is already marked for handoff, which is acceptable\n\t\t// This can happen if multiple MOVING notifications are received for the same connection\n\t\treturn nil\n\t}\n\t// Optionally track in m\n\tif snh.operationsManager != nil {\n\t\tconnID := conn.GetID()\n\t\t// Track the operation (ignore errors since this is optional)\n\t\t_ = snh.operationsManager.TrackMovingOperationWithConnID(context.Background(), newEndpoint, deadline, seqID, connID)\n\t} else {\n\t\treturn errors.New(logs.ManagerNotInitialized())\n\t}\n\treturn nil\n}\n\n// handleMigrating processes MIGRATING notifications.\n// MIGRATING indicates that a connection migration is starting.\n// This is a per-connection notification that applies relaxed timeouts.\n// Expected format: [\"MIGRATING\", ...]\nfunc (snh *NotificationHandler) handleMigrating(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\tif len(notification) < 2 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"MIGRATING\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tif handlerCtx.Conn == nil {\n\t\tinternal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext(\"MIGRATING\"))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tconn, ok := handlerCtx.Conn.(*pool.Conn)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext(\"MIGRATING\", handlerCtx.Conn, handlerCtx))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Apply relaxed timeout to this specific connection\n\tif internal.LogLevel.InfoOrAbove() {\n\t\tinternal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(conn.GetID(), \"MIGRATING\", snh.manager.config.RelaxedTimeout))\n\t}\n\tconn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout)\n\n\t// Record relaxed timeout metric\n\tif relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil {\n\t\trelaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, \"MIGRATING\")\n\t}\n\n\treturn nil\n}\n\n// handleMigrated processes MIGRATED notifications.\n// MIGRATED indicates that a connection migration has completed.\n// This is a per-connection notification that clears relaxed timeouts.\n// Expected format: [\"MIGRATED\", ...]\nfunc (snh *NotificationHandler) handleMigrated(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\tif len(notification) < 2 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"MIGRATED\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tif handlerCtx.Conn == nil {\n\t\tinternal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext(\"MIGRATED\"))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tconn, ok := handlerCtx.Conn.(*pool.Conn)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext(\"MIGRATED\", handlerCtx.Conn, handlerCtx))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Clear relaxed timeout for this specific connection\n\tif internal.LogLevel.InfoOrAbove() {\n\t\tconnID := conn.GetID()\n\t\tinternal.Logger.Printf(ctx, logs.UnrelaxedTimeout(connID))\n\t}\n\tconn.ClearRelaxedTimeout()\n\treturn nil\n}\n\n// handleFailingOver processes FAILING_OVER notifications.\n// FAILING_OVER indicates that a failover is starting.\n// This is a per-connection notification that applies relaxed timeouts.\n// Expected format: [\"FAILING_OVER\", ...]\nfunc (snh *NotificationHandler) handleFailingOver(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\tif len(notification) < 2 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"FAILING_OVER\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tif handlerCtx.Conn == nil {\n\t\tinternal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext(\"FAILING_OVER\"))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tconn, ok := handlerCtx.Conn.(*pool.Conn)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext(\"FAILING_OVER\", handlerCtx.Conn, handlerCtx))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Apply relaxed timeout to this specific connection\n\tif internal.LogLevel.InfoOrAbove() {\n\t\tconnID := conn.GetID()\n\t\tinternal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(connID, \"FAILING_OVER\", snh.manager.config.RelaxedTimeout))\n\t}\n\tconn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout)\n\n\t// Record relaxed timeout metric\n\tif relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil {\n\t\trelaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, \"FAILING_OVER\")\n\t}\n\n\treturn nil\n}\n\n// handleFailedOver processes FAILED_OVER notifications.\n// FAILED_OVER indicates that a failover has completed.\n// This is a per-connection notification that clears relaxed timeouts.\n// Expected format: [\"FAILED_OVER\", ...]\nfunc (snh *NotificationHandler) handleFailedOver(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\tif len(notification) < 2 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"FAILED_OVER\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tif handlerCtx.Conn == nil {\n\t\tinternal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext(\"FAILED_OVER\"))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tconn, ok := handlerCtx.Conn.(*pool.Conn)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext(\"FAILED_OVER\", handlerCtx.Conn, handlerCtx))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Clear relaxed timeout for this specific connection\n\tif internal.LogLevel.InfoOrAbove() {\n\t\tconnID := conn.GetID()\n\t\tinternal.Logger.Printf(ctx, logs.UnrelaxedTimeout(connID))\n\t}\n\tconn.ClearRelaxedTimeout()\n\treturn nil\n}\n\n// handleSMigrating processes SMIGRATING notifications.\n// SMIGRATING indicates that a cluster slot is in the process of migrating to a different node.\n// This is a per-connection notification that applies relaxed timeouts during slot migration.\n// Expected format: [\"SMIGRATING\", SeqID, slot/range1-range2, ...]\nfunc (snh *NotificationHandler) handleSMigrating(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\tif len(notification) < 3 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATING\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Validate SeqID (position 1)\n\tif _, ok := notification[1].(int64); !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidSeqIDInSMigratingNotification(notification[1]))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tif handlerCtx.Conn == nil {\n\t\tinternal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext(\"SMIGRATING\"))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tconn, ok := handlerCtx.Conn.(*pool.Conn)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext(\"SMIGRATING\", handlerCtx.Conn, handlerCtx))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Apply relaxed timeout to this specific connection\n\tif internal.LogLevel.InfoOrAbove() {\n\t\tinternal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(conn.GetID(), \"SMIGRATING\", snh.manager.config.RelaxedTimeout))\n\t}\n\tconn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout)\n\treturn nil\n}\n\n// handleSMigrated processes SMIGRATED notifications.\n// SMIGRATED indicates that a cluster slot has finished migrating to a different node.\n// This is a cluster-level notification that triggers cluster state reload.\n//\n// Expected RESP3 format:\n//\n//\t>3\n//\t+SMIGRATED\n//\t:SeqID\n//\t*<num_entries>       <- array of triplet arrays\n//\t  *3                 <- each triplet is a 3-element array\n//\t    +<source>        <- node from which slots are migrating FROM\n//\t    +<destination>   <- node to which slots are migrating TO\n//\t    +<slots>         <- comma-separated slots and/or ranges (e.g., \"123,789-1000\")\n//\n// A source and target endpoint may appear in multiple triplets.\n// The notification is only processed if the connection's NodeAddress matches one of the source endpoints.\n//\n// Note: Multiple connections may receive the same notification, so we deduplicate by SeqID before triggering reload.\n// but we still process the notification on each connection to clear the relaxed timeout.\n// In the case when the connection is from MOVED/ASK, the connection's original endpoint is not set,\n// so we will not be able to match the source endpoint. In such case, we will trigger the reload callback with the first target endpoint.\nfunc (snh *NotificationHandler) handleSMigrated(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {\n\t// Expected: [\"SMIGRATED\", SeqID, [[source, target, slots], ...]]\n\t// Minimum 3 elements: SMIGRATED, SeqID, and the array of triplets\n\tif len(notification) < 3 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATED\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Extract SeqID (position 1)\n\tseqID, ok := notification[1].(int64)\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidSeqIDInSMigratedNotification(notification[1]))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Extract the array of triplets (position 2)\n\ttriplets, ok := notification[2].([]interface{})\n\tif !ok {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATED (triplets array)\", notification[2]))\n\t\treturn ErrInvalidNotification\n\t}\n\n\tif len(triplets) == 0 {\n\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATED (empty triplets)\", notification))\n\t\treturn ErrInvalidNotification\n\t}\n\n\t// Get the connection's endpoints to check if this notification is relevant\n\t// We check against both nodeAddress (from CLUSTER SLOTS) and addr (after resolution)\n\t// since we cannot be certain which format the notification source will use\n\tvar connectionNodeAddress string\n\tvar connectionAddr string\n\tif snh.manager.options != nil {\n\t\tconnectionNodeAddress = snh.manager.options.GetNodeAddress()\n\t\tconnectionAddr = snh.manager.options.GetAddr()\n\t}\n\n\t// Helper function to check if source matches either of our endpoints\n\t// notification source can be either the node address or the addr after resolution\n\tsourceMatchesConnection := func(source string) bool {\n\t\tif source == connectionNodeAddress {\n\t\t\treturn true\n\t\t}\n\t\tif source == connectionAddr {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\t// Parse triplets and check if any source matches our connection's endpoints\n\tvar matchingTriplets []struct {\n\t\tsource string\n\t\ttarget string\n\t\tslots  string\n\t}\n\tvar allSlotRanges []string\n\n\tfor _, tripletInterface := range triplets {\n\t\t// Each triplet should be a 3-element array: [source, target, slots]\n\t\ttriplet, ok := tripletInterface.([]interface{})\n\t\tif !ok || len(triplet) != 3 {\n\t\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATED (triplet format)\", tripletInterface))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Extract source endpoint\n\t\tsource, ok := triplet[0].(string)\n\t\tif !ok {\n\t\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATED (source)\", triplet[0]))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Extract target endpoint\n\t\ttarget, ok := triplet[1].(string)\n\t\tif !ok {\n\t\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATED (target)\", triplet[1]))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Extract slots\n\t\tslots, ok := triplet[2].(string)\n\t\tif !ok {\n\t\t\tinternal.Logger.Printf(ctx, logs.InvalidNotification(\"SMIGRATED (slots)\", triplet[2]))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if this triplet's source matches our connection's endpoints\n\t\tif sourceMatchesConnection(source) {\n\t\t\tmatchingTriplets = append(matchingTriplets, struct {\n\t\t\t\tsource string\n\t\t\t\ttarget string\n\t\t\t\tslots  string\n\t\t\t}{source, target, slots})\n\t\t\tslotRanges := strings.Split(slots, \",\")\n\t\t\tallSlotRanges = append(allSlotRanges, slotRanges...)\n\t\t}\n\t}\n\n\tvar connID uint64\n\t// Reset relaxed timeout for this specific connection\n\tif handlerCtx.Conn != nil {\n\t\tconn, ok := handlerCtx.Conn.(*pool.Conn)\n\t\tif ok {\n\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\tconnID = conn.GetID()\n\t\t\t\tinternal.Logger.Printf(ctx, logs.UnrelaxedTimeout(connID))\n\t\t\t}\n\t\t\tconn.ClearRelaxedTimeout()\n\t\t}\n\t}\n\n\t// If no matching triplets, this notification is not relevant to this connection\n\tif len(matchingTriplets) == 0 {\n\t\treturn nil\n\t}\n\n\t// Deduplicate by SeqID - multiple connections may receive the same notification\n\t// Only trigger cluster state reload once per seqID\n\tif snh.manager.MarkSMigratedSeqIDProcessed(seqID) {\n\t\t// Use the first matching triplet\n\t\ttarget := matchingTriplets[0].target\n\t\tslotsForLog := allSlotRanges\n\n\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\tinternal.Logger.Printf(ctx, logs.TriggeringClusterStateReload(seqID, target, slotsForLog))\n\t\t}\n\n\t\t// Trigger cluster state reload via callback\n\t\tsnh.manager.TriggerClusterStateReload(ctx, target, slotsForLog)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "maintnotifications/push_notification_handler_test.go",
    "content": "package maintnotifications\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/interfaces\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// testOptions implements interfaces.OptionsInterface for testing SMIGRATED\ntype testOptions struct {\n\tnodeAddress string\n}\n\nfunc (to *testOptions) GetReadTimeout() time.Duration  { return 5 * time.Second }\nfunc (to *testOptions) GetWriteTimeout() time.Duration { return 5 * time.Second }\nfunc (to *testOptions) GetNetwork() string             { return \"tcp\" }\nfunc (to *testOptions) GetAddr() string                { return \"localhost:6379\" }\nfunc (to *testOptions) GetNodeAddress() string         { return to.nodeAddress }\nfunc (to *testOptions) IsTLSEnabled() bool             { return false }\nfunc (to *testOptions) GetProtocol() int               { return 3 }\nfunc (to *testOptions) GetPoolSize() int               { return 10 }\nfunc (to *testOptions) NewDialer() func(context.Context) (net.Conn, error) {\n\treturn func(ctx context.Context) (net.Conn, error) { return nil, nil }\n}\n\nfunc createTestManager(nodeAddress string) *Manager {\n\tconfig := DefaultConfig()\n\tvar opts interfaces.OptionsInterface\n\tif nodeAddress != \"\" {\n\t\topts = &testOptions{nodeAddress: nodeAddress}\n\t}\n\treturn &Manager{\n\t\tconfig:  config,\n\t\toptions: opts,\n\t}\n}\n\n// TestHandleSMigrated_CorrectFormat tests that handleSMigrated correctly parses the nested array format\nfunc TestHandleSMigrated_CorrectFormat(t *testing.T) {\n\t// Create a manager with matching original endpoint\n\tmanager := createTestManager(\"127.0.0.1:6379\")\n\n\thandler := &NotificationHandler{\n\t\tmanager: manager,\n\t}\n\n\t// Create notification in the correct nested array format:\n\t// [\"SMIGRATED\", SeqID, [[source, target, slots], [source, target, slots], ...]]\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(12346),\n\t\t[]interface{}{\n\t\t\t[]interface{}{\"127.0.0.1:6379\", \"127.0.0.1:6380\", \"123,456,789-1000\"},\n\t\t\t[]interface{}{\"127.0.0.1:6379\", \"127.0.0.1:6381\", \"124,457,300-500\"},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\thandlerCtx := push.NotificationHandlerContext{\n\t\tConn: nil, // No connection needed for this test\n\t}\n\n\t// This should not return an error\n\terr := handler.handleSMigrated(ctx, handlerCtx, notification)\n\tif err != nil {\n\t\tt.Errorf(\"handleSMigrated failed with correct format: %v\", err)\n\t}\n}\n\n// TestHandleSMigrated_SingleTriplet tests parsing with a single triplet\nfunc TestHandleSMigrated_SingleTriplet(t *testing.T) {\n\tmanager := createTestManager(\"127.0.0.1:6379\")\n\n\thandler := &NotificationHandler{\n\t\tmanager: manager,\n\t}\n\n\t// Single triplet in nested array format\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(100),\n\t\t[]interface{}{\n\t\t\t[]interface{}{\"127.0.0.1:6379\", \"127.0.0.1:6380\", \"1000,2000-3000\"},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\thandlerCtx := push.NotificationHandlerContext{\n\t\tConn: nil,\n\t}\n\n\terr := handler.handleSMigrated(ctx, handlerCtx, notification)\n\tif err != nil {\n\t\tt.Errorf(\"handleSMigrated failed with single triplet: %v\", err)\n\t}\n}\n\n// TestHandleSMigrated_NoMatchingSource tests that notification is ignored when source doesn't match\nfunc TestHandleSMigrated_NoMatchingSource(t *testing.T) {\n\t// Create a manager with a different original endpoint\n\tmanager := createTestManager(\"127.0.0.1:9999\")\n\n\thandler := &NotificationHandler{\n\t\tmanager: manager,\n\t}\n\n\t// Notification with source that doesn't match our endpoint\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(200),\n\t\t[]interface{}{\n\t\t\t[]interface{}{\"127.0.0.1:6379\", \"127.0.0.1:6380\", \"1000,2000-3000\"},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\thandlerCtx := push.NotificationHandlerContext{\n\t\tConn: nil,\n\t}\n\n\t// Should not return an error, just silently ignore\n\terr := handler.handleSMigrated(ctx, handlerCtx, notification)\n\tif err != nil {\n\t\tt.Errorf(\"handleSMigrated should not error when source doesn't match: %v\", err)\n\t}\n}\n\n// TestHandleSMigrated_IncompleteTriplet tests that incomplete triplets are skipped\nfunc TestHandleSMigrated_IncompleteTriplet(t *testing.T) {\n\tmanager := createTestManager(\"127.0.0.1:6379\")\n\n\thandler := &NotificationHandler{\n\t\tmanager: manager,\n\t}\n\n\t// Triplet with only 2 elements (missing slots) - should be skipped\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(300),\n\t\t[]interface{}{\n\t\t\t[]interface{}{\"127.0.0.1:6379\", \"127.0.0.1:6380\"}, // Missing slots\n\t\t},\n\t}\n\n\tctx := context.Background()\n\thandlerCtx := push.NotificationHandlerContext{\n\t\tConn: nil,\n\t}\n\n\t// Should not error, just skip the malformed triplet\n\terr := handler.handleSMigrated(ctx, handlerCtx, notification)\n\tif err != nil {\n\t\tt.Errorf(\"handleSMigrated should not error with incomplete triplet (just skip it): %v\", err)\n\t}\n}\n\n// TestHandleSMigrated_TooFewElements tests that notifications with too few elements are rejected\nfunc TestHandleSMigrated_TooFewElements(t *testing.T) {\n\tmanager := createTestManager(\"127.0.0.1:6379\")\n\n\thandler := &NotificationHandler{\n\t\tmanager: manager,\n\t}\n\n\t// Only SMIGRATED and SeqID, no triplets array\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(400),\n\t}\n\n\tctx := context.Background()\n\thandlerCtx := push.NotificationHandlerContext{\n\t\tConn: nil,\n\t}\n\n\terr := handler.handleSMigrated(ctx, handlerCtx, notification)\n\tif err == nil {\n\t\tt.Error(\"handleSMigrated should error with too few elements\")\n\t}\n}\n\n// TestHandleSMigrated_EmptyTripletsArray tests that empty triplets array is rejected\nfunc TestHandleSMigrated_EmptyTripletsArray(t *testing.T) {\n\tmanager := createTestManager(\"127.0.0.1:6379\")\n\n\thandler := &NotificationHandler{\n\t\tmanager: manager,\n\t}\n\n\t// Empty triplets array\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(500),\n\t\t[]interface{}{},\n\t}\n\n\tctx := context.Background()\n\thandlerCtx := push.NotificationHandlerContext{\n\t\tConn: nil,\n\t}\n\n\terr := handler.handleSMigrated(ctx, handlerCtx, notification)\n\tif err == nil {\n\t\tt.Error(\"handleSMigrated should error with empty triplets array\")\n\t}\n}\n\n// TestHandleSMigrated_InvalidTripletsType tests that non-array triplets is rejected\nfunc TestHandleSMigrated_InvalidTripletsType(t *testing.T) {\n\tmanager := createTestManager(\"127.0.0.1:6379\")\n\n\thandler := &NotificationHandler{\n\t\tmanager: manager,\n\t}\n\n\t// Triplets is a string instead of array\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(600),\n\t\t\"not an array\",\n\t}\n\n\tctx := context.Background()\n\thandlerCtx := push.NotificationHandlerContext{\n\t\tConn: nil,\n\t}\n\n\terr := handler.handleSMigrated(ctx, handlerCtx, notification)\n\tif err == nil {\n\t\tt.Error(\"handleSMigrated should error with invalid triplets type\")\n\t}\n}\n"
  },
  {
    "path": "maintnotifications/state.go",
    "content": "package maintnotifications\n\n// State represents the current state of a maintenance operation\ntype State int\n\nconst (\n\t// StateIdle indicates no upgrade is in progress\n\tStateIdle State = iota\n\n\t// StateHandoff indicates a connection handoff is in progress\n\tStateMoving\n)\n\n// String returns a string representation of the state.\nfunc (s State) String() string {\n\tswitch s {\n\tcase StateIdle:\n\t\treturn \"idle\"\n\tcase StateMoving:\n\t\treturn \"moving\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n"
  },
  {
    "path": "monitor_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// This test is for manual use and is not part of the CI of Go-Redis.\nvar _ = Describe(\"Monitor command\", Label(\"monitor\"), func() {\n\tctx := context.TODO()\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tif os.Getenv(\"RUN_MONITOR_TEST\") != \"true\" {\n\t\t\tSkip(\"Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.\")\n\t\t}\n\t\tclient = redis.NewClient(&redis.Options{Addr: redisPort})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should monitor\", Label(\"monitor\"), func() {\n\t\tress := make(chan string)\n\t\tclient1 := redis.NewClient(&redis.Options{Addr: redisPort})\n\t\tmn := client1.Monitor(ctx, ress)\n\t\tmn.Start()\n\t\t// Wait for the Redis server to be in monitoring mode.\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tclient.Set(ctx, \"foo\", \"bar\", 0)\n\t\tclient.Set(ctx, \"bar\", \"baz\", 0)\n\t\tclient.Set(ctx, \"bap\", 8, 0)\n\t\tclient.Get(ctx, \"bap\")\n\t\tlst := []string{}\n\t\tfor i := 0; i < 5; i++ {\n\t\t\ts := <-ress\n\t\t\tlst = append(lst, s)\n\t\t}\n\t\tmn.Stop()\n\t\tExpect(lst[0]).To(ContainSubstring(\"OK\"))\n\t\tExpect(lst[1]).To(ContainSubstring(`\"set\" \"foo\" \"bar\"`))\n\t\tExpect(lst[2]).To(ContainSubstring(`\"set\" \"bar\" \"baz\"`))\n\t\tExpect(lst[3]).To(ContainSubstring(`\"set\" \"bap\" \"8\"`))\n\t})\n})\n\nfunc TestMonitorCommand(t *testing.T) {\n\tif os.Getenv(\"RUN_MONITOR_TEST\") != \"true\" {\n\t\tt.Skip(\"Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.\")\n\t}\n\n\tctx := context.TODO()\n\tclient := redis.NewClient(&redis.Options{Addr: redisPort})\n\tif err := client.FlushDB(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"FlushDB failed: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tif err := client.Close(); err != nil {\n\t\t\tt.Fatalf(\"Close failed: %v\", err)\n\t\t}\n\t}()\n\n\tress := make(chan string, 10)                               // Buffer to prevent blocking\n\tclient1 := redis.NewClient(&redis.Options{Addr: redisPort}) // Adjust the Addr field as necessary\n\tmn := client1.Monitor(ctx, ress)\n\tmn.Start()\n\t// Wait for the Redis server to be in monitoring mode.\n\ttime.Sleep(100 * time.Millisecond)\n\tclient.Set(ctx, \"foo\", \"bar\", 0)\n\tclient.Set(ctx, \"bar\", \"baz\", 0)\n\tclient.Set(ctx, \"bap\", 8, 0)\n\tclient.Get(ctx, \"bap\")\n\tmn.Stop()\n\tvar lst []string\n\tfor i := 0; i < 5; i++ {\n\t\ts := <-ress\n\t\tlst = append(lst, s)\n\t}\n\n\t// Assertions\n\tif !containsSubstring(lst[0], \"OK\") {\n\t\tt.Errorf(\"Expected lst[0] to contain 'OK', got %s\", lst[0])\n\t}\n\tif !containsSubstring(lst[1], `\"set\" \"foo\" \"bar\"`) {\n\t\tt.Errorf(`Expected lst[1] to contain '\"set\" \"foo\" \"bar\"', got %s`, lst[1])\n\t}\n\tif !containsSubstring(lst[2], `\"set\" \"bar\" \"baz\"`) {\n\t\tt.Errorf(`Expected lst[2] to contain '\"set\" \"bar\" \"baz\"', got %s`, lst[2])\n\t}\n\tif !containsSubstring(lst[3], `\"set\" \"bap\" \"8\"`) {\n\t\tt.Errorf(`Expected lst[3] to contain '\"set\" \"bap\" \"8\"', got %s`, lst[3])\n\t}\n}\n\nfunc containsSubstring(s, substr string) bool {\n\treturn strings.Contains(s, substr)\n}\n"
  },
  {
    "path": "options.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/util\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// poolIDCounter is a global auto-increment counter for generating unique pool IDs.\nvar poolIDCounter atomic.Uint64\n\n// generateUniqueID generates a short unique identifier for pool names using auto-increment.\n// This makes it easier to identify and track pools in order of creation.\nfunc generateUniqueID() string {\n\tid := poolIDCounter.Add(1)\n\treturn strconv.FormatUint(id, 10)\n}\n\n// Limiter is the interface of a rate limiter or a circuit breaker.\ntype Limiter interface {\n\t// Allow returns nil if operation is allowed or an error otherwise.\n\t// If operation is allowed client must ReportResult of the operation\n\t// whether it is a success or a failure.\n\tAllow() error\n\t// ReportResult reports the result of the previously allowed operation.\n\t// nil indicates a success, non-nil error usually indicates a failure.\n\tReportResult(result error)\n}\n\n// Options keeps the settings to set up redis connection.\ntype Options struct {\n\t// Network type, either tcp or unix.\n\t//\n\t// default: is tcp.\n\tNetwork string\n\n\t// Addr is the address formated as host:port\n\tAddr string\n\n\t// NodeAddress is the address of the Redis node as reported by the server.\n\t// For cluster clients, this is the exact endpoint string returned by CLUSTER SLOTS\n\t// before any resolution or transformation (e.g., loopback replacement).\n\t// For standalone clients, this defaults to Addr.\n\t//\n\t// This is used to match the source endpoint in maintenance notifications\n\t// (e.g. SMIGRATED).\n\t//\n\t// Use Client.NodeAddress() to access this value.\n\tNodeAddress string\n\n\t// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.\n\tClientName string\n\n\t// Dialer creates new network connection and has priority over\n\t// Network and Addr options.\n\tDialer func(ctx context.Context, network, addr string) (net.Conn, error)\n\n\t// Hook that is called when new connection is established.\n\tOnConnect func(ctx context.Context, cn *Conn) error\n\n\t// Protocol 2 or 3. Use the version to negotiate RESP version with redis-server.\n\t//\n\t// default: 3.\n\tProtocol int\n\n\t// Username is used to authenticate the current connection\n\t// with one of the connections defined in the ACL list when connecting\n\t// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.\n\tUsername string\n\n\t// Password is an optional password. Must match the password specified in the\n\t// `requirepass` server configuration option (if connecting to a Redis 5.0 instance, or lower),\n\t// or the User Password when connecting to a Redis 6.0 instance, or greater,\n\t// that is using the Redis ACL system.\n\tPassword string\n\n\t// CredentialsProvider allows the username and password to be updated\n\t// before reconnecting. It should return the current username and password.\n\tCredentialsProvider func() (username string, password string)\n\n\t// CredentialsProviderContext is an enhanced parameter of CredentialsProvider,\n\t// done to maintain API compatibility. In the future,\n\t// there might be a merge between CredentialsProviderContext and CredentialsProvider.\n\t// There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider.\n\tCredentialsProviderContext func(ctx context.Context) (username string, password string, err error)\n\n\t// StreamingCredentialsProvider is used to retrieve the credentials\n\t// for the connection from an external source. Those credentials may change\n\t// during the connection lifetime. This is useful for managed identity\n\t// scenarios where the credentials are retrieved from an external source.\n\t//\n\t// Currently, this is a placeholder for the future implementation.\n\tStreamingCredentialsProvider auth.StreamingCredentialsProvider\n\n\t// DB is the database to be selected after connecting to the server.\n\tDB int\n\n\t// MaxRetries is the maximum number of retries before giving up.\n\t// -1 (not 0) disables retries.\n\t//\n\t// default: 3 retries\n\tMaxRetries int\n\n\t// MinRetryBackoff is the minimum backoff between each retry.\n\t// -1 disables backoff.\n\t//\n\t// default: 8 milliseconds\n\tMinRetryBackoff time.Duration\n\n\t// MaxRetryBackoff is the maximum backoff between each retry.\n\t// -1 disables backoff.\n\t// default: 512 milliseconds;\n\tMaxRetryBackoff time.Duration\n\n\t// DialTimeout for establishing new connections.\n\t//\n\t// default: 5 seconds\n\tDialTimeout time.Duration\n\n\t// DialerRetries is the maximum number of retry attempts when dialing fails.\n\t//\n\t// default: 5\n\tDialerRetries int\n\n\t// DialerRetryTimeout is the backoff duration between retry attempts.\n\t//\n\t// default: 100 milliseconds\n\tDialerRetryTimeout time.Duration\n\n\t// DialerRetryBackoff controls the delay between dial retry attempts.\n\t//\n\t// attempt is 0-based: attempt=0 is the delay after the 1st failed dial (before the 2nd attempt).\n\t//\n\t// If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms).\n\tDialerRetryBackoff func(attempt int) time.Duration\n\n\t// ReadTimeout for socket reads. If reached, commands will fail\n\t// with a timeout instead of blocking. Supported values:\n\t//\n\t//\t- `-1` - no timeout (block indefinitely).\n\t//\t- `-2` - disables SetReadDeadline calls completely.\n\t//\n\t// default: 3 seconds\n\tReadTimeout time.Duration\n\n\t// WriteTimeout for socket writes. If reached, commands will fail\n\t// with a timeout instead of blocking.  Supported values:\n\t//\n\t//\t- `-1` - no timeout (block indefinitely).\n\t//\t- `-2` - disables SetWriteDeadline calls completely.\n\t//\n\t// default: 3 seconds\n\tWriteTimeout time.Duration\n\n\t// ContextTimeoutEnabled controls whether the client respects context timeouts and deadlines.\n\t// See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts\n\tContextTimeoutEnabled bool\n\n\t// ReadBufferSize is the size of the bufio.Reader buffer for each connection.\n\t// Larger buffers can improve performance for commands that return large responses.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tReadBufferSize int\n\n\t// WriteBufferSize is the size of the bufio.Writer buffer for each connection.\n\t// Larger buffers can improve performance for large pipelines and commands with many arguments.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tWriteBufferSize int\n\n\t// PoolFIFO type of connection pool.\n\t//\n\t//\t- true for FIFO pool\n\t//\t- false for LIFO pool.\n\t//\n\t// Note that FIFO has slightly higher overhead compared to LIFO,\n\t// but it helps closing idle connections faster reducing the pool size.\n\t// default: false\n\tPoolFIFO bool\n\n\t// PoolSize is the base number of socket connections.\n\t// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.\n\t// If there is not enough connections in the pool, new connections will be allocated in excess of PoolSize,\n\t// you can limit it through MaxActiveConns\n\t//\n\t// default: 10 * runtime.GOMAXPROCS(0)\n\tPoolSize int\n\n\t// MaxConcurrentDials is the maximum number of concurrent connection creation goroutines.\n\t// If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize.\n\tMaxConcurrentDials int\n\n\t// PoolTimeout is the amount of time client waits for connection if all connections\n\t// are busy before returning an error.\n\t//\n\t// default: ReadTimeout + 1 second\n\tPoolTimeout time.Duration\n\n\t// MinIdleConns is the minimum number of idle connections which is useful when establishing\n\t// new connection is slow. The idle connections are not closed by default.\n\t//\n\t// default: 0\n\tMinIdleConns int\n\n\t// MaxIdleConns is the maximum number of idle connections.\n\t// The idle connections are not closed by default.\n\t//\n\t// default: 0\n\tMaxIdleConns int\n\n\t// MaxActiveConns is the maximum number of connections allocated by the pool at a given time.\n\t// When zero, there is no limit on the number of connections in the pool.\n\t// If the pool is full, the next call to Get() will block until a connection is released.\n\t//\n\t// default: 0\n\tMaxActiveConns int\n\n\t// ConnMaxIdleTime is the maximum amount of time a connection may be idle.\n\t// Should be less than server's timeout.\n\t//\n\t// Expired connections may be closed lazily before reuse.\n\t// If d <= 0, connections are not closed due to a connection's idle time.\n\t// -1 disables idle timeout check.\n\t//\n\t// default: 30 minutes\n\tConnMaxIdleTime time.Duration\n\n\t// ConnMaxLifetime is the maximum amount of time a connection may be reused.\n\t//\n\t// Expired connections may be closed lazily before reuse.\n\t// If <= 0, connections are not closed due to a connection's age.\n\t//\n\t// default: 0\n\tConnMaxLifetime time.Duration\n\n\t// ConnMaxLifetimeJitter is the absolute jitter duration applied to ConnMaxLifetime\n\t// to prevent all connections from expiring simultaneously.\n\t//\n\t// The jitter is applied as a random offset in the range [-jitter, +jitter].\n\t// For example, if ConnMaxLifetime is 1 hour and ConnMaxLifetimeJitter is 6 minutes,\n\t// connections will expire between 54 minutes and 66 minutes.\n\t//\n\t// If <= 0, no jitter is applied.\n\t// If > ConnMaxLifetime, it will be capped at ConnMaxLifetime.\n\t//\n\t// default: 0\n\tConnMaxLifetimeJitter time.Duration\n\n\t// TLSConfig to use. When set, TLS will be negotiated.\n\tTLSConfig *tls.Config\n\n\t// Limiter interface used to implement circuit breaker or rate limiter.\n\tLimiter Limiter\n\n\t// readOnly enables read only queries on slave/follower nodes.\n\treadOnly bool\n\n\t// DisableIndentity - Disable set-lib on connect.\n\t//\n\t// default: false\n\t//\n\t// Deprecated: Use DisableIdentity instead.\n\tDisableIndentity bool\n\n\t// DisableIdentity is used to disable CLIENT SETINFO command on connect.\n\t//\n\t// default: false\n\tDisableIdentity bool\n\n\t// Add suffix to client name. Default is empty.\n\t// IdentitySuffix - add suffix to client name.\n\tIdentitySuffix string\n\n\t// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.\n\t// When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult\n\tUnstableResp3 bool\n\n\t// Push notifications are always enabled for RESP3 connections (Protocol: 3)\n\t// and are not available for RESP2 connections. No configuration option is needed.\n\n\t// PushNotificationProcessor is the processor for handling push notifications.\n\t// If nil, a default processor will be created for RESP3 connections.\n\tPushNotificationProcessor push.NotificationProcessor\n\n\t// FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing.\n\t// When a node is marked as failing, it will be avoided for this duration.\n\t// Default is 15 seconds.\n\tFailingTimeoutSeconds int\n\n\t// MaintNotificationsConfig provides custom configuration for maintnotifications.\n\t// When MaintNotificationsConfig.Mode is not \"disabled\", the client will handle\n\t// cluster upgrade notifications gracefully and manage connection/pool state\n\t// transitions seamlessly. Requires Protocol: 3 (RESP3) for push notifications.\n\t// If nil, maintnotifications are in \"auto\" mode and will be enabled if the server supports it.\n\tMaintNotificationsConfig *maintnotifications.Config\n}\n\nfunc (opt *Options) init() {\n\tif opt.Addr == \"\" {\n\t\topt.Addr = \"localhost:6379\"\n\t}\n\tif opt.Network == \"\" {\n\t\tif strings.HasPrefix(opt.Addr, \"/\") {\n\t\t\topt.Network = \"unix\"\n\t\t} else {\n\t\t\topt.Network = \"tcp\"\n\t\t}\n\t}\n\t// For standalone clients, default NodeAddress to Addr if not set.\n\t// This ensures maintenance notifications (SMIGRATED, etc.) can match\n\t// the connection's endpoint even for non-cluster clients.\n\tif opt.NodeAddress == \"\" {\n\t\topt.NodeAddress = opt.Addr\n\t}\n\tif opt.Protocol < 2 {\n\t\topt.Protocol = 3\n\t}\n\tif opt.DialTimeout == 0 {\n\t\topt.DialTimeout = 5 * time.Second\n\t}\n\tif opt.DialerRetries == 0 {\n\t\topt.DialerRetries = 5\n\t}\n\tif opt.DialerRetryTimeout == 0 {\n\t\topt.DialerRetryTimeout = 100 * time.Millisecond\n\t}\n\tif opt.Dialer == nil {\n\t\topt.Dialer = NewDialer(opt)\n\t}\n\tif opt.PoolSize == 0 {\n\t\topt.PoolSize = 10 * runtime.GOMAXPROCS(0)\n\t}\n\tif opt.MaxConcurrentDials <= 0 {\n\t\topt.MaxConcurrentDials = opt.PoolSize\n\t} else if opt.MaxConcurrentDials > opt.PoolSize {\n\t\topt.MaxConcurrentDials = opt.PoolSize\n\t}\n\tif opt.ReadBufferSize == 0 {\n\t\topt.ReadBufferSize = proto.DefaultBufferSize\n\t}\n\tif opt.WriteBufferSize == 0 {\n\t\topt.WriteBufferSize = proto.DefaultBufferSize\n\t}\n\tswitch opt.ReadTimeout {\n\tcase -2:\n\t\topt.ReadTimeout = -1\n\tcase -1:\n\t\topt.ReadTimeout = 0\n\tcase 0:\n\t\topt.ReadTimeout = 3 * time.Second\n\t}\n\tswitch opt.WriteTimeout {\n\tcase -2:\n\t\topt.WriteTimeout = -1\n\tcase -1:\n\t\topt.WriteTimeout = 0\n\tcase 0:\n\t\topt.WriteTimeout = opt.ReadTimeout\n\t}\n\tif opt.PoolTimeout == 0 {\n\t\tif opt.ReadTimeout > 0 {\n\t\t\topt.PoolTimeout = opt.ReadTimeout + time.Second\n\t\t} else {\n\t\t\topt.PoolTimeout = 30 * time.Second\n\t\t}\n\t}\n\tif opt.ConnMaxIdleTime == 0 {\n\t\topt.ConnMaxIdleTime = 30 * time.Minute\n\t}\n\n\topt.ConnMaxLifetimeJitter = min(opt.ConnMaxLifetimeJitter, opt.ConnMaxLifetime)\n\n\tswitch opt.MaxRetries {\n\tcase -1:\n\t\topt.MaxRetries = 0\n\tcase 0:\n\t\topt.MaxRetries = 3\n\t}\n\tswitch opt.MinRetryBackoff {\n\tcase -1:\n\t\topt.MinRetryBackoff = 0\n\tcase 0:\n\t\topt.MinRetryBackoff = 8 * time.Millisecond\n\t}\n\tswitch opt.MaxRetryBackoff {\n\tcase -1:\n\t\topt.MaxRetryBackoff = 0\n\tcase 0:\n\t\topt.MaxRetryBackoff = 512 * time.Millisecond\n\t}\n\n\tif opt.FailingTimeoutSeconds == 0 {\n\t\topt.FailingTimeoutSeconds = 15\n\t}\n\n\topt.MaintNotificationsConfig = opt.MaintNotificationsConfig.ApplyDefaultsWithPoolConfig(opt.PoolSize, opt.MaxActiveConns)\n\n\t// auto-detect endpoint type if not specified\n\tendpointType := opt.MaintNotificationsConfig.EndpointType\n\tif endpointType == \"\" || endpointType == maintnotifications.EndpointTypeAuto {\n\t\t// Auto-detect endpoint type if not specified\n\t\tendpointType = maintnotifications.DetectEndpointType(opt.Addr, opt.TLSConfig != nil)\n\t}\n\topt.MaintNotificationsConfig.EndpointType = endpointType\n}\n\nfunc (opt *Options) clone() *Options {\n\tclone := *opt\n\n\t// Deep clone MaintNotificationsConfig to avoid sharing between clients\n\tif opt.MaintNotificationsConfig != nil {\n\t\tconfigClone := *opt.MaintNotificationsConfig\n\t\tclone.MaintNotificationsConfig = &configClone\n\t}\n\n\treturn &clone\n}\n\n// NewDialer returns a function that will be used as the default dialer\n// when none is specified in Options.Dialer.\nfunc (opt *Options) NewDialer() func(context.Context, string, string) (net.Conn, error) {\n\treturn NewDialer(opt)\n}\n\n// NewDialer returns a function that will be used as the default dialer\n// when none is specified in Options.Dialer.\nfunc NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, error) {\n\treturn func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\tnetDialer := &net.Dialer{\n\t\t\tTimeout:   opt.DialTimeout,\n\t\t\tKeepAlive: 5 * time.Minute,\n\t\t}\n\t\tif opt.TLSConfig == nil {\n\t\t\treturn netDialer.DialContext(ctx, network, addr)\n\t\t}\n\t\treturn tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig)\n\t}\n}\n\n// ParseURL parses a URL into Options that can be used to connect to Redis.\n// Scheme is required.\n// There are two connection types: by tcp socket and by unix socket.\n// Tcp connection:\n//\n//\tredis://<user>:<password>@<host>:<port>/<db_number>\n//\n// Unix connection:\n//\n//\tunix://<user>:<password>@</path/to/redis.sock>?db=<db_number>\n//\n// Most Option fields can be set using query parameters, with the following restrictions:\n//   - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries\n//   - only scalar type fields are supported (bool, int, time.Duration)\n//   - for time.Duration fields, values must be a valid input for time.ParseDuration();\n//     additionally a plain integer as value (i.e. without unit) is interpreted as seconds\n//   - to disable a duration field, use value less than or equal to 0; to use the default\n//     value, leave the value blank or remove the parameter\n//   - only the last value is interpreted if a parameter is given multiple times\n//   - fields \"network\", \"addr\", \"username\" and \"password\" can only be set using other\n//     URL attributes (scheme, host, userinfo, resp.), query parameters using these\n//     names will be treated as unknown parameters\n//   - unknown parameter names will result in an error\n//   - use \"skip_verify=true\" to ignore TLS certificate validation\n//\n// Examples:\n//\n//\tredis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2\n//\tis equivalent to:\n//\t&Options{\n//\t\tNetwork:     \"tcp\",\n//\t\tAddr:        \"localhost:6789\",\n//\t\tDB:          1,               // path \"/3\" was overridden by \"&db=1\"\n//\t\tDialTimeout: 3 * time.Second, // no time unit = seconds\n//\t\tReadTimeout: 6 * time.Second,\n//\t\tMaxRetries:  2,\n//\t}\nfunc ParseURL(redisURL string) (*Options, error) {\n\tu, err := url.Parse(redisURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch u.Scheme {\n\tcase \"redis\", \"rediss\":\n\t\treturn setupTCPConn(u)\n\tcase \"unix\":\n\t\treturn setupUnixConn(u)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: invalid URL scheme: %s\", u.Scheme)\n\t}\n}\n\nfunc setupTCPConn(u *url.URL) (*Options, error) {\n\to := &Options{Network: \"tcp\"}\n\n\to.Username, o.Password = getUserPassword(u)\n\n\th, p := getHostPortWithDefaults(u)\n\to.Addr = net.JoinHostPort(h, p)\n\n\tf := strings.FieldsFunc(u.Path, func(r rune) bool {\n\t\treturn r == '/'\n\t})\n\tswitch len(f) {\n\tcase 0:\n\t\to.DB = 0\n\tcase 1:\n\t\tvar err error\n\t\tif o.DB, err = strconv.Atoi(f[0]); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"redis: invalid database number: %q\", f[0])\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: invalid URL path: %s\", u.Path)\n\t}\n\n\tif u.Scheme == \"rediss\" {\n\t\to.TLSConfig = &tls.Config{\n\t\t\tServerName: h,\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t}\n\t}\n\n\treturn setupConnParams(u, o)\n}\n\n// getHostPortWithDefaults is a helper function that splits the url into\n// a host and a port. If the host is missing, it defaults to localhost\n// and if the port is missing, it defaults to 6379.\nfunc getHostPortWithDefaults(u *url.URL) (string, string) {\n\thost, port, err := net.SplitHostPort(u.Host)\n\tif err != nil {\n\t\thost = u.Host\n\t}\n\tif host == \"\" {\n\t\thost = \"localhost\"\n\t}\n\tif port == \"\" {\n\t\tport = \"6379\"\n\t}\n\treturn host, port\n}\n\nfunc setupUnixConn(u *url.URL) (*Options, error) {\n\to := &Options{\n\t\tNetwork: \"unix\",\n\t}\n\n\tif strings.TrimSpace(u.Path) == \"\" { // path is required with unix connection\n\t\treturn nil, errors.New(\"redis: empty unix socket path\")\n\t}\n\to.Addr = u.Path\n\to.Username, o.Password = getUserPassword(u)\n\treturn setupConnParams(u, o)\n}\n\ntype queryOptions struct {\n\tq   url.Values\n\terr error\n}\n\nfunc (o *queryOptions) has(name string) bool {\n\treturn len(o.q[name]) > 0\n}\n\nfunc (o *queryOptions) string(name string) string {\n\tvs := o.q[name]\n\tif len(vs) == 0 {\n\t\treturn \"\"\n\t}\n\tdelete(o.q, name) // enable detection of unknown parameters\n\treturn vs[len(vs)-1]\n}\n\nfunc (o *queryOptions) strings(name string) []string {\n\tvs := o.q[name]\n\tdelete(o.q, name)\n\treturn vs\n}\n\nfunc (o *queryOptions) int(name string) int {\n\ts := o.string(name)\n\tif s == \"\" {\n\t\treturn 0\n\t}\n\ti, err := strconv.Atoi(s)\n\tif err == nil {\n\t\treturn i\n\t}\n\tif o.err == nil {\n\t\to.err = fmt.Errorf(\"redis: invalid %s number: %s\", name, err)\n\t}\n\treturn 0\n}\n\nfunc (o *queryOptions) duration(name string) time.Duration {\n\ts := o.string(name)\n\tif s == \"\" {\n\t\treturn 0\n\t}\n\t// try plain number first\n\tif i, err := strconv.Atoi(s); err == nil {\n\t\tif i <= 0 {\n\t\t\t// disable timeouts\n\t\t\treturn -1\n\t\t}\n\t\treturn time.Duration(i) * time.Second\n\t}\n\tdur, err := time.ParseDuration(s)\n\tif err == nil {\n\t\treturn dur\n\t}\n\tif o.err == nil {\n\t\to.err = fmt.Errorf(\"redis: invalid %s duration: %w\", name, err)\n\t}\n\treturn 0\n}\n\nfunc (o *queryOptions) bool(name string) bool {\n\tswitch s := o.string(name); s {\n\tcase \"true\", \"1\":\n\t\treturn true\n\tcase \"false\", \"0\", \"\":\n\t\treturn false\n\tdefault:\n\t\tif o.err == nil {\n\t\t\to.err = fmt.Errorf(\"redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q\", name, s)\n\t\t}\n\t\treturn false\n\t}\n}\n\nfunc (o *queryOptions) remaining() []string {\n\tif len(o.q) == 0 {\n\t\treturn nil\n\t}\n\tkeys := make([]string, 0, len(o.q))\n\tfor k := range o.q {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\treturn keys\n}\n\n// setupConnParams converts query parameters in u to option value in o.\nfunc setupConnParams(u *url.URL, o *Options) (*Options, error) {\n\tq := queryOptions{q: u.Query()}\n\n\t// compat: a future major release may use q.int(\"db\")\n\tif tmp := q.string(\"db\"); tmp != \"\" {\n\t\tdb, err := strconv.Atoi(tmp)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"redis: invalid database number: %w\", err)\n\t\t}\n\t\to.DB = db\n\t}\n\n\to.Protocol = q.int(\"protocol\")\n\to.ClientName = q.string(\"client_name\")\n\to.MaxRetries = q.int(\"max_retries\")\n\to.MinRetryBackoff = q.duration(\"min_retry_backoff\")\n\to.MaxRetryBackoff = q.duration(\"max_retry_backoff\")\n\to.DialTimeout = q.duration(\"dial_timeout\")\n\to.ReadTimeout = q.duration(\"read_timeout\")\n\to.WriteTimeout = q.duration(\"write_timeout\")\n\to.PoolFIFO = q.bool(\"pool_fifo\")\n\to.PoolSize = q.int(\"pool_size\")\n\to.PoolTimeout = q.duration(\"pool_timeout\")\n\to.MinIdleConns = q.int(\"min_idle_conns\")\n\to.MaxIdleConns = q.int(\"max_idle_conns\")\n\to.MaxActiveConns = q.int(\"max_active_conns\")\n\to.MaxConcurrentDials = q.int(\"max_concurrent_dials\")\n\tif q.has(\"conn_max_idle_time\") {\n\t\to.ConnMaxIdleTime = q.duration(\"conn_max_idle_time\")\n\t} else {\n\t\to.ConnMaxIdleTime = q.duration(\"idle_timeout\")\n\t}\n\tif q.has(\"conn_max_lifetime\") {\n\t\to.ConnMaxLifetime = q.duration(\"conn_max_lifetime\")\n\t} else {\n\t\to.ConnMaxLifetime = q.duration(\"max_conn_age\")\n\t}\n\tif q.has(\"conn_max_lifetime_jitter\") {\n\t\to.ConnMaxLifetimeJitter = min(q.duration(\"conn_max_lifetime_jitter\"), o.ConnMaxLifetime)\n\t}\n\tif q.err != nil {\n\t\treturn nil, q.err\n\t}\n\tif o.TLSConfig != nil && q.has(\"skip_verify\") {\n\t\to.TLSConfig.InsecureSkipVerify = q.bool(\"skip_verify\")\n\t}\n\n\t// any parameters left?\n\tif r := q.remaining(); len(r) > 0 {\n\t\treturn nil, fmt.Errorf(\"redis: unexpected option: %s\", strings.Join(r, \", \"))\n\t}\n\n\treturn o, nil\n}\n\nfunc getUserPassword(u *url.URL) (string, string) {\n\tvar user, password string\n\tif u.User != nil {\n\t\tuser = u.User.Username()\n\t\tif p, ok := u.User.Password(); ok {\n\t\t\tpassword = p\n\t\t}\n\t}\n\treturn user, password\n}\n\nfunc newConnPool(\n\topt *Options,\n\tdialer func(ctx context.Context, network, addr string) (net.Conn, error),\n\tpoolName string,\n) (*pool.ConnPool, error) {\n\tpoolSize, err := util.SafeIntToInt32(opt.PoolSize, \"PoolSize\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tminIdleConns, err := util.SafeIntToInt32(opt.MinIdleConns, \"MinIdleConns\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmaxIdleConns, err := util.SafeIntToInt32(opt.MaxIdleConns, \"MaxIdleConns\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmaxActiveConns, err := util.SafeIntToInt32(opt.MaxActiveConns, \"MaxActiveConns\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pool.NewConnPool(&pool.Options{\n\t\tDialer: func(ctx context.Context) (net.Conn, error) {\n\t\t\treturn dialer(ctx, opt.Network, opt.Addr)\n\t\t},\n\t\tPoolFIFO:                 opt.PoolFIFO,\n\t\tPoolSize:                 poolSize,\n\t\tMaxConcurrentDials:       opt.MaxConcurrentDials,\n\t\tPoolTimeout:              opt.PoolTimeout,\n\t\tDialTimeout:              opt.DialTimeout,\n\t\tDialerRetries:            opt.DialerRetries,\n\t\tDialerRetryTimeout:       opt.DialerRetryTimeout,\n\t\tDialerRetryBackoff:       opt.DialerRetryBackoff,\n\t\tMinIdleConns:             minIdleConns,\n\t\tMaxIdleConns:             maxIdleConns,\n\t\tMaxActiveConns:           maxActiveConns,\n\t\tConnMaxIdleTime:          opt.ConnMaxIdleTime,\n\t\tConnMaxLifetime:          opt.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter:    opt.ConnMaxLifetimeJitter,\n\t\tReadBufferSize:           opt.ReadBufferSize,\n\t\tWriteBufferSize:          opt.WriteBufferSize,\n\t\tPushNotificationsEnabled: opt.Protocol == 3,\n\t\tName:                     poolName,\n\t}), nil\n}\n\nfunc newPubSubPool(\n\topt *Options,\n\tdialer func(ctx context.Context, network, addr string) (net.Conn, error),\n\tpoolName string,\n) (*pool.PubSubPool, error) {\n\tpoolSize, err := util.SafeIntToInt32(opt.PoolSize, \"PoolSize\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tminIdleConns, err := util.SafeIntToInt32(opt.MinIdleConns, \"MinIdleConns\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmaxIdleConns, err := util.SafeIntToInt32(opt.MaxIdleConns, \"MaxIdleConns\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmaxActiveConns, err := util.SafeIntToInt32(opt.MaxActiveConns, \"MaxActiveConns\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pool.NewPubSubPool(&pool.Options{\n\t\tPoolFIFO:                 opt.PoolFIFO,\n\t\tPoolSize:                 poolSize,\n\t\tMaxConcurrentDials:       opt.MaxConcurrentDials,\n\t\tPoolTimeout:              opt.PoolTimeout,\n\t\tDialTimeout:              opt.DialTimeout,\n\t\tDialerRetries:            opt.DialerRetries,\n\t\tDialerRetryTimeout:       opt.DialerRetryTimeout,\n\t\tDialerRetryBackoff:       opt.DialerRetryBackoff,\n\t\tMinIdleConns:             minIdleConns,\n\t\tMaxIdleConns:             maxIdleConns,\n\t\tMaxActiveConns:           maxActiveConns,\n\t\tConnMaxIdleTime:          opt.ConnMaxIdleTime,\n\t\tConnMaxLifetime:          opt.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter:    opt.ConnMaxLifetimeJitter,\n\t\tReadBufferSize:           32 * 1024,\n\t\tWriteBufferSize:          32 * 1024,\n\t\tPushNotificationsEnabled: opt.Protocol == 3,\n\t\tName:                     poolName,\n\t}, dialer), nil\n}\n"
  },
  {
    "path": "options_test.go",
    "content": "//go:build go1.7\n\npackage redis\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\nfunc TestParseURL(t *testing.T) {\n\tcases := []struct {\n\t\turl string\n\t\to   *Options // expected value\n\t\terr error\n\t}{\n\t\t{\n\t\t\turl: \"redis://localhost:123/1\",\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 1},\n\t\t}, {\n\t\t\turl: \"redis://localhost:123\",\n\t\t\to:   &Options{Addr: \"localhost:123\"},\n\t\t}, {\n\t\t\turl: \"redis://localhost/1\",\n\t\t\to:   &Options{Addr: \"localhost:6379\", DB: 1},\n\t\t}, {\n\t\t\turl: \"redis://12345\",\n\t\t\to:   &Options{Addr: \"12345:6379\"},\n\t\t}, {\n\t\t\turl: \"rediss://localhost:123\",\n\t\t\to:   &Options{Addr: \"localhost:123\", TLSConfig: &tls.Config{ /* no deep comparison */ }},\n\t\t}, {\n\t\t\turl: \"rediss://localhost:123/?skip_verify=true\",\n\t\t\to:   &Options{Addr: \"localhost:123\", TLSConfig: &tls.Config{InsecureSkipVerify: true}},\n\t\t}, {\n\t\t\turl: \"redis://:bar@localhost:123\",\n\t\t\to:   &Options{Addr: \"localhost:123\", Password: \"bar\"},\n\t\t}, {\n\t\t\turl: \"redis://foo@localhost:123\",\n\t\t\to:   &Options{Addr: \"localhost:123\", Username: \"foo\"},\n\t\t}, {\n\t\t\turl: \"redis://foo:bar@localhost:123\",\n\t\t\to:   &Options{Addr: \"localhost:123\", Username: \"foo\", Password: \"bar\"},\n\t\t}, {\n\t\t\t// multiple params\n\t\t\turl: \"redis://localhost:123/?db=2&read_timeout=2&pool_fifo=true\",\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 2, ReadTimeout: 2 * time.Second, PoolFIFO: true},\n\t\t}, {\n\t\t\t// special case handling for disabled timeouts\n\t\t\turl: \"redis://localhost:123/?db=2&conn_max_idle_time=0\",\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 2, ConnMaxIdleTime: -1},\n\t\t}, {\n\t\t\t// negative values disable timeouts as well\n\t\t\turl: \"redis://localhost:123/?db=2&conn_max_idle_time=-1\",\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 2, ConnMaxIdleTime: -1},\n\t\t}, {\n\t\t\t// absent timeout values will use defaults\n\t\t\turl: \"redis://localhost:123/?db=2&conn_max_idle_time=\",\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 2, ConnMaxIdleTime: 0},\n\t\t}, {\n\t\t\turl: \"redis://localhost:123/?db=2&conn_max_idle_time\", // missing \"=\" at the end\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 2, ConnMaxIdleTime: 0},\n\t\t}, {\n\t\t\turl: \"redis://localhost:123/?db=2&client_name=hi\", // client name\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 2, ClientName: \"hi\"},\n\t\t}, {\n\t\t\turl: \"redis://localhost:123/?db=2&protocol=2\", // RESP Protocol\n\t\t\to:   &Options{Addr: \"localhost:123\", DB: 2, Protocol: 2},\n\t\t}, {\n\t\t\turl: \"redis://localhost:123/?max_concurrent_dials=5\", // MaxConcurrentDials parameter\n\t\t\to:   &Options{Addr: \"localhost:123\", MaxConcurrentDials: 5},\n\t\t}, {\n\t\t\turl: \"redis://localhost:123/?max_concurrent_dials=0\", // MaxConcurrentDials zero value\n\t\t\to:   &Options{Addr: \"localhost:123\", MaxConcurrentDials: 0},\n\t\t}, {\n\t\t\turl: \"redis://localhost:123/?conn_max_lifetime=1h&conn_max_lifetime_jitter=6m\",\n\t\t\to:   &Options{Addr: \"localhost:123\", ConnMaxLifetime: time.Hour, ConnMaxLifetimeJitter: 6 * time.Minute},\n\t\t}, {\n\t\t\t// jitter > lifetime should be capped\n\t\t\turl: \"redis://localhost:123/?conn_max_lifetime=30m&conn_max_lifetime_jitter=1h\",\n\t\t\to:   &Options{Addr: \"localhost:123\", ConnMaxLifetime: 30 * time.Minute, ConnMaxLifetimeJitter: 30 * time.Minute},\n\t\t}, {\n\t\t\t// jitter without lifetime should be capped to 0\n\t\t\turl: \"redis://localhost:123/?conn_max_lifetime_jitter=6m\",\n\t\t\to:   &Options{Addr: \"localhost:123\", ConnMaxLifetimeJitter: 0},\n\t\t}, {\n\t\t\turl: \"unix:///tmp/redis.sock\",\n\t\t\to:   &Options{Addr: \"/tmp/redis.sock\"},\n\t\t}, {\n\t\t\turl: \"unix://foo:bar@/tmp/redis.sock\",\n\t\t\to:   &Options{Addr: \"/tmp/redis.sock\", Username: \"foo\", Password: \"bar\"},\n\t\t}, {\n\t\t\turl: \"unix://foo:bar@/tmp/redis.sock?db=3\",\n\t\t\to:   &Options{Addr: \"/tmp/redis.sock\", Username: \"foo\", Password: \"bar\", DB: 3},\n\t\t}, {\n\t\t\t// invalid db format\n\t\t\turl: \"unix://foo:bar@/tmp/redis.sock?db=test\",\n\t\t\terr: errors.New(`redis: invalid database number: strconv.Atoi: parsing \"test\": invalid syntax`),\n\t\t}, {\n\t\t\t// invalid int value\n\t\t\turl: \"redis://localhost/?pool_size=five\",\n\t\t\terr: errors.New(`redis: invalid pool_size number: strconv.Atoi: parsing \"five\": invalid syntax`),\n\t\t}, {\n\t\t\t// invalid bool value\n\t\t\turl: \"redis://localhost/?pool_fifo=yes\",\n\t\t\terr: errors.New(`redis: invalid pool_fifo boolean: expected true/false/1/0 or an empty string, got \"yes\"`),\n\t\t}, {\n\t\t\t// it returns first error\n\t\t\turl: \"redis://localhost/?db=foo&pool_size=five\",\n\t\t\terr: errors.New(`redis: invalid database number: strconv.Atoi: parsing \"foo\": invalid syntax`),\n\t\t}, {\n\t\t\turl: \"redis://localhost/?abc=123\",\n\t\t\terr: errors.New(\"redis: unexpected option: abc\"),\n\t\t}, {\n\t\t\turl: \"redis://foo@localhost/?username=bar\",\n\t\t\terr: errors.New(\"redis: unexpected option: username\"),\n\t\t}, {\n\t\t\turl: \"redis://localhost/?wrte_timout=10s&abc=123\",\n\t\t\terr: errors.New(\"redis: unexpected option: abc, wrte_timout\"),\n\t\t}, {\n\t\t\turl: \"http://google.com\",\n\t\t\terr: errors.New(\"redis: invalid URL scheme: http\"),\n\t\t}, {\n\t\t\turl: \"redis://localhost/1/2/3/4\",\n\t\t\terr: errors.New(\"redis: invalid URL path: /1/2/3/4\"),\n\t\t}, {\n\t\t\turl: \"12345\",\n\t\t\terr: errors.New(\"redis: invalid URL scheme: \"),\n\t\t}, {\n\t\t\turl: \"redis://localhost/iamadatabase\",\n\t\t\terr: errors.New(`redis: invalid database number: \"iamadatabase\"`),\n\t\t},\n\t}\n\n\tfor i := range cases {\n\t\ttc := cases[i]\n\t\tt.Run(tc.url, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := ParseURL(tc.url)\n\t\t\tif tc.err == nil && err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %q\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tc.err != nil && err != nil {\n\t\t\t\tif tc.err.Error() != err.Error() {\n\t\t\t\t\tt.Fatalf(\"got %q, expected %q\", err, tc.err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcomprareOptions(t, actual, tc.o)\n\t\t})\n\t}\n}\n\nfunc comprareOptions(t *testing.T, actual, expected *Options) {\n\tt.Helper()\n\n\tif actual.Addr != expected.Addr {\n\t\tt.Errorf(\"got %q, want %q\", actual.Addr, expected.Addr)\n\t}\n\tif actual.DB != expected.DB {\n\t\tt.Errorf(\"DB: got %q, expected %q\", actual.DB, expected.DB)\n\t}\n\tif actual.TLSConfig == nil && expected.TLSConfig != nil {\n\t\tt.Errorf(\"got nil TLSConfig, expected a TLSConfig\")\n\t}\n\tif actual.TLSConfig != nil && expected.TLSConfig == nil {\n\t\tt.Errorf(\"got TLSConfig, expected no TLSConfig\")\n\t}\n\tif actual.Username != expected.Username {\n\t\tt.Errorf(\"Username: got %q, expected %q\", actual.Username, expected.Username)\n\t}\n\tif actual.Password != expected.Password {\n\t\tt.Errorf(\"Password: got %q, expected %q\", actual.Password, expected.Password)\n\t}\n\tif actual.MaxRetries != expected.MaxRetries {\n\t\tt.Errorf(\"MaxRetries: got %v, expected %v\", actual.MaxRetries, expected.MaxRetries)\n\t}\n\tif actual.MinRetryBackoff != expected.MinRetryBackoff {\n\t\tt.Errorf(\"MinRetryBackoff: got %v, expected %v\", actual.MinRetryBackoff, expected.MinRetryBackoff)\n\t}\n\tif actual.MaxRetryBackoff != expected.MaxRetryBackoff {\n\t\tt.Errorf(\"MaxRetryBackoff: got %v, expected %v\", actual.MaxRetryBackoff, expected.MaxRetryBackoff)\n\t}\n\tif actual.DialTimeout != expected.DialTimeout {\n\t\tt.Errorf(\"DialTimeout: got %v, expected %v\", actual.DialTimeout, expected.DialTimeout)\n\t}\n\tif actual.ReadTimeout != expected.ReadTimeout {\n\t\tt.Errorf(\"ReadTimeout: got %v, expected %v\", actual.ReadTimeout, expected.ReadTimeout)\n\t}\n\tif actual.WriteTimeout != expected.WriteTimeout {\n\t\tt.Errorf(\"WriteTimeout: got %v, expected %v\", actual.WriteTimeout, expected.WriteTimeout)\n\t}\n\tif actual.PoolFIFO != expected.PoolFIFO {\n\t\tt.Errorf(\"PoolFIFO: got %v, expected %v\", actual.PoolFIFO, expected.PoolFIFO)\n\t}\n\tif actual.PoolSize != expected.PoolSize {\n\t\tt.Errorf(\"PoolSize: got %v, expected %v\", actual.PoolSize, expected.PoolSize)\n\t}\n\tif actual.PoolTimeout != expected.PoolTimeout {\n\t\tt.Errorf(\"PoolTimeout: got %v, expected %v\", actual.PoolTimeout, expected.PoolTimeout)\n\t}\n\tif actual.MinIdleConns != expected.MinIdleConns {\n\t\tt.Errorf(\"MinIdleConns: got %v, expected %v\", actual.MinIdleConns, expected.MinIdleConns)\n\t}\n\tif actual.MaxIdleConns != expected.MaxIdleConns {\n\t\tt.Errorf(\"MaxIdleConns: got %v, expected %v\", actual.MaxIdleConns, expected.MaxIdleConns)\n\t}\n\tif actual.ConnMaxIdleTime != expected.ConnMaxIdleTime {\n\t\tt.Errorf(\"ConnMaxIdleTime: got %v, expected %v\", actual.ConnMaxIdleTime, expected.ConnMaxIdleTime)\n\t}\n\tif actual.ConnMaxLifetime != expected.ConnMaxLifetime {\n\t\tt.Errorf(\"ConnMaxLifetime: got %v, expected %v\", actual.ConnMaxLifetime, expected.ConnMaxLifetime)\n\t}\n\tif actual.ConnMaxLifetimeJitter != expected.ConnMaxLifetimeJitter {\n\t\tt.Errorf(\"ConnMaxLifetimeJitter: got %v, expected %v\", actual.ConnMaxLifetimeJitter, expected.ConnMaxLifetimeJitter)\n\t}\n\tif actual.MaxConcurrentDials != expected.MaxConcurrentDials {\n\t\tt.Errorf(\"MaxConcurrentDials: got %v, expected %v\", actual.MaxConcurrentDials, expected.MaxConcurrentDials)\n\t}\n}\n\n// Test ReadTimeout option initialization, including special values -1 and 0.\n// And also test behaviour of WriteTimeout option, when it is not explicitly set and use\n// ReadTimeout value.\nfunc TestReadTimeoutOptions(t *testing.T) {\n\ttestDataInputOutputMap := map[time.Duration]time.Duration{\n\t\t-1: 0 * time.Second,\n\t\t0:  3 * time.Second,\n\t\t1:  1 * time.Nanosecond,\n\t\t3:  3 * time.Nanosecond,\n\t}\n\n\tfor in, out := range testDataInputOutputMap {\n\t\to := &Options{ReadTimeout: in}\n\t\to.init()\n\t\tif o.ReadTimeout != out {\n\t\t\tt.Errorf(\"got %d instead of %d as ReadTimeout option\", o.ReadTimeout, out)\n\t\t}\n\n\t\tif o.WriteTimeout != o.ReadTimeout {\n\t\t\tt.Errorf(\"got %d instead of %d as WriteTimeout option\", o.WriteTimeout, o.ReadTimeout)\n\t\t}\n\t}\n}\n\nfunc TestProtocolOptions(t *testing.T) {\n\ttestCasesMap := map[int]int{\n\t\t0: 3,\n\t\t1: 3,\n\t\t2: 2,\n\t\t3: 3,\n\t}\n\n\to := &Options{}\n\to.init()\n\tif o.Protocol != 3 {\n\t\tt.Errorf(\"got %d instead of %d as protocol option\", o.Protocol, 3)\n\t}\n\n\tfor set, want := range testCasesMap {\n\t\to := &Options{Protocol: set}\n\t\to.init()\n\t\tif o.Protocol != want {\n\t\t\tt.Errorf(\"got %d instead of %d as protocol option\", o.Protocol, want)\n\t\t}\n\t}\n}\n\nfunc TestMaxConcurrentDialsOptions(t *testing.T) {\n\t// Test cases for MaxConcurrentDials initialization logic\n\ttestCases := []struct {\n\t\tname                    string\n\t\tpoolSize                int\n\t\tmaxConcurrentDials      int\n\t\texpectedConcurrentDials int\n\t}{\n\t\t// Edge cases and invalid values - negative/zero values set to PoolSize\n\t\t{\n\t\t\tname:                    \"negative value gets set to pool size\",\n\t\t\tpoolSize:                10,\n\t\t\tmaxConcurrentDials:      -1,\n\t\t\texpectedConcurrentDials: 10, // negative values are set to PoolSize\n\t\t},\n\t\t// Zero value tests - MaxConcurrentDials should be set to PoolSize\n\t\t{\n\t\t\tname:                    \"zero value with positive pool size\",\n\t\t\tpoolSize:                1,\n\t\t\tmaxConcurrentDials:      0,\n\t\t\texpectedConcurrentDials: 1, // MaxConcurrentDials = PoolSize when 0\n\t\t},\n\t\t// Explicit positive value tests\n\t\t{\n\t\t\tname:                    \"explicit value within limit\",\n\t\t\tpoolSize:                10,\n\t\t\tmaxConcurrentDials:      3,\n\t\t\texpectedConcurrentDials: 3, // should remain unchanged when < PoolSize\n\t\t},\n\t\t// Capping tests - values exceeding PoolSize should be capped\n\t\t{\n\t\t\tname:                    \"value exceeding pool size\",\n\t\t\tpoolSize:                5,\n\t\t\tmaxConcurrentDials:      10,\n\t\t\texpectedConcurrentDials: 5, // should be capped at PoolSize\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\topts := &Options{\n\t\t\t\tPoolSize:           tc.poolSize,\n\t\t\t\tMaxConcurrentDials: tc.maxConcurrentDials,\n\t\t\t}\n\t\t\topts.init()\n\n\t\t\tif opts.MaxConcurrentDials != tc.expectedConcurrentDials {\n\t\t\t\tt.Errorf(\"MaxConcurrentDials: got %v, expected %v (PoolSize=%v)\",\n\t\t\t\t\topts.MaxConcurrentDials, tc.expectedConcurrentDials, opts.PoolSize)\n\t\t\t}\n\n\t\t\t// Ensure MaxConcurrentDials never exceeds PoolSize (for all inputs)\n\t\t\tif opts.MaxConcurrentDials > opts.PoolSize {\n\t\t\t\tt.Errorf(\"MaxConcurrentDials (%v) should not exceed PoolSize (%v)\",\n\t\t\t\t\topts.MaxConcurrentDials, opts.PoolSize)\n\t\t\t}\n\n\t\t\t// Ensure MaxConcurrentDials is always positive (for all inputs)\n\t\t\tif opts.MaxConcurrentDials <= 0 {\n\t\t\t\tt.Errorf(\"MaxConcurrentDials should be positive, got %v\", opts.MaxConcurrentDials)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClusterOptionsDialerRetries(t *testing.T) {\n\tclusterOpt := &ClusterOptions{\n\t\tDialerRetries:      10,\n\t\tDialerRetryTimeout: 200 * time.Millisecond,\n\t}\n\n\topt := clusterOpt.clientOptions()\n\n\tif opt.DialerRetries != 10 {\n\t\tt.Errorf(\"expected DialerRetries=10, got %d\", opt.DialerRetries)\n\t}\n\tif opt.DialerRetryTimeout != 200*time.Millisecond {\n\t\tt.Errorf(\"expected DialerRetryTimeout=200ms, got %v\", opt.DialerRetryTimeout)\n\t}\n}\n\nfunc TestRingOptionsDialerRetries(t *testing.T) {\n\tringOpt := &RingOptions{\n\t\tDialerRetries:      10,\n\t\tDialerRetryTimeout: 200 * time.Millisecond,\n\t}\n\n\topt := ringOpt.clientOptions()\n\n\tif opt.DialerRetries != 10 {\n\t\tt.Errorf(\"expected DialerRetries=10, got %d\", opt.DialerRetries)\n\t}\n\tif opt.DialerRetryTimeout != 200*time.Millisecond {\n\t\tt.Errorf(\"expected DialerRetryTimeout=200ms, got %v\", opt.DialerRetryTimeout)\n\t}\n}\n\nfunc TestFailoverOptionsDialerRetries(t *testing.T) {\n\tfailoverOpt := &FailoverOptions{\n\t\tDialerRetries:      10,\n\t\tDialerRetryTimeout: 200 * time.Millisecond,\n\t}\n\n\topt := failoverOpt.clientOptions()\n\n\tif opt.DialerRetries != 10 {\n\t\tt.Errorf(\"expected DialerRetries=10, got %d\", opt.DialerRetries)\n\t}\n\tif opt.DialerRetryTimeout != 200*time.Millisecond {\n\t\tt.Errorf(\"expected DialerRetryTimeout=200ms, got %v\", opt.DialerRetryTimeout)\n\t}\n\n\t// Also verify sentinelOptions passes them through\n\tsentinelOpt := failoverOpt.sentinelOptions(\"localhost:26379\")\n\tif sentinelOpt.DialerRetries != 10 {\n\t\tt.Errorf(\"expected sentinel DialerRetries=10, got %d\", sentinelOpt.DialerRetries)\n\t}\n\tif sentinelOpt.DialerRetryTimeout != 200*time.Millisecond {\n\t\tt.Errorf(\"expected sentinel DialerRetryTimeout=200ms, got %v\", sentinelOpt.DialerRetryTimeout)\n\t}\n}\n\n// TestOptionsCloneMaintNotificationsRace verifies that cloning options via\n// baseClient is safe when initConn concurrently writes MaintNotificationsConfig.Mode.\n// Run with -race to detect the data race.\nfunc TestOptionsCloneMaintNotificationsRace(t *testing.T) {\n\topt := &Options{\n\t\tAddr: \"localhost:6379\",\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeAuto,\n\t\t},\n\t}\n\n\tbc := baseClient{opt: opt}\n\n\tvar wg sync.WaitGroup\n\tconst iterations = 1000\n\n\t// Writer: simulates initConn toggling MaintNotificationsConfig.Mode under optLock\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < iterations; i++ {\n\t\t\tbc.optLock.Lock()\n\t\t\tbc.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeDisabled\n\t\t\tbc.optLock.Unlock()\n\n\t\t\tbc.optLock.Lock()\n\t\t\tbc.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeAuto\n\t\t\tbc.optLock.Unlock()\n\t\t}\n\t}()\n\n\t// Reader: simulates newTx / withTimeout calling cloneOpt() (acquires RLock)\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor i := 0; i < iterations; i++ {\n\t\t\tcloned := bc.cloneOpt()\n\t\t\t_ = cloned\n\t\t}\n\t}()\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "osscluster.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"net\"\n\t\"net/url\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n\t\"github.com/redis/go-redis/v9/internal/otel\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/rand\"\n\t\"github.com/redis/go-redis/v9/internal/routing\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\nconst (\n\tminLatencyMeasurementInterval = 10 * time.Second\n)\n\nvar (\n\terrClusterNoNodes = errors.New(\"redis: cluster has no nodes\")\n\terrNoWatchKeys    = errors.New(\"redis: Watch requires at least one key\")\n\terrWatchCrosslot  = errors.New(\"redis: Watch requires all keys to be in the same slot\")\n)\n\n// ClusterOptions are used to configure a cluster client and should be\n// passed to NewClusterClient.\ntype ClusterOptions struct {\n\t// A seed list of host:port addresses of cluster nodes.\n\tAddrs []string\n\n\t// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.\n\tClientName string\n\n\t// NewClient creates a cluster node client with provided name and options.\n\t// If NewClient is set by the user, the user is responsible for handling maintnotifications upgrades and push notifications.\n\tNewClient func(opt *Options) *Client\n\n\t// The maximum number of retries before giving up. Command is retried\n\t// on network errors and MOVED/ASK redirects.\n\t// Default is 3 retries.\n\tMaxRedirects int\n\n\t// Enables read-only commands on slave nodes.\n\tReadOnly bool\n\t// Allows routing read-only commands to the closest master or slave node.\n\t// It automatically enables ReadOnly.\n\tRouteByLatency bool\n\t// Allows routing read-only commands to the random master or slave node.\n\t// It automatically enables ReadOnly.\n\tRouteRandomly bool\n\n\t// Optional function that returns cluster slots information.\n\t// It is useful to manually create cluster of standalone Redis servers\n\t// and load-balance read/write operations between master and slaves.\n\t// It can use service like ZooKeeper to maintain configuration information\n\t// and Cluster.ReloadState to manually trigger state reloading.\n\tClusterSlots func(context.Context) ([]ClusterSlot, error)\n\n\t// Following options are copied from Options struct.\n\n\tDialer func(ctx context.Context, network, addr string) (net.Conn, error)\n\n\tOnConnect func(ctx context.Context, cn *Conn) error\n\n\tProtocol                     int\n\tUsername                     string\n\tPassword                     string\n\tCredentialsProvider          func() (username string, password string)\n\tCredentialsProviderContext   func(ctx context.Context) (username string, password string, err error)\n\tStreamingCredentialsProvider auth.StreamingCredentialsProvider\n\n\t// MaxRetries is the maximum number of retries before giving up.\n\t// For ClusterClient, retries are disabled by default (set to -1),\n\t// because the cluster client handles all kinds of retries internally.\n\t// This is intentional and differs from the standalone Options default.\n\tMaxRetries      int\n\tMinRetryBackoff time.Duration\n\tMaxRetryBackoff time.Duration\n\n\tDialTimeout time.Duration\n\n\t// DialerRetries is the maximum number of retry attempts when dialing fails.\n\t//\n\t// default: 5\n\tDialerRetries int\n\n\t// DialerRetryTimeout is the backoff duration between retry attempts.\n\t//\n\t// default: 100 milliseconds\n\tDialerRetryTimeout time.Duration\n\n\t// DialerRetryBackoff controls the delay between dial retry attempts.\n\t// See Options.DialerRetryBackoff for details.\n\tDialerRetryBackoff func(attempt int) time.Duration\n\n\tReadTimeout           time.Duration\n\tWriteTimeout          time.Duration\n\tContextTimeoutEnabled bool\n\n\t// MaxConcurrentDials is the maximum number of concurrent connection creation goroutines.\n\t// If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize.\n\tMaxConcurrentDials int\n\n\tPoolFIFO              bool\n\tPoolSize              int // applies per cluster node and not for the whole cluster\n\tPoolTimeout           time.Duration\n\tMinIdleConns          int\n\tMaxIdleConns          int\n\tMaxActiveConns        int // applies per cluster node and not for the whole cluster\n\tConnMaxIdleTime       time.Duration\n\tConnMaxLifetime       time.Duration\n\tConnMaxLifetimeJitter time.Duration\n\n\t// ReadBufferSize is the size of the bufio.Reader buffer for each connection.\n\t// Larger buffers can improve performance for commands that return large responses.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tReadBufferSize int\n\n\t// WriteBufferSize is the size of the bufio.Writer buffer for each connection.\n\t// Larger buffers can improve performance for large pipelines and commands with many arguments.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tWriteBufferSize int\n\n\tTLSConfig *tls.Config\n\n\t// DisableRoutingPolicies disables the request/response policy routing system.\n\t// When disabled, all commands use the legacy routing behavior.\n\t// Experimental. Will be removed when shard picker is fully implemented.\n\tDisableRoutingPolicies bool\n\n\t// DisableIndentity - Disable set-lib on connect.\n\t//\n\t// default: false\n\t//\n\t// Deprecated: Use DisableIdentity instead.\n\tDisableIndentity bool\n\n\t// DisableIdentity is used to disable CLIENT SETINFO command on connect.\n\t//\n\t// default: false\n\tDisableIdentity bool\n\n\tIdentitySuffix string // Add suffix to client name. Default is empty.\n\n\t// UnstableResp3 enables Unstable mode for Redis Search module with RESP3.\n\tUnstableResp3 bool\n\n\t// PushNotificationProcessor is the processor for handling push notifications.\n\t// If nil, a default processor will be created for RESP3 connections.\n\tPushNotificationProcessor push.NotificationProcessor\n\n\t// FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing.\n\t// When a node is marked as failing, it will be avoided for this duration.\n\t// Default is 15 seconds.\n\tFailingTimeoutSeconds int\n\n\t// MaintNotificationsConfig provides custom configuration for maintnotifications upgrades.\n\t// When MaintNotificationsConfig.Mode is not \"disabled\", the client will handle\n\t// cluster upgrade notifications gracefully and manage connection/pool state\n\t// transitions seamlessly. Requires Protocol: 3 (RESP3) for push notifications.\n\t// If nil, maintnotifications upgrades are in \"auto\" mode and will be enabled if the server supports it.\n\t// The ClusterClient supports SMIGRATING and SMIGRATED notifications for cluster state management.\n\t// Individual node clients handle other maintenance notifications (MOVING, MIGRATING, etc.).\n\tMaintNotificationsConfig *maintnotifications.Config\n\t// ShardPicker is used to pick a shard when the request_policy is\n\t// ReqDefault and the command has no keys.\n\tShardPicker routing.ShardPicker\n\n\t// ClusterStateReloadInterval is the interval for reloading the cluster state.\n\t// Default is 10 seconds.\n\tClusterStateReloadInterval time.Duration\n}\n\nfunc (opt *ClusterOptions) init() {\n\tswitch opt.MaxRedirects {\n\tcase -1:\n\t\topt.MaxRedirects = 0\n\tcase 0:\n\t\topt.MaxRedirects = 3\n\t}\n\n\tif opt.RouteByLatency || opt.RouteRandomly {\n\t\topt.ReadOnly = true\n\t}\n\n\tif opt.DialTimeout == 0 {\n\t\topt.DialTimeout = 5 * time.Second\n\t}\n\tif opt.DialerRetries == 0 {\n\t\topt.DialerRetries = 5\n\t}\n\tif opt.DialerRetryTimeout == 0 {\n\t\topt.DialerRetryTimeout = 100 * time.Millisecond\n\t}\n\n\tif opt.PoolSize == 0 {\n\t\topt.PoolSize = 5 * runtime.GOMAXPROCS(0)\n\t}\n\tif opt.MaxConcurrentDials <= 0 {\n\t\topt.MaxConcurrentDials = opt.PoolSize\n\t} else if opt.MaxConcurrentDials > opt.PoolSize {\n\t\topt.MaxConcurrentDials = opt.PoolSize\n\t}\n\tif opt.ReadBufferSize == 0 {\n\t\topt.ReadBufferSize = proto.DefaultBufferSize\n\t}\n\tif opt.WriteBufferSize == 0 {\n\t\topt.WriteBufferSize = proto.DefaultBufferSize\n\t}\n\n\tswitch opt.ReadTimeout {\n\tcase -1:\n\t\topt.ReadTimeout = 0\n\tcase 0:\n\t\topt.ReadTimeout = 3 * time.Second\n\t}\n\tswitch opt.WriteTimeout {\n\tcase -1:\n\t\topt.WriteTimeout = 0\n\tcase 0:\n\t\topt.WriteTimeout = opt.ReadTimeout\n\t}\n\n\tif opt.MaxRetries == 0 {\n\t\topt.MaxRetries = -1\n\t}\n\tswitch opt.MinRetryBackoff {\n\tcase -1:\n\t\topt.MinRetryBackoff = 0\n\tcase 0:\n\t\topt.MinRetryBackoff = 8 * time.Millisecond\n\t}\n\tswitch opt.MaxRetryBackoff {\n\tcase -1:\n\t\topt.MaxRetryBackoff = 0\n\tcase 0:\n\t\topt.MaxRetryBackoff = 512 * time.Millisecond\n\t}\n\n\tif opt.NewClient == nil {\n\t\topt.NewClient = NewClient\n\t}\n\n\tif opt.FailingTimeoutSeconds == 0 {\n\t\topt.FailingTimeoutSeconds = 15\n\t}\n\n\tif opt.ShardPicker == nil {\n\t\topt.ShardPicker = &routing.RoundRobinPicker{}\n\t}\n\n\tif opt.ClusterStateReloadInterval == 0 {\n\t\topt.ClusterStateReloadInterval = 10 * time.Second\n\t}\n}\n\n// ParseClusterURL parses a URL into ClusterOptions that can be used to connect to Redis.\n// The URL must be in the form:\n//\n//\tredis://<user>:<password>@<host>:<port>\n//\tor\n//\trediss://<user>:<password>@<host>:<port>\n//\n// To add additional addresses, specify the query parameter, \"addr\" one or more times. e.g:\n//\n//\tredis://<user>:<password>@<host>:<port>?addr=<host2>:<port2>&addr=<host3>:<port3>\n//\tor\n//\trediss://<user>:<password>@<host>:<port>?addr=<host2>:<port2>&addr=<host3>:<port3>\n//\n// Most Option fields can be set using query parameters, with the following restrictions:\n//   - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries\n//   - only scalar type fields are supported (bool, int, time.Duration)\n//   - for time.Duration fields, values must be a valid input for time.ParseDuration();\n//     additionally a plain integer as value (i.e. without unit) is interpreted as seconds\n//   - to disable a duration field, use value less than or equal to 0; to use the default\n//     value, leave the value blank or remove the parameter\n//   - only the last value is interpreted if a parameter is given multiple times\n//   - fields \"network\", \"addr\", \"username\" and \"password\" can only be set using other\n//     URL attributes (scheme, host, userinfo, resp.), query parameters using these\n//     names will be treated as unknown parameters\n//   - unknown parameter names will result in an error\n//\n// Example:\n//\n//\tredis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791\n//\tis equivalent to:\n//\t&ClusterOptions{\n//\t\tAddr:        [\"localhost:6789\", \"localhost:6790\", \"localhost:6791\"]\n//\t\tDialTimeout: 3 * time.Second, // no time unit = seconds\n//\t\tReadTimeout: 6 * time.Second,\n//\t}\nfunc ParseClusterURL(redisURL string) (*ClusterOptions, error) {\n\to := &ClusterOptions{}\n\n\tu, err := url.Parse(redisURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// add base URL to the array of addresses\n\t// more addresses may be added through the URL params\n\th, p := getHostPortWithDefaults(u)\n\to.Addrs = append(o.Addrs, net.JoinHostPort(h, p))\n\n\t// setup username, password, and other configurations\n\to, err = setupClusterConn(u, h, o)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn o, nil\n}\n\n// setupClusterConn gets the username and password from the URL and the query parameters.\nfunc setupClusterConn(u *url.URL, host string, o *ClusterOptions) (*ClusterOptions, error) {\n\tswitch u.Scheme {\n\tcase \"rediss\":\n\t\to.TLSConfig = &tls.Config{ServerName: host}\n\t\tfallthrough\n\tcase \"redis\":\n\t\to.Username, o.Password = getUserPassword(u)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: invalid URL scheme: %s\", u.Scheme)\n\t}\n\n\t// retrieve the configuration from the query parameters\n\to, err := setupClusterQueryParams(u, o)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn o, nil\n}\n\n// setupClusterQueryParams converts query parameters in u to option value in o.\nfunc setupClusterQueryParams(u *url.URL, o *ClusterOptions) (*ClusterOptions, error) {\n\tq := queryOptions{q: u.Query()}\n\n\to.Protocol = q.int(\"protocol\")\n\to.ClientName = q.string(\"client_name\")\n\to.MaxRedirects = q.int(\"max_redirects\")\n\to.ReadOnly = q.bool(\"read_only\")\n\to.RouteByLatency = q.bool(\"route_by_latency\")\n\to.RouteRandomly = q.bool(\"route_randomly\")\n\to.MaxRetries = q.int(\"max_retries\")\n\to.MinRetryBackoff = q.duration(\"min_retry_backoff\")\n\to.MaxRetryBackoff = q.duration(\"max_retry_backoff\")\n\to.DialTimeout = q.duration(\"dial_timeout\")\n\to.DialerRetries = q.int(\"dialer_retries\")\n\to.DialerRetryTimeout = q.duration(\"dialer_retry_timeout\")\n\to.ReadTimeout = q.duration(\"read_timeout\")\n\to.WriteTimeout = q.duration(\"write_timeout\")\n\to.PoolFIFO = q.bool(\"pool_fifo\")\n\to.PoolSize = q.int(\"pool_size\")\n\to.MaxConcurrentDials = q.int(\"max_concurrent_dials\")\n\to.MinIdleConns = q.int(\"min_idle_conns\")\n\to.MaxIdleConns = q.int(\"max_idle_conns\")\n\to.MaxActiveConns = q.int(\"max_active_conns\")\n\to.PoolTimeout = q.duration(\"pool_timeout\")\n\to.ConnMaxLifetime = q.duration(\"conn_max_lifetime\")\n\tif q.has(\"conn_max_lifetime_jitter\") {\n\t\to.ConnMaxLifetimeJitter = min(q.duration(\"conn_max_lifetime_jitter\"), o.ConnMaxLifetime)\n\t}\n\to.ConnMaxIdleTime = q.duration(\"conn_max_idle_time\")\n\to.FailingTimeoutSeconds = q.int(\"failing_timeout_seconds\")\n\n\tif q.err != nil {\n\t\treturn nil, q.err\n\t}\n\n\t// addr can be specified as many times as needed\n\taddrs := q.strings(\"addr\")\n\tfor _, addr := range addrs {\n\t\th, p, err := net.SplitHostPort(addr)\n\t\tif err != nil || h == \"\" || p == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"redis: unable to parse addr param: %s\", addr)\n\t\t}\n\n\t\to.Addrs = append(o.Addrs, net.JoinHostPort(h, p))\n\t}\n\n\t// any parameters left?\n\tif r := q.remaining(); len(r) > 0 {\n\t\treturn nil, fmt.Errorf(\"redis: unexpected option: %s\", strings.Join(r, \", \"))\n\t}\n\n\treturn o, nil\n}\n\nfunc (opt *ClusterOptions) clientOptions() *Options {\n\t// Clone MaintNotificationsConfig to avoid sharing between cluster node clients\n\tvar maintNotificationsConfig *maintnotifications.Config\n\tif opt.MaintNotificationsConfig != nil {\n\t\tconfigClone := *opt.MaintNotificationsConfig\n\t\tmaintNotificationsConfig = &configClone\n\t}\n\n\treturn &Options{\n\t\tClientName: opt.ClientName,\n\t\tDialer:     opt.Dialer,\n\t\tOnConnect:  opt.OnConnect,\n\n\t\tProtocol:                     opt.Protocol,\n\t\tUsername:                     opt.Username,\n\t\tPassword:                     opt.Password,\n\t\tCredentialsProvider:          opt.CredentialsProvider,\n\t\tCredentialsProviderContext:   opt.CredentialsProviderContext,\n\t\tStreamingCredentialsProvider: opt.StreamingCredentialsProvider,\n\n\t\tMaxRetries:      opt.MaxRetries,\n\t\tMinRetryBackoff: opt.MinRetryBackoff,\n\t\tMaxRetryBackoff: opt.MaxRetryBackoff,\n\n\t\tDialTimeout:        opt.DialTimeout,\n\t\tDialerRetries:      opt.DialerRetries,\n\t\tDialerRetryTimeout: opt.DialerRetryTimeout,\n\t\tDialerRetryBackoff: opt.DialerRetryBackoff,\n\t\tReadTimeout:        opt.ReadTimeout,\n\t\tWriteTimeout:       opt.WriteTimeout,\n\n\t\tContextTimeoutEnabled: opt.ContextTimeoutEnabled,\n\n\t\tPoolFIFO:              opt.PoolFIFO,\n\t\tPoolSize:              opt.PoolSize,\n\t\tMaxConcurrentDials:    opt.MaxConcurrentDials,\n\t\tPoolTimeout:           opt.PoolTimeout,\n\t\tMinIdleConns:          opt.MinIdleConns,\n\t\tMaxIdleConns:          opt.MaxIdleConns,\n\t\tMaxActiveConns:        opt.MaxActiveConns,\n\t\tConnMaxIdleTime:       opt.ConnMaxIdleTime,\n\t\tConnMaxLifetime:       opt.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,\n\t\tReadBufferSize:        opt.ReadBufferSize,\n\t\tWriteBufferSize:       opt.WriteBufferSize,\n\t\tDisableIdentity:       opt.DisableIdentity,\n\t\tDisableIndentity:      opt.DisableIdentity,\n\t\tIdentitySuffix:        opt.IdentitySuffix,\n\t\tFailingTimeoutSeconds: opt.FailingTimeoutSeconds,\n\t\tTLSConfig:             opt.TLSConfig,\n\t\t// If ClusterSlots is populated, then we probably have an artificial\n\t\t// cluster whose nodes are not in clustering mode (otherwise there isn't\n\t\t// much use for ClusterSlots config).  This means we cannot execute the\n\t\t// READONLY command against that node -- setting readOnly to false in such\n\t\t// situations in the options below will prevent that from happening.\n\t\treadOnly:                  opt.ReadOnly && opt.ClusterSlots == nil,\n\t\tUnstableResp3:             opt.UnstableResp3,\n\t\tMaintNotificationsConfig:  maintNotificationsConfig,\n\t\tPushNotificationProcessor: opt.PushNotificationProcessor,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype clusterNode struct {\n\tClient *Client\n\n\tlatency    uint32 // atomic\n\tgeneration uint32 // atomic\n\tfailing    uint32 // atomic\n\tloaded     uint32 // atomic\n\n\t// last time the latency measurement was performed for the node, stored in nanoseconds from epoch\n\tlastLatencyMeasurement int64 // atomic\n}\n\nfunc newClusterNodeWithNodeAddress(clOpt *ClusterOptions, addr, nodeAddress string) *clusterNode {\n\topt := clOpt.clientOptions()\n\topt.Addr = addr\n\topt.NodeAddress = nodeAddress\n\tnode := clusterNode{\n\t\tClient: clOpt.NewClient(opt),\n\t}\n\n\tnode.latency = math.MaxUint32\n\tif clOpt.RouteByLatency {\n\t\tgo node.updateLatency()\n\t}\n\n\treturn &node\n}\n\nfunc (n *clusterNode) String() string {\n\treturn n.Client.String()\n}\n\nfunc (n *clusterNode) Close() error {\n\treturn n.Client.Close()\n}\n\nconst maximumNodeLatency = 1 * time.Minute\n\nfunc (n *clusterNode) updateLatency() {\n\tconst numProbe = 10\n\tvar dur uint64\n\n\tsuccesses := 0\n\tfor i := 0; i < numProbe; i++ {\n\t\ttime.Sleep(time.Duration(10+rand.Intn(10)) * time.Millisecond)\n\n\t\tstart := time.Now()\n\t\terr := n.Client.Ping(context.TODO()).Err()\n\t\tif err == nil {\n\t\t\tdur += uint64(time.Since(start) / time.Microsecond)\n\t\t\tsuccesses++\n\t\t}\n\t}\n\n\tvar latency float64\n\tif successes == 0 {\n\t\t// If none of the pings worked, set latency to some arbitrarily high value so this node gets\n\t\t// least priority.\n\t\tlatency = float64((maximumNodeLatency) / time.Microsecond)\n\t} else {\n\t\tlatency = float64(dur) / float64(successes)\n\t}\n\tatomic.StoreUint32(&n.latency, uint32(latency+0.5))\n\tn.SetLastLatencyMeasurement(time.Now())\n}\n\nfunc (n *clusterNode) Latency() time.Duration {\n\tlatency := atomic.LoadUint32(&n.latency)\n\treturn time.Duration(latency) * time.Microsecond\n}\n\nfunc (n *clusterNode) MarkAsFailing() {\n\tatomic.StoreUint32(&n.failing, uint32(time.Now().Unix()))\n\tatomic.StoreUint32(&n.loaded, 0)\n}\n\nfunc (n *clusterNode) Failing() bool {\n\ttimeout := int64(n.Client.opt.FailingTimeoutSeconds)\n\n\tfailing := atomic.LoadUint32(&n.failing)\n\tif failing == 0 {\n\t\treturn false\n\t}\n\tif time.Now().Unix()-int64(failing) < timeout {\n\t\treturn true\n\t}\n\tatomic.StoreUint32(&n.failing, 0)\n\treturn false\n}\n\nfunc (n *clusterNode) Generation() uint32 {\n\treturn atomic.LoadUint32(&n.generation)\n}\n\nfunc (n *clusterNode) LastLatencyMeasurement() int64 {\n\treturn atomic.LoadInt64(&n.lastLatencyMeasurement)\n}\n\nfunc (n *clusterNode) SetGeneration(gen uint32) {\n\tfor {\n\t\tv := atomic.LoadUint32(&n.generation)\n\t\tif gen < v || atomic.CompareAndSwapUint32(&n.generation, v, gen) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (n *clusterNode) SetLastLatencyMeasurement(t time.Time) {\n\tfor {\n\t\tv := atomic.LoadInt64(&n.lastLatencyMeasurement)\n\t\tif t.UnixNano() < v || atomic.CompareAndSwapInt64(&n.lastLatencyMeasurement, v, t.UnixNano()) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (n *clusterNode) Loading() bool {\n\tloaded := atomic.LoadUint32(&n.loaded)\n\tif loaded == 1 {\n\t\treturn false\n\t}\n\n\t// check if the node is loading\n\tctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n\tdefer cancel()\n\n\terr := n.Client.Ping(ctx).Err()\n\tloading := err != nil && isLoadingError(err)\n\tif !loading {\n\t\tatomic.StoreUint32(&n.loaded, 1)\n\t}\n\treturn loading\n}\n\n//------------------------------------------------------------------------------\n\ntype clusterNodes struct {\n\topt *ClusterOptions\n\n\tmu          sync.RWMutex\n\taddrs       []string\n\tnodes       map[string]*clusterNode\n\tactiveAddrs []string\n\tclosed      bool\n\tonNewNode   []func(rdb *Client)\n\n\tgeneration uint32 // atomic\n}\n\nfunc newClusterNodes(opt *ClusterOptions) *clusterNodes {\n\treturn &clusterNodes{\n\t\topt:   opt,\n\t\taddrs: opt.Addrs,\n\t\tnodes: make(map[string]*clusterNode),\n\t}\n}\n\nfunc (c *clusterNodes) Close() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.closed {\n\t\treturn nil\n\t}\n\tc.closed = true\n\n\tvar firstErr error\n\tfor _, node := range c.nodes {\n\t\tif err := node.Client.Close(); err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\n\tc.nodes = nil\n\tc.activeAddrs = nil\n\n\treturn firstErr\n}\n\nfunc (c *clusterNodes) OnNewNode(fn func(rdb *Client)) {\n\tc.mu.Lock()\n\tc.onNewNode = append(c.onNewNode, fn)\n\tc.mu.Unlock()\n}\n\nfunc (c *clusterNodes) Addrs() ([]string, error) {\n\tvar addrs []string\n\n\tc.mu.RLock()\n\tclosed := c.closed //nolint:ifshort\n\tif !closed {\n\t\tif len(c.activeAddrs) > 0 {\n\t\t\taddrs = make([]string, len(c.activeAddrs))\n\t\t\tcopy(addrs, c.activeAddrs)\n\t\t} else {\n\t\t\taddrs = make([]string, len(c.addrs))\n\t\t\tcopy(addrs, c.addrs)\n\t\t}\n\t}\n\tc.mu.RUnlock()\n\n\tif closed {\n\t\treturn nil, pool.ErrClosed\n\t}\n\tif len(addrs) == 0 {\n\t\treturn nil, errClusterNoNodes\n\t}\n\treturn addrs, nil\n}\n\nfunc (c *clusterNodes) NextGeneration() uint32 {\n\treturn atomic.AddUint32(&c.generation, 1)\n}\n\n// GC removes unused nodes.\nfunc (c *clusterNodes) GC(generation uint32) {\n\tvar collected []*clusterNode\n\n\tc.mu.Lock()\n\n\tc.activeAddrs = c.activeAddrs[:0]\n\tnow := time.Now()\n\tfor addr, node := range c.nodes {\n\t\tif node.Generation() >= generation {\n\t\t\tc.activeAddrs = append(c.activeAddrs, addr)\n\t\t\tif c.opt.RouteByLatency && node.LastLatencyMeasurement() < now.Add(-minLatencyMeasurementInterval).UnixNano() {\n\t\t\t\tgo node.updateLatency()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tdelete(c.nodes, addr)\n\t\tcollected = append(collected, node)\n\t}\n\n\tc.mu.Unlock()\n\n\tfor _, node := range collected {\n\t\t_ = node.Client.Close()\n\t}\n}\n\nfunc (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) {\n\treturn c.GetOrCreateWithNodeAddress(addr, \"\")\n}\n\nfunc (c *clusterNodes) GetOrCreateWithNodeAddress(addr, nodeAddress string) (*clusterNode, error) {\n\tnode, err := c.get(addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif node != nil {\n\t\treturn node, nil\n\t}\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.closed {\n\t\treturn nil, pool.ErrClosed\n\t}\n\n\tnode, ok := c.nodes[addr]\n\tif ok {\n\t\treturn node, nil\n\t}\n\n\tnode = newClusterNodeWithNodeAddress(c.opt, addr, nodeAddress)\n\tfor _, fn := range c.onNewNode {\n\t\tfn(node.Client)\n\t}\n\n\tc.addrs = appendIfNotExist(c.addrs, addr)\n\tc.nodes[addr] = node\n\n\treturn node, nil\n}\n\nfunc (c *clusterNodes) get(addr string) (*clusterNode, error) {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\tif c.closed {\n\t\treturn nil, pool.ErrClosed\n\t}\n\treturn c.nodes[addr], nil\n}\n\nfunc (c *clusterNodes) All() ([]*clusterNode, error) {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\tif c.closed {\n\t\treturn nil, pool.ErrClosed\n\t}\n\n\tcp := make([]*clusterNode, 0, len(c.nodes))\n\tfor _, node := range c.nodes {\n\t\tcp = append(cp, node)\n\t}\n\treturn cp, nil\n}\n\nfunc (c *clusterNodes) Random() (*clusterNode, error) {\n\taddrs, err := c.Addrs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tn := rand.Intn(len(addrs))\n\treturn c.GetOrCreate(addrs[n])\n}\n\n//------------------------------------------------------------------------------\n\ntype clusterSlot struct {\n\tstart int\n\tend   int\n\tnodes []*clusterNode\n}\n\ntype clusterSlotSlice []*clusterSlot\n\nfunc (p clusterSlotSlice) Len() int {\n\treturn len(p)\n}\n\nfunc (p clusterSlotSlice) Less(i, j int) bool {\n\treturn p[i].start < p[j].start\n}\n\nfunc (p clusterSlotSlice) Swap(i, j int) {\n\tp[i], p[j] = p[j], p[i]\n}\n\ntype clusterState struct {\n\tnodes   *clusterNodes\n\tMasters []*clusterNode\n\tSlaves  []*clusterNode\n\n\tslots []*clusterSlot\n\n\tgeneration uint32\n\tcreatedAt  time.Time\n}\n\nfunc newClusterState(\n\tnodes *clusterNodes, slots []ClusterSlot, origin string,\n) (*clusterState, error) {\n\tc := clusterState{\n\t\tnodes: nodes,\n\n\t\tslots: make([]*clusterSlot, 0, len(slots)),\n\n\t\tgeneration: nodes.NextGeneration(),\n\t\tcreatedAt:  time.Now(),\n\t}\n\n\toriginHost, _, _ := net.SplitHostPort(origin)\n\tisLoopbackOrigin := isLoopback(originHost)\n\n\tfor _, slot := range slots {\n\t\tvar nodes []*clusterNode\n\t\tfor i, slotNode := range slot.Nodes {\n\t\t\t// slotNode.Addr is the node address from CLUSTER SLOTS\n\t\t\tnodeAddress := slotNode.Addr\n\t\t\taddr := nodeAddress\n\t\t\tif !isLoopbackOrigin {\n\t\t\t\taddr = replaceLoopbackHost(addr, originHost)\n\t\t\t}\n\n\t\t\tnode, err := c.nodes.GetOrCreateWithNodeAddress(addr, nodeAddress)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnode.SetGeneration(c.generation)\n\t\t\tnodes = append(nodes, node)\n\n\t\t\tif i == 0 {\n\t\t\t\tc.Masters = appendIfNotExist(c.Masters, node)\n\t\t\t} else {\n\t\t\t\tc.Slaves = appendIfNotExist(c.Slaves, node)\n\t\t\t}\n\t\t}\n\n\t\tc.slots = append(c.slots, &clusterSlot{\n\t\t\tstart: slot.Start,\n\t\t\tend:   slot.End,\n\t\t\tnodes: nodes,\n\t\t})\n\t}\n\n\tsort.Sort(clusterSlotSlice(c.slots))\n\n\ttime.AfterFunc(time.Minute, func() {\n\t\tnodes.GC(c.generation)\n\t})\n\n\treturn &c, nil\n}\n\nfunc replaceLoopbackHost(nodeAddr, originHost string) string {\n\tnodeHost, nodePort, err := net.SplitHostPort(nodeAddr)\n\tif err != nil {\n\t\treturn nodeAddr\n\t}\n\n\tnodeIP := net.ParseIP(nodeHost)\n\tif nodeIP == nil {\n\t\treturn nodeAddr\n\t}\n\n\tif !nodeIP.IsLoopback() {\n\t\treturn nodeAddr\n\t}\n\n\t// Use origin host which is not loopback and node port.\n\treturn net.JoinHostPort(originHost, nodePort)\n}\n\n// isLoopback returns true if the host is a loopback address.\n// For IP addresses, it uses net.IP.IsLoopback().\n// For hostnames, it recognizes well-known loopback hostnames like \"localhost\"\n// and Docker-specific loopback patterns like \"*.docker.internal\".\nfunc isLoopback(host string) bool {\n\tip := net.ParseIP(host)\n\tif ip != nil {\n\t\treturn ip.IsLoopback()\n\t}\n\n\tif strings.ToLower(host) == \"localhost\" {\n\t\treturn true\n\t}\n\n\tif strings.HasSuffix(strings.ToLower(host), \".docker.internal\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (c *clusterState) slotMasterNode(slot int) (*clusterNode, error) {\n\tnodes := c.slotNodes(slot)\n\tif len(nodes) > 0 {\n\t\treturn nodes[0], nil\n\t}\n\treturn c.nodes.Random()\n}\n\nfunc (c *clusterState) slotSlaveNode(slot int) (*clusterNode, error) {\n\tnodes := c.slotNodes(slot)\n\tswitch len(nodes) {\n\tcase 0:\n\t\treturn c.nodes.Random()\n\tcase 1:\n\t\treturn nodes[0], nil\n\tcase 2:\n\t\tslave := nodes[1]\n\t\tif !slave.Failing() && !slave.Loading() {\n\t\t\treturn slave, nil\n\t\t}\n\t\treturn nodes[0], nil\n\tdefault:\n\t\tvar slave *clusterNode\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tn := rand.Intn(len(nodes)-1) + 1\n\t\t\tslave = nodes[n]\n\t\t\tif !slave.Failing() && !slave.Loading() {\n\t\t\t\treturn slave, nil\n\t\t\t}\n\t\t}\n\n\t\t// All slaves are loading - use master.\n\t\treturn nodes[0], nil\n\t}\n}\n\nfunc (c *clusterState) slotClosestNode(slot int) (*clusterNode, error) {\n\tnodes := c.slotNodes(slot)\n\tif len(nodes) == 0 {\n\t\treturn c.nodes.Random()\n\t}\n\n\tvar allNodesFailing = true\n\tvar (\n\t\tclosestNonFailingNode *clusterNode\n\t\tclosestNode           *clusterNode\n\t\tminLatency            time.Duration\n\t)\n\n\t// setting the max possible duration as zerovalue for minlatency\n\tminLatency = time.Duration(math.MaxInt64)\n\n\tfor _, n := range nodes {\n\t\tif closestNode == nil || n.Latency() < minLatency {\n\t\t\tclosestNode = n\n\t\t\tminLatency = n.Latency()\n\t\t\tif !n.Failing() {\n\t\t\t\tclosestNonFailingNode = n\n\t\t\t\tallNodesFailing = false\n\t\t\t}\n\t\t}\n\t}\n\n\t// pick the healthly node with the lowest latency\n\tif !allNodesFailing && closestNonFailingNode != nil {\n\t\treturn closestNonFailingNode, nil\n\t}\n\n\t// if all nodes are failing, we will pick the temporarily failing node with lowest latency\n\tif minLatency < maximumNodeLatency && closestNode != nil {\n\t\tinternal.Logger.Printf(context.TODO(), \"redis: all nodes are marked as failed, picking the temporarily failing node with lowest latency\")\n\t\treturn closestNode, nil\n\t}\n\n\t// If all nodes are having the maximum latency(all pings are failing) - return a random node across the cluster\n\tinternal.Logger.Printf(context.TODO(), \"redis: pings to all nodes are failing, picking a random node across the cluster\")\n\treturn c.nodes.Random()\n}\n\nfunc (c *clusterState) slotRandomNode(slot int) (*clusterNode, error) {\n\tnodes := c.slotNodes(slot)\n\tif len(nodes) == 0 {\n\t\treturn c.nodes.Random()\n\t}\n\tif len(nodes) == 1 {\n\t\treturn nodes[0], nil\n\t}\n\trandomNodes := rand.Perm(len(nodes))\n\tfor _, idx := range randomNodes {\n\t\tif node := nodes[idx]; !node.Failing() {\n\t\t\treturn node, nil\n\t\t}\n\t}\n\treturn nodes[randomNodes[0]], nil\n}\n\nfunc (c *clusterState) slotShardPickerSlaveNode(slot int, shardPicker routing.ShardPicker) (*clusterNode, error) {\n\tnodes := c.slotNodes(slot)\n\tif len(nodes) == 0 {\n\t\treturn c.nodes.Random()\n\t}\n\n\t// nodes[0] is master, nodes[1:] are slaves\n\t// First, try all slave nodes for this slot using ShardPicker order\n\tslaves := nodes[1:]\n\tif len(slaves) > 0 {\n\t\tfor i := 0; i < len(slaves); i++ {\n\t\t\tidx := shardPicker.Next(len(slaves))\n\t\t\tslave := slaves[idx]\n\t\t\tif !slave.Failing() && !slave.Loading() {\n\t\t\t\treturn slave, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// All slaves are failing or loading - return master\n\treturn nodes[0], nil\n}\n\nfunc (c *clusterState) slotNodes(slot int) []*clusterNode {\n\ti := sort.Search(len(c.slots), func(i int) bool {\n\t\treturn c.slots[i].end >= slot\n\t})\n\tif i >= len(c.slots) {\n\t\treturn nil\n\t}\n\tx := c.slots[i]\n\tif slot >= x.start && slot <= x.end {\n\t\treturn x.nodes\n\t}\n\treturn nil\n}\n\n//------------------------------------------------------------------------------\n\ntype clusterStateHolder struct {\n\tload func(ctx context.Context) (*clusterState, error)\n\n\treloadInterval time.Duration\n\tstate          atomic.Value\n\treloading      uint32 // atomic\n\treloadPending  uint32 // atomic - set to 1 when reload is requested during active reload\n}\n\nfunc newClusterStateHolder(load func(ctx context.Context) (*clusterState, error), reloadInterval time.Duration) *clusterStateHolder {\n\treturn &clusterStateHolder{\n\t\tload:           load,\n\t\treloadInterval: reloadInterval,\n\t}\n}\n\nfunc (c *clusterStateHolder) Reload(ctx context.Context) (*clusterState, error) {\n\tstate, err := c.load(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc.state.Store(state)\n\treturn state, nil\n}\n\nfunc (c *clusterStateHolder) LazyReload() {\n\t// If already reloading, mark that another reload is pending\n\tif !atomic.CompareAndSwapUint32(&c.reloading, 0, 1) {\n\t\tatomic.StoreUint32(&c.reloadPending, 1)\n\t\treturn\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\t_, err := c.Reload(context.Background())\n\t\t\tif err != nil {\n\t\t\t\tatomic.StoreUint32(&c.reloadPending, 0)\n\t\t\t\tatomic.StoreUint32(&c.reloading, 0)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Clear pending flag after reload completes, before cooldown\n\t\t\t// This captures notifications that arrived during the reload\n\t\t\tatomic.StoreUint32(&c.reloadPending, 0)\n\n\t\t\t// Wait cooldown period\n\t\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t\t// Check if another reload was requested during cooldown\n\t\t\tif atomic.LoadUint32(&c.reloadPending) == 0 {\n\t\t\t\t// No pending reload, we're done\n\t\t\t\tatomic.StoreUint32(&c.reloading, 0)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Pending reload requested, loop to reload again\n\t\t}\n\t}()\n}\n\nfunc (c *clusterStateHolder) Get(ctx context.Context) (*clusterState, error) {\n\tv := c.state.Load()\n\tif v == nil {\n\t\treturn c.Reload(ctx)\n\t}\n\n\tstate := v.(*clusterState)\n\tif time.Since(state.createdAt) > c.reloadInterval {\n\t\tc.LazyReload()\n\t}\n\treturn state, nil\n}\n\nfunc (c *clusterStateHolder) ReloadOrGet(ctx context.Context) (*clusterState, error) {\n\tstate, err := c.Reload(ctx)\n\tif err == nil {\n\t\treturn state, nil\n\t}\n\treturn c.Get(ctx)\n}\n\n//------------------------------------------------------------------------------\n\n// ClusterClient is a Redis Cluster client representing a pool of zero\n// or more underlying connections. It's safe for concurrent use by\n// multiple goroutines.\ntype ClusterClient struct {\n\topt             *ClusterOptions\n\tnodes           *clusterNodes\n\tstate           *clusterStateHolder\n\tcmdsInfoCache   *cmdsInfoCache\n\tcmdInfoResolver *commandInfoResolver\n\tcmdable\n\thooksMixin\n}\n\n// NewClusterClient returns a Redis Cluster client as described in\n// https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec.\nfunc NewClusterClient(opt *ClusterOptions) *ClusterClient {\n\topt.init()\n\n\tc := &ClusterClient{\n\t\topt:   opt,\n\t\tnodes: newClusterNodes(opt),\n\t}\n\n\tc.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo)\n\n\tc.state = newClusterStateHolder(c.loadState, opt.ClusterStateReloadInterval)\n\n\tc.SetCommandInfoResolver(NewDefaultCommandPolicyResolver())\n\n\tc.cmdable = c.Process\n\tc.initHooks(hooks{\n\t\tdial:       nil,\n\t\tprocess:    c.process,\n\t\tpipeline:   c.processPipeline,\n\t\ttxPipeline: c.processTxPipeline,\n\t})\n\n\t// Set up SMIGRATED notification handling for cluster state reload\n\t// When a node client receives a SMIGRATED notification, it should trigger\n\t// cluster state reload on the parent ClusterClient\n\tif opt.MaintNotificationsConfig != nil {\n\t\tc.nodes.OnNewNode(func(nodeClient *Client) {\n\t\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\t\tif manager != nil {\n\t\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\t\t// Log the migration details for now\n\t\t\t\t\tif internal.LogLevel.InfoOrAbove() {\n\t\t\t\t\t\tinternal.Logger.Printf(ctx, \"cluster: slots %v migrated to %s, reloading cluster state\", slotRanges, hostPort)\n\t\t\t\t\t}\n\t\t\t\t\t// Currently we reload the entire cluster state\n\t\t\t\t\t// In the future, this could be optimized to reload only the specific slots\n\t\t\t\t\tc.state.LazyReload()\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n\n\treturn c\n}\n\n// Options returns read-only Options that were used to create the client.\nfunc (c *ClusterClient) Options() *ClusterOptions {\n\treturn c.opt\n}\n\n// ReloadState reloads cluster state. If available it calls ClusterSlots func\n// to get cluster slots information.\nfunc (c *ClusterClient) ReloadState(ctx context.Context) {\n\tc.state.LazyReload()\n}\n\n// Close closes the cluster client, releasing any open resources.\n//\n// It is rare to Close a ClusterClient, as the ClusterClient is meant\n// to be long-lived and shared between many goroutines.\nfunc (c *ClusterClient) Close() error {\n\treturn c.nodes.Close()\n}\n\nfunc (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error {\n\terr := c.processHook(ctx, cmd)\n\tcmd.SetErr(err)\n\treturn err\n}\n\nfunc (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {\n\tslot := c.cmdSlot(cmd, -1)\n\tvar node *clusterNode\n\tvar moved bool\n\tvar ask bool\n\tvar lastErr error\n\tfor attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {\n\t\t// MOVED and ASK responses are not transient errors that require retry delay; they\n\t\t// should be attempted immediately.\n\t\tif attempt > 0 && !moved && !ask {\n\t\t\tif err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif node == nil {\n\t\t\tvar err error\n\t\t\tif !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil {\n\t\t\t\tnode, err = c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)\n\t\t\t} else {\n\t\t\t\tnode, err = c.cmdNode(ctx, cmd.Name(), slot)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif ask {\n\t\t\task = false\n\t\t\tpipe := node.Client.Pipeline()\n\t\t\t_ = pipe.Process(ctx, NewCmd(ctx, \"asking\"))\n\t\t\t_ = pipe.Process(ctx, cmd)\n\t\t\t_, lastErr = pipe.Exec(ctx)\n\t\t} else {\n\t\t\tif !c.opt.DisableRoutingPolicies {\n\t\t\t\tlastErr = c.routeAndRun(ctx, cmd, node)\n\t\t\t} else {\n\t\t\t\tlastErr = node.Client.Process(ctx, cmd)\n\t\t\t}\n\t\t}\n\n\t\t// If there is no error - we are done.\n\t\tif lastErr == nil {\n\t\t\treturn nil\n\t\t}\n\t\tif isReadOnly := isReadOnlyError(lastErr); isReadOnly || lastErr == pool.ErrClosed {\n\t\t\tif isReadOnly {\n\t\t\t\tc.state.LazyReload()\n\t\t\t}\n\t\t\tnode = nil\n\t\t\tcontinue\n\t\t}\n\n\t\t// If slave is loading - pick another node.\n\t\tif c.opt.ReadOnly && isLoadingError(lastErr) {\n\t\t\tnode.MarkAsFailing()\n\t\t\tnode = nil\n\t\t\tcontinue\n\t\t}\n\n\t\tvar addr string\n\t\tmoved, ask, addr = isMovedError(lastErr)\n\t\tif moved || ask {\n\t\t\tc.state.LazyReload()\n\n\t\t\t// Record error metrics\n\t\t\tif errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {\n\t\t\t\terrorType := \"MOVED\"\n\t\t\t\tstatusCode := \"MOVED\"\n\t\t\t\tif ask {\n\t\t\t\t\terrorType = \"ASK\"\n\t\t\t\t\tstatusCode = \"ASK\"\n\t\t\t\t}\n\t\t\t\t// MOVED/ASK are not internal errors, and this is the first attempt (retry count = 0)\n\t\t\t\terrorCallback(ctx, errorType, nil, statusCode, false, 0)\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tnode, err = c.nodes.GetOrCreate(addr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif shouldRetry(lastErr, cmd.readTimeout() == nil) {\n\t\t\t// First retry the same node.\n\t\t\tif attempt == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Second try another node.\n\t\t\tnode.MarkAsFailing()\n\t\t\tnode = nil\n\t\t\tcontinue\n\t\t}\n\n\t\treturn lastErr\n\t}\n\treturn lastErr\n}\n\nfunc (c *ClusterClient) OnNewNode(fn func(rdb *Client)) {\n\tc.nodes.OnNewNode(fn)\n}\n\n// ForEachMaster concurrently calls the fn on each master node in the cluster.\n// It returns the first error if any.\nfunc (c *ClusterClient) ForEachMaster(\n\tctx context.Context,\n\tfn func(ctx context.Context, client *Client) error,\n) error {\n\tstate, err := c.state.ReloadOrGet(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar wg sync.WaitGroup\n\terrCh := make(chan error, 1)\n\n\tfor _, master := range state.Masters {\n\t\twg.Add(1)\n\t\tgo func(node *clusterNode) {\n\t\t\tdefer wg.Done()\n\t\t\terr := fn(ctx, node.Client)\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase errCh <- err:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t}(master)\n\t}\n\n\twg.Wait()\n\n\tselect {\n\tcase err := <-errCh:\n\t\treturn err\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// ForEachSlave concurrently calls the fn on each slave node in the cluster.\n// It returns the first error if any.\nfunc (c *ClusterClient) ForEachSlave(\n\tctx context.Context,\n\tfn func(ctx context.Context, client *Client) error,\n) error {\n\tstate, err := c.state.ReloadOrGet(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar wg sync.WaitGroup\n\terrCh := make(chan error, 1)\n\n\tfor _, slave := range state.Slaves {\n\t\twg.Add(1)\n\t\tgo func(node *clusterNode) {\n\t\t\tdefer wg.Done()\n\t\t\terr := fn(ctx, node.Client)\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase errCh <- err:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t}(slave)\n\t}\n\n\twg.Wait()\n\n\tselect {\n\tcase err := <-errCh:\n\t\treturn err\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// ForEachShard concurrently calls the fn on each known node in the cluster.\n// It returns the first error if any.\nfunc (c *ClusterClient) ForEachShard(\n\tctx context.Context,\n\tfn func(ctx context.Context, client *Client) error,\n) error {\n\tstate, err := c.state.ReloadOrGet(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar wg sync.WaitGroup\n\terrCh := make(chan error, 1)\n\n\tworker := func(node *clusterNode) {\n\t\tdefer wg.Done()\n\t\terr := fn(ctx, node.Client)\n\t\tif err != nil {\n\t\t\tselect {\n\t\t\tcase errCh <- err:\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, node := range state.Masters {\n\t\twg.Add(1)\n\t\tgo worker(node)\n\t}\n\tfor _, node := range state.Slaves {\n\t\twg.Add(1)\n\t\tgo worker(node)\n\t}\n\n\twg.Wait()\n\n\tselect {\n\tcase err := <-errCh:\n\t\treturn err\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// PoolStats returns accumulated connection pool stats.\nfunc (c *ClusterClient) PoolStats() *PoolStats {\n\tvar acc PoolStats\n\n\tstate, _ := c.state.Get(context.TODO())\n\tif state == nil {\n\t\treturn &acc\n\t}\n\n\tfor _, node := range state.Masters {\n\t\ts := node.Client.connPool.Stats()\n\t\tacc.Hits += s.Hits\n\t\tacc.Misses += s.Misses\n\t\tacc.Timeouts += s.Timeouts\n\n\t\tacc.TotalConns += s.TotalConns\n\t\tacc.IdleConns += s.IdleConns\n\t\tacc.StaleConns += s.StaleConns\n\t}\n\n\tfor _, node := range state.Slaves {\n\t\ts := node.Client.connPool.Stats()\n\t\tacc.Hits += s.Hits\n\t\tacc.Misses += s.Misses\n\t\tacc.Timeouts += s.Timeouts\n\n\t\tacc.TotalConns += s.TotalConns\n\t\tacc.IdleConns += s.IdleConns\n\t\tacc.StaleConns += s.StaleConns\n\t}\n\n\treturn &acc\n}\n\nfunc (c *ClusterClient) loadState(ctx context.Context) (*clusterState, error) {\n\tif c.opt.ClusterSlots != nil {\n\t\tslots, err := c.opt.ClusterSlots(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn newClusterState(c.nodes, slots, \"\")\n\t}\n\n\taddrs, err := c.nodes.Addrs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar firstErr error\n\n\tfor _, idx := range rand.Perm(len(addrs)) {\n\t\taddr := addrs[idx]\n\n\t\tnode, err := c.nodes.GetOrCreate(addr)\n\t\tif err != nil {\n\t\t\tif firstErr == nil {\n\t\t\t\tfirstErr = err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tslots, err := node.Client.ClusterSlots(ctx).Result()\n\t\tif err != nil {\n\t\t\tif firstErr == nil {\n\t\t\t\tfirstErr = err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\treturn newClusterState(c.nodes, slots, addr)\n\t}\n\n\t/*\n\t * No node is connectable. It's possible that all nodes' IP has changed.\n\t * Clear activeAddrs to let client be able to re-connect using the initial\n\t * setting of the addresses (e.g. [redis-cluster-0:6379, redis-cluster-1:6379]),\n\t * which might have chance to resolve domain name and get updated IP address.\n\t */\n\tc.nodes.mu.Lock()\n\tc.nodes.activeAddrs = nil\n\tc.nodes.mu.Unlock()\n\n\treturn nil, firstErr\n}\n\nfunc (c *ClusterClient) Pipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: pipelineExecer(c.processPipelineHook),\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc (c *ClusterClient) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.Pipeline().Pipelined(ctx, fn)\n}\n\nfunc (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error {\n\t// Only call time.Now() if pipeline operation duration callback is set to avoid overhead\n\tvar operationStart time.Time\n\tpipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback()\n\tif pipelineOpDurationCallback != nil {\n\t\toperationStart = time.Now()\n\t}\n\ttotalAttempts := 0\n\n\tcmdsMap := newCmdsMap()\n\n\tif err := c.mapCmdsByNode(ctx, cmdsMap, cmds); err != nil {\n\t\tsetCmdsErr(cmds, err)\n\t\tif pipelineOpDurationCallback != nil {\n\t\t\toperationDuration := time.Since(operationStart)\n\t\t\tpipelineOpDurationCallback(ctx, operationDuration, \"PIPELINE\", len(cmds), 1, err, nil, 0)\n\t\t}\n\t\treturn err\n\t}\n\n\tvar lastErr error\n\tfor attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {\n\t\ttotalAttempts++\n\t\tif attempt > 0 {\n\t\t\tif err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {\n\t\t\t\tsetCmdsErr(cmds, err)\n\t\t\t\tif pipelineOpDurationCallback != nil {\n\t\t\t\t\toperationDuration := time.Since(operationStart)\n\t\t\t\t\tpipelineOpDurationCallback(ctx, operationDuration, \"PIPELINE\", len(cmds), totalAttempts, err, nil, 0)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tfailedCmds := newCmdsMap()\n\t\tvar wg sync.WaitGroup\n\n\t\tfor node, cmds := range cmdsMap.m {\n\t\t\twg.Add(1)\n\t\t\tgo func(node *clusterNode, cmds []Cmder) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tc.processPipelineNode(ctx, node, cmds, failedCmds)\n\t\t\t}(node, cmds)\n\t\t}\n\n\t\twg.Wait()\n\t\tif len(failedCmds.m) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tcmdsMap = failedCmds\n\t\tlastErr = cmdsFirstErr(cmds)\n\t}\n\n\t// Record pipeline operation duration\n\tif pipelineOpDurationCallback != nil {\n\t\toperationDuration := time.Since(operationStart)\n\t\tfinalErr := cmdsFirstErr(cmds)\n\t\tif finalErr == nil {\n\t\t\tfinalErr = lastErr\n\t\t}\n\t\tpipelineOpDurationCallback(ctx, operationDuration, \"PIPELINE\", len(cmds), totalAttempts, finalErr, nil, 0)\n\t}\n\n\treturn cmdsFirstErr(cmds)\n}\n\nfunc (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmds []Cmder) error {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {\n\t\tfor _, cmd := range cmds {\n\t\t\tvar policy *routing.CommandPolicy\n\t\t\tif c.cmdInfoResolver != nil {\n\t\t\t\tpolicy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)\n\t\t\t}\n\t\t\tif policy != nil && !policy.CanBeUsedInPipeline() {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future\", cmd.Name(),\n\t\t\t\t)\n\t\t\t}\n\t\t\tslot := c.cmdSlot(cmd, -1)\n\t\t\tvar node *clusterNode\n\t\t\t// For keyless commands (slot == -1), use ShardPicker if routing policies are enabled\n\t\t\tif slot == -1 && !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil {\n\t\t\t\tif len(state.Masters) == 0 {\n\t\t\t\t\treturn errClusterNoNodes\n\t\t\t\t}\n\t\t\t\t// For read-only keyless commands, pick from all nodes (masters + slaves)\n\t\t\t\tallNodes := append(state.Masters, state.Slaves...)\n\t\t\t\tidx := c.opt.ShardPicker.Next(len(allNodes))\n\t\t\t\tnode = allNodes[idx]\n\t\t\t} else {\n\t\t\t\tnode, err = c.slotReadOnlyNode(state, slot)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tcmdsMap.Add(node, cmd)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor _, cmd := range cmds {\n\t\tvar policy *routing.CommandPolicy\n\t\tif c.cmdInfoResolver != nil {\n\t\t\tpolicy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)\n\t\t}\n\t\tif policy != nil && !policy.CanBeUsedInPipeline() {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future\", cmd.Name(),\n\t\t\t)\n\t\t}\n\t\tslot := c.cmdSlot(cmd, -1)\n\t\tvar node *clusterNode\n\t\t// For keyless commands (slot == -1), use ShardPicker if routing policies are enabled\n\t\tif slot == -1 && !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil {\n\t\t\tif len(state.Masters) == 0 {\n\t\t\t\treturn errClusterNoNodes\n\t\t\t}\n\t\t\tidx := c.opt.ShardPicker.Next(len(state.Masters))\n\t\t\tnode = state.Masters[idx]\n\t\t} else {\n\t\t\tnode, err = state.slotMasterNode(slot)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tcmdsMap.Add(node, cmd)\n\t}\n\treturn nil\n}\n\nfunc (c *ClusterClient) cmdsAreReadOnly(ctx context.Context, cmds []Cmder) bool {\n\tfor _, cmd := range cmds {\n\t\tcmdInfo := c.cmdInfo(ctx, cmd.Name())\n\t\tif cmdInfo == nil || !cmdInfo.ReadOnly {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (c *ClusterClient) processPipelineNode(\n\tctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap,\n) {\n\t_ = node.Client.withProcessPipelineHook(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {\n\t\tcn, err := node.Client.getConn(ctx)\n\t\tif err != nil {\n\t\t\tif !isContextError(err) {\n\t\t\t\tnode.MarkAsFailing()\n\t\t\t}\n\t\t\t_ = c.mapCmdsByNode(ctx, failedCmds, cmds)\n\t\t\tsetCmdsErr(cmds, err)\n\t\t\treturn err\n\t\t}\n\n\t\tvar processErr error\n\t\tdefer func() {\n\t\t\tnode.Client.releaseConn(ctx, cn, processErr)\n\t\t}()\n\t\tprocessErr = c.processPipelineNodeConn(ctx, node, cn, cmds, failedCmds)\n\n\t\treturn processErr\n\t})\n}\n\nfunc (c *ClusterClient) processPipelineNodeConn(\n\tctx context.Context, node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap,\n) error {\n\tif err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {\n\t\treturn writeCmds(wr, cmds)\n\t}); err != nil {\n\t\tif isBadConn(err, false, node.Client.getAddr()) {\n\t\t\tnode.MarkAsFailing()\n\t\t}\n\t\tif shouldRetry(err, true) {\n\t\t\t_ = c.mapCmdsByNode(ctx, failedCmds, cmds)\n\t\t}\n\t\tsetCmdsErr(cmds, err)\n\t\treturn err\n\t}\n\n\treturn cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {\n\t\treturn c.pipelineReadCmds(ctx, node, rd, cmds, failedCmds)\n\t})\n}\n\nfunc (c *ClusterClient) pipelineReadCmds(\n\tctx context.Context,\n\tnode *clusterNode,\n\trd *proto.Reader,\n\tcmds []Cmder,\n\tfailedCmds *cmdsMap,\n) error {\n\tfor i, cmd := range cmds {\n\t\terr := cmd.readReply(rd)\n\t\tcmd.SetErr(err)\n\n\t\tif err == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif c.checkMovedErr(ctx, cmd, err, failedCmds) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif c.opt.ReadOnly && isBadConn(err, false, node.Client.getAddr()) {\n\t\t\tnode.MarkAsFailing()\n\t\t}\n\n\t\tif !isRedisError(err) {\n\t\t\tif shouldRetry(err, true) {\n\t\t\t\t_ = c.mapCmdsByNode(ctx, failedCmds, cmds)\n\t\t\t}\n\t\t\tsetCmdsErr(cmds[i+1:], err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := cmds[0].Err(); err != nil && shouldRetry(err, true) {\n\t\t_ = c.mapCmdsByNode(ctx, failedCmds, cmds)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *ClusterClient) checkMovedErr(\n\tctx context.Context, cmd Cmder, err error, failedCmds *cmdsMap,\n) bool {\n\tmoved, ask, addr := isMovedError(err)\n\tif !moved && !ask {\n\t\treturn false\n\t}\n\n\tnode, err := c.nodes.GetOrCreate(addr)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tif moved {\n\t\tc.state.LazyReload()\n\t\tfailedCmds.Add(node, cmd)\n\t\treturn true\n\t}\n\n\tif ask {\n\t\tfailedCmds.Add(node, NewCmd(ctx, \"asking\"), cmd)\n\t\treturn true\n\t}\n\n\tpanic(\"not reached\")\n}\n\n// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.\nfunc (c *ClusterClient) TxPipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: func(ctx context.Context, cmds []Cmder) error {\n\t\t\tcmds = wrapMultiExec(ctx, cmds)\n\t\t\treturn c.processTxPipelineHook(ctx, cmds)\n\t\t},\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc (c *ClusterClient) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.TxPipeline().Pipelined(ctx, fn)\n}\n\nfunc (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {\n\t// Only call time.Now() if pipeline operation duration callback is set to avoid overhead\n\tvar operationStart time.Time\n\tpipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback()\n\tif pipelineOpDurationCallback != nil {\n\t\toperationStart = time.Now()\n\t}\n\ttotalAttempts := 0\n\n\t// Trim multi .. exec.\n\tcmds = cmds[1 : len(cmds)-1]\n\n\tif len(cmds) == 0 {\n\t\treturn nil\n\t}\n\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\tsetCmdsErr(cmds, err)\n\t\tif pipelineOpDurationCallback != nil {\n\t\t\toperationDuration := time.Since(operationStart)\n\t\t\tpipelineOpDurationCallback(ctx, operationDuration, \"MULTI\", len(cmds), 1, err, nil, 0)\n\t\t}\n\t\treturn err\n\t}\n\n\tkeyedCmdsBySlot := c.slottedKeyedCommands(ctx, cmds)\n\tslot := -1\n\tswitch len(keyedCmdsBySlot) {\n\tcase 0:\n\t\tslot = hashtag.RandomSlot()\n\tcase 1:\n\t\tfor sl := range keyedCmdsBySlot {\n\t\t\tslot = sl\n\t\t\tbreak\n\t\t}\n\tdefault:\n\t\t// TxPipeline does not support cross slot transaction.\n\t\tsetCmdsErr(cmds, ErrCrossSlot)\n\t\tif pipelineOpDurationCallback != nil {\n\t\t\toperationDuration := time.Since(operationStart)\n\t\t\tpipelineOpDurationCallback(ctx, operationDuration, \"MULTI\", len(cmds), 1, ErrCrossSlot, nil, 0)\n\t\t}\n\t\treturn ErrCrossSlot\n\t}\n\n\tnode, err := state.slotMasterNode(slot)\n\tif err != nil {\n\t\tsetCmdsErr(cmds, err)\n\t\tif pipelineOpDurationCallback != nil {\n\t\t\toperationDuration := time.Since(operationStart)\n\t\t\tpipelineOpDurationCallback(ctx, operationDuration, \"MULTI\", len(cmds), 1, err, nil, 0)\n\t\t}\n\t\treturn err\n\t}\n\n\tvar lastErr error\n\tcmdsMap := map[*clusterNode][]Cmder{node: cmds}\n\tfor attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {\n\t\ttotalAttempts++\n\t\tif attempt > 0 {\n\t\t\tif err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {\n\t\t\t\tsetCmdsErr(cmds, err)\n\t\t\t\tif pipelineOpDurationCallback != nil {\n\t\t\t\t\toperationDuration := time.Since(operationStart)\n\t\t\t\t\tpipelineOpDurationCallback(ctx, operationDuration, \"MULTI\", len(cmds), totalAttempts, err, nil, 0)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tfailedCmds := newCmdsMap()\n\t\tvar wg sync.WaitGroup\n\n\t\tfor node, cmds := range cmdsMap {\n\t\t\twg.Add(1)\n\t\t\tgo func(node *clusterNode, cmds []Cmder) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tc.processTxPipelineNode(ctx, node, cmds, failedCmds)\n\t\t\t}(node, cmds)\n\t\t}\n\n\t\twg.Wait()\n\t\tif len(failedCmds.m) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tcmdsMap = failedCmds.m\n\t\tlastErr = cmdsFirstErr(cmds)\n\t}\n\n\tif pipelineOpDurationCallback != nil {\n\t\toperationDuration := time.Since(operationStart)\n\t\tfinalErr := cmdsFirstErr(cmds)\n\t\tif finalErr == nil {\n\t\t\tfinalErr = lastErr\n\t\t}\n\t\tpipelineOpDurationCallback(ctx, operationDuration, \"MULTI\", len(cmds), totalAttempts, finalErr, nil, 0)\n\t}\n\n\treturn cmdsFirstErr(cmds)\n}\n\n// slottedKeyedCommands returns a map of slot to commands taking into account\n// only commands that have keys.\nfunc (c *ClusterClient) slottedKeyedCommands(ctx context.Context, cmds []Cmder) map[int][]Cmder {\n\tcmdsSlots := map[int][]Cmder{}\n\n\tprefferedRandomSlot := -1\n\tfor _, cmd := range cmds {\n\t\tif cmdFirstKeyPos(cmd) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tslot := c.cmdSlot(cmd, prefferedRandomSlot)\n\t\tif prefferedRandomSlot == -1 {\n\t\t\tprefferedRandomSlot = slot\n\t\t}\n\n\t\tcmdsSlots[slot] = append(cmdsSlots[slot], cmd)\n\t}\n\n\treturn cmdsSlots\n}\n\nfunc (c *ClusterClient) processTxPipelineNode(\n\tctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap,\n) {\n\tcmds = wrapMultiExec(ctx, cmds)\n\t_ = node.Client.withProcessPipelineHook(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {\n\t\tcn, err := node.Client.getConn(ctx)\n\t\tif err != nil {\n\t\t\t_ = c.mapCmdsByNode(ctx, failedCmds, cmds)\n\t\t\tsetCmdsErr(cmds, err)\n\t\t\treturn err\n\t\t}\n\n\t\tvar processErr error\n\t\tdefer func() {\n\t\t\tnode.Client.releaseConn(ctx, cn, processErr)\n\t\t}()\n\t\tprocessErr = c.processTxPipelineNodeConn(ctx, node, cn, cmds, failedCmds)\n\n\t\treturn processErr\n\t})\n}\n\nfunc (c *ClusterClient) processTxPipelineNodeConn(\n\tctx context.Context, node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap,\n) error {\n\tif err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {\n\t\treturn writeCmds(wr, cmds)\n\t}); err != nil {\n\t\tif shouldRetry(err, true) {\n\t\t\t_ = c.mapCmdsByNode(ctx, failedCmds, cmds)\n\t\t}\n\t\tsetCmdsErr(cmds, err)\n\t\treturn err\n\t}\n\n\treturn cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {\n\t\tstatusCmd := cmds[0].(*StatusCmd)\n\t\t// Trim multi and exec.\n\t\ttrimmedCmds := cmds[1 : len(cmds)-1]\n\n\t\tif err := c.txPipelineReadQueued(\n\t\t\tctx, node, cn, rd, statusCmd, trimmedCmds, failedCmds,\n\t\t); err != nil {\n\t\t\tsetCmdsErr(cmds, err)\n\n\t\t\tmoved, ask, addr := isMovedError(err)\n\t\t\tif moved || ask {\n\t\t\t\treturn c.cmdsMoved(ctx, trimmedCmds, moved, ask, addr, failedCmds)\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\n\t\treturn node.Client.pipelineReadCmds(ctx, cn, rd, trimmedCmds)\n\t})\n}\n\nfunc (c *ClusterClient) txPipelineReadQueued(\n\tctx context.Context,\n\tnode *clusterNode,\n\tcn *pool.Conn,\n\trd *proto.Reader,\n\tstatusCmd *StatusCmd,\n\tcmds []Cmder,\n\tfailedCmds *cmdsMap,\n) error {\n\t// Parse queued replies.\n\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\tif err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\t// Log the error but don't fail the command execution\n\t\t// Push notification processing errors shouldn't break normal Redis operations\n\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t}\n\tif err := statusCmd.readReply(rd); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, cmd := range cmds {\n\t\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\t\tif err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\t\t// Log the error but don't fail the command execution\n\t\t\t// Push notification processing errors shouldn't break normal Redis operations\n\t\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t\t}\n\t\terr := statusCmd.readReply(rd)\n\t\tif err != nil {\n\t\t\tif c.checkMovedErr(ctx, cmd, err, failedCmds) {\n\t\t\t\t// will be processed later\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcmd.SetErr(err)\n\t\t\tif !isRedisError(err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\tif err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\t// Log the error but don't fail the command execution\n\t\t// Push notification processing errors shouldn't break normal Redis operations\n\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t}\n\t// Parse number of replies.\n\tline, err := rd.ReadLine()\n\tif err != nil {\n\t\tif err == Nil {\n\t\t\terr = TxFailedErr\n\t\t}\n\t\treturn err\n\t}\n\n\tif line[0] != proto.RespArray {\n\t\treturn fmt.Errorf(\"redis: expected '*', but got line %q\", line)\n\t}\n\n\treturn nil\n}\n\nfunc (c *ClusterClient) cmdsMoved(\n\tctx context.Context, cmds []Cmder,\n\tmoved, ask bool,\n\taddr string,\n\tfailedCmds *cmdsMap,\n) error {\n\tnode, err := c.nodes.GetOrCreate(addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif moved {\n\t\tc.state.LazyReload()\n\t\tfor _, cmd := range cmds {\n\t\t\tfailedCmds.Add(node, cmd)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif ask {\n\t\tfor _, cmd := range cmds {\n\t\t\tfailedCmds.Add(node, NewCmd(ctx, \"asking\"), cmd)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\nfunc (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {\n\tif len(keys) == 0 {\n\t\treturn errNoWatchKeys\n\t}\n\n\tslot := hashtag.Slot(keys[0])\n\tfor _, key := range keys[1:] {\n\t\tif hashtag.Slot(key) != slot {\n\t\t\treturn errWatchCrosslot\n\t\t}\n\t}\n\n\tnode, err := c.slotMasterNode(ctx, slot)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {\n\t\tif attempt > 0 {\n\t\t\tif err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\terr = node.Client.Watch(ctx, fn, keys...)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tmoved, ask, addr := isMovedError(err)\n\t\tif moved || ask {\n\t\t\tnode, err = c.nodes.GetOrCreate(addr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif isReadOnly := isReadOnlyError(err); isReadOnly || err == pool.ErrClosed {\n\t\t\tif isReadOnly {\n\t\t\t\tc.state.LazyReload()\n\t\t\t}\n\t\t\tnode, err = c.slotMasterNode(ctx, slot)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif shouldRetry(err, true) {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn err\n}\n\n// maintenance notifications won't work here for now\nfunc (c *ClusterClient) pubSub() *PubSub {\n\tvar node *clusterNode\n\tpubsub := &PubSub{\n\t\topt: c.opt.clientOptions(),\n\t\tnewConn: func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) {\n\t\t\tif node != nil {\n\t\t\t\tpanic(\"node != nil\")\n\t\t\t}\n\n\t\t\tvar err error\n\n\t\t\tif len(channels) > 0 {\n\t\t\t\tslot := hashtag.Slot(channels[0])\n\n\t\t\t\t// newConn in PubSub is only used for subscription connections, so it is safe to\n\t\t\t\t// assume that a slave node can always be used when client options specify ReadOnly.\n\t\t\t\tif c.opt.ReadOnly {\n\t\t\t\t\tstate, err := c.state.Get(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tnode, err = c.slotReadOnlyNode(state, slot)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tnode, err = c.slotMasterNode(ctx, slot)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnode, err = c.nodes.Random()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\tcn, err := node.Client.pubSubPool.NewConn(ctx, node.Client.opt.Network, node.Client.opt.Addr, channels)\n\t\t\tif err != nil {\n\t\t\t\tnode = nil\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// will return nil if already initialized\n\t\t\terr = node.Client.initConn(ctx, cn)\n\t\t\tif err != nil {\n\t\t\t\t_ = cn.Close()\n\t\t\t\tnode = nil\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tnode.Client.pubSubPool.TrackConn(cn)\n\t\t\treturn cn, nil\n\t\t},\n\t\tcloseConn: func(cn *pool.Conn) error {\n\t\t\t// Untrack connection from PubSubPool\n\t\t\tnode.Client.pubSubPool.UntrackConn(cn)\n\t\t\terr := cn.Close()\n\t\t\tnode = nil\n\t\t\treturn err\n\t\t},\n\t}\n\tpubsub.init()\n\n\treturn pubsub\n}\n\n// Subscribe subscribes the client to the specified channels.\n// Channels can be omitted to create empty subscription.\nfunc (c *ClusterClient) Subscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.Subscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\n// PSubscribe subscribes the client to the given patterns.\n// Patterns can be omitted to create empty subscription.\nfunc (c *ClusterClient) PSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.PSubscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\n// SSubscribe Subscribes the client to the specified shard channels.\nfunc (c *ClusterClient) SSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.SSubscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\nfunc (c *ClusterClient) retryBackoff(attempt int) time.Duration {\n\treturn internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)\n}\n\nfunc (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) {\n\t// Try 3 random nodes.\n\tconst nodeLimit = 3\n\n\taddrs, err := c.nodes.Addrs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar firstErr error\n\n\tperm := rand.Perm(len(addrs))\n\tif len(perm) > nodeLimit {\n\t\tperm = perm[:nodeLimit]\n\t}\n\n\tfor _, idx := range perm {\n\t\taddr := addrs[idx]\n\t\tnode, err := c.nodes.GetOrCreate(addr)\n\t\tif err != nil {\n\t\t\tif firstErr == nil {\n\t\t\t\tfirstErr = err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tinfo, err := node.Client.Command(ctx).Result()\n\t\tif err == nil {\n\t\t\treturn info, nil\n\t\t}\n\n\t\tif firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\n\tif firstErr == nil {\n\t\tpanic(\"not reached\")\n\t}\n\treturn nil, firstErr\n}\n\n// cmdInfo will fetch and cache the command policies after the first execution\nfunc (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo {\n\t// Use a separate context that won't be canceled to ensure command info lookup\n\t// doesn't fail due to original context cancellation\n\tcmdInfoCtx := c.context(ctx)\n\tif c.opt.ContextTimeoutEnabled && ctx != nil {\n\t\t// If context timeout is enabled, still use a reasonable timeout\n\t\tvar cancel context.CancelFunc\n\t\tcmdInfoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\t}\n\n\tcmdsInfo, err := c.cmdsInfoCache.Get(cmdInfoCtx)\n\tif err != nil {\n\t\tinternal.Logger.Printf(cmdInfoCtx, \"getting command info: %s\", err)\n\t\treturn nil\n\t}\n\n\tinfo := cmdsInfo[name]\n\tif info == nil {\n\t\tinternal.Logger.Printf(cmdInfoCtx, \"info for cmd=%s not found\", name)\n\t}\n\n\treturn info\n}\n\nfunc (c *ClusterClient) cmdSlot(cmd Cmder, prefferedSlot int) int {\n\targs := cmd.Args()\n\tif args[0] == \"cluster\" && (args[1] == \"getkeysinslot\" || args[1] == \"countkeysinslot\") {\n\t\treturn args[2].(int)\n\t}\n\n\treturn cmdSlot(cmd, cmdFirstKeyPos(cmd), prefferedSlot)\n}\n\nfunc cmdSlot(cmd Cmder, pos int, prefferedRandomSlot int) int {\n\tif pos == 0 {\n\t\tif prefferedRandomSlot != -1 {\n\t\t\treturn prefferedRandomSlot\n\t\t}\n\t\t// Return -1 for keyless commands to signal that ShardPicker should be used\n\t\treturn -1\n\t}\n\tfirstKey := cmd.stringArg(pos)\n\treturn hashtag.Slot(firstKey)\n}\n\nfunc (c *ClusterClient) cmdNode(\n\tctx context.Context,\n\tcmdName string,\n\tslot int,\n) (*clusterNode, error) {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif c.opt.ReadOnly {\n\t\tcmdInfo := c.cmdInfo(ctx, cmdName)\n\t\tif cmdInfo != nil && cmdInfo.ReadOnly {\n\t\t\treturn c.slotReadOnlyNode(state, slot)\n\t\t}\n\t}\n\treturn state.slotMasterNode(slot)\n}\n\nfunc (c *ClusterClient) cmdNodeWithShardPicker(\n\tctx context.Context,\n\tcmdName string,\n\tslot int,\n\tshardPicker routing.ShardPicker,\n) (*clusterNode, error) {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// For keyless commands (slot == -1), use ShardPicker to select a shard\n\t// This respects the user's configured ShardPicker policy\n\tif slot == -1 {\n\t\tif len(state.Masters) == 0 {\n\t\t\treturn nil, errClusterNoNodes\n\t\t}\n\t\tidx := shardPicker.Next(len(state.Masters))\n\t\treturn state.Masters[idx], nil\n\t}\n\n\tif c.opt.ReadOnly {\n\t\tcmdInfo := c.cmdInfo(ctx, cmdName)\n\t\tif cmdInfo != nil && cmdInfo.ReadOnly {\n\t\t\treturn c.slotReadOnlyNode(state, slot)\n\t\t}\n\t}\n\treturn state.slotMasterNode(slot)\n}\n\nfunc (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) {\n\tif c.opt.RouteByLatency {\n\t\treturn state.slotClosestNode(slot)\n\t}\n\tif c.opt.RouteRandomly {\n\t\treturn state.slotRandomNode(slot)\n\t}\n\n\tif c.opt.ShardPicker != nil {\n\t\treturn state.slotShardPickerSlaveNode(slot, c.opt.ShardPicker)\n\t}\n\n\treturn state.slotSlaveNode(slot)\n}\n\nfunc (c *ClusterClient) slotMasterNode(ctx context.Context, slot int) (*clusterNode, error) {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn state.slotMasterNode(slot)\n}\n\n// SlaveForKey gets a client for a replica node to run any command on it.\n// This is especially useful if we want to run a particular lua script which has\n// only read only commands on the replica.\n// This is because other redis commands generally have a flag that points that\n// they are read only and automatically run on the replica nodes\n// if ClusterOptions.ReadOnly flag is set to true.\nfunc (c *ClusterClient) SlaveForKey(ctx context.Context, key string) (*Client, error) {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tslot := hashtag.Slot(key)\n\tnode, err := c.slotReadOnlyNode(state, slot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn node.Client, err\n}\n\n// MasterForKey return a client to the master node for a particular key.\nfunc (c *ClusterClient) MasterForKey(ctx context.Context, key string) (*Client, error) {\n\tslot := hashtag.Slot(key)\n\tnode, err := c.slotMasterNode(ctx, slot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn node.Client, nil\n}\n\nfunc (c *ClusterClient) context(ctx context.Context) context.Context {\n\tif c.opt.ContextTimeoutEnabled {\n\t\treturn ctx\n\t}\n\treturn context.Background()\n}\n\nfunc (c *ClusterClient) GetResolver() *commandInfoResolver {\n\treturn c.cmdInfoResolver\n}\n\nfunc (c *ClusterClient) SetCommandInfoResolver(cmdInfoResolver *commandInfoResolver) {\n\tc.cmdInfoResolver = cmdInfoResolver\n}\n\n// extractCommandInfo retrieves the routing policy for a command\nfunc (c *ClusterClient) extractCommandInfo(ctx context.Context, cmd Cmder) *routing.CommandPolicy {\n\tif cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.CommandPolicy != nil {\n\t\treturn cmdInfo.CommandPolicy\n\t}\n\n\treturn nil\n}\n\n// NewDynamicResolver returns a CommandInfoResolver\n// that uses the underlying cmdInfo cache to resolve the policies\nfunc (c *ClusterClient) NewDynamicResolver() *commandInfoResolver {\n\treturn &commandInfoResolver{\n\t\tresolveFunc: c.extractCommandInfo,\n\t}\n}\n\nfunc appendIfNotExist[T comparable](vals []T, newVal T) []T {\n\tfor _, v := range vals {\n\t\tif v == newVal {\n\t\t\treturn vals\n\t\t}\n\t}\n\treturn append(vals, newVal)\n}\n\n//------------------------------------------------------------------------------\n\ntype cmdsMap struct {\n\tmu sync.Mutex\n\tm  map[*clusterNode][]Cmder\n}\n\nfunc newCmdsMap() *cmdsMap {\n\treturn &cmdsMap{\n\t\tm: make(map[*clusterNode][]Cmder),\n\t}\n}\n\nfunc (m *cmdsMap) Add(node *clusterNode, cmds ...Cmder) {\n\tm.mu.Lock()\n\tm.m[node] = append(m.m[node], cmds...)\n\tm.mu.Unlock()\n}\n"
  },
  {
    "path": "osscluster_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\nfunc (c *ClusterClient) DBSize(ctx context.Context) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"dbsize\")\n\t_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {\n\t\tvar size int64\n\t\terr := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error {\n\t\t\tn, err := master.DBSize(ctx).Result()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tatomic.AddInt64(&size, n)\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tcmd.SetErr(err)\n\t\t} else {\n\t\t\tcmd.val = size\n\t\t}\n\t\treturn nil\n\t})\n\treturn cmd\n}\n\nfunc (c *ClusterClient) ScriptLoad(ctx context.Context, script string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"script\", \"load\", script)\n\t_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {\n\t\tvar mu sync.Mutex\n\t\terr := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {\n\t\t\tval, err := shard.ScriptLoad(ctx, script).Result()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tif cmd.Val() == \"\" {\n\t\t\t\tcmd.val = val\n\t\t\t}\n\t\t\tmu.Unlock()\n\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tcmd.SetErr(err)\n\t\t}\n\t\treturn nil\n\t})\n\treturn cmd\n}\n\nfunc (c *ClusterClient) ScriptFlush(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"script\", \"flush\")\n\t_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {\n\t\terr := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {\n\t\t\treturn shard.ScriptFlush(ctx).Err()\n\t\t})\n\t\tif err != nil {\n\t\t\tcmd.SetErr(err)\n\t\t}\n\t\treturn nil\n\t})\n\treturn cmd\n}\n\nfunc (c *ClusterClient) ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd {\n\targs := make([]interface{}, 2+len(hashes))\n\targs[0] = \"script\"\n\targs[1] = \"exists\"\n\tfor i, hash := range hashes {\n\t\targs[2+i] = hash\n\t}\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\n\tresult := make([]bool, len(hashes))\n\tfor i := range result {\n\t\tresult[i] = true\n\t}\n\n\t_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {\n\t\tvar mu sync.Mutex\n\t\terr := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {\n\t\t\tval, err := shard.ScriptExists(ctx, hashes...).Result()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tfor i, v := range val {\n\t\t\t\tresult[i] = result[i] && v\n\t\t\t}\n\t\t\tmu.Unlock()\n\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tcmd.SetErr(err)\n\t\t} else {\n\t\t\tcmd.val = result\n\t\t}\n\t\treturn nil\n\t})\n\treturn cmd\n}\n"
  },
  {
    "path": "osscluster_lazy_reload_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\n// TestLazyReloadQueueBehavior tests that LazyReload properly queues reload requests\nfunc TestLazyReloadQueueBehavior(t *testing.T) {\n\tt.Run(\"SingleReload\", func(t *testing.T) {\n\t\tvar reloadCount atomic.Int32\n\n\t\tholder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {\n\t\t\treloadCount.Add(1)\n\t\t\ttime.Sleep(50 * time.Millisecond) // Simulate reload work\n\t\t\treturn &clusterState{}, nil\n\t\t}, 10*time.Second)\n\n\t\t// Trigger one reload\n\t\tholder.LazyReload()\n\n\t\t// Wait for reload to complete\n\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\tif count := reloadCount.Load(); count != 1 {\n\t\t\tt.Errorf(\"Expected 1 reload, got %d\", count)\n\t\t}\n\t})\n\n\tt.Run(\"ConcurrentReloadsDeduplication\", func(t *testing.T) {\n\t\tvar reloadCount atomic.Int32\n\n\t\tholder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {\n\t\t\treloadCount.Add(1)\n\t\t\ttime.Sleep(50 * time.Millisecond) // Simulate reload work\n\t\t\treturn &clusterState{}, nil\n\t\t}, 10*time.Second)\n\n\t\t// Trigger multiple reloads concurrently\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tgo holder.LazyReload()\n\t\t}\n\n\t\t// Wait for all to complete\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Should only reload once (all concurrent calls deduplicated)\n\t\tif count := reloadCount.Load(); count != 1 {\n\t\t\tt.Errorf(\"Expected 1 reload (deduplication), got %d\", count)\n\t\t}\n\t})\n\n\tt.Run(\"PendingReloadDuringCooldown\", func(t *testing.T) {\n\t\tvar reloadCount atomic.Int32\n\n\t\tholder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {\n\t\t\treloadCount.Add(1)\n\t\t\ttime.Sleep(10 * time.Millisecond) // Simulate reload work\n\t\t\treturn &clusterState{}, nil\n\t\t}, 10*time.Second)\n\n\t\t// Trigger first reload\n\t\tholder.LazyReload()\n\n\t\t// Wait for reload to complete but still in cooldown\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t// Trigger second reload during cooldown period\n\t\tholder.LazyReload()\n\n\t\t// Wait for second reload to complete\n\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t// Should have reloaded twice (second request queued and executed)\n\t\tif count := reloadCount.Load(); count != 2 {\n\t\t\tt.Errorf(\"Expected 2 reloads (queued during cooldown), got %d\", count)\n\t\t}\n\t})\n\n\tt.Run(\"MultiplePendingReloadsCollapsed\", func(t *testing.T) {\n\t\tvar reloadCount atomic.Int32\n\n\t\tholder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {\n\t\t\treloadCount.Add(1)\n\t\t\ttime.Sleep(10 * time.Millisecond) // Simulate reload work\n\t\t\treturn &clusterState{}, nil\n\t\t}, 10*time.Second)\n\n\t\t// Trigger first reload\n\t\tholder.LazyReload()\n\n\t\t// Wait for reload to start\n\t\ttime.Sleep(5 * time.Millisecond)\n\n\t\t// Trigger multiple reloads during active reload + cooldown\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tholder.LazyReload()\n\t\t\ttime.Sleep(5 * time.Millisecond)\n\t\t}\n\n\t\t// Wait for all to complete\n\t\ttime.Sleep(400 * time.Millisecond)\n\n\t\t// Should have reloaded exactly twice:\n\t\t// 1. Initial reload\n\t\t// 2. One more reload for all the pending requests (collapsed into one)\n\t\tif count := reloadCount.Load(); count != 2 {\n\t\t\tt.Errorf(\"Expected 2 reloads (initial + collapsed pending), got %d\", count)\n\t\t}\n\t})\n\n\tt.Run(\"ReloadAfterCooldownPeriod\", func(t *testing.T) {\n\t\tvar reloadCount atomic.Int32\n\n\t\tholder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {\n\t\t\treloadCount.Add(1)\n\t\t\ttime.Sleep(10 * time.Millisecond) // Simulate reload work\n\t\t\treturn &clusterState{}, nil\n\t\t}, 10*time.Second)\n\n\t\t// Trigger first reload\n\t\tholder.LazyReload()\n\n\t\t// Wait for reload + cooldown to complete\n\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t// Trigger second reload after cooldown\n\t\tholder.LazyReload()\n\n\t\t// Wait for second reload to complete\n\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t// Should have reloaded twice (separate reload cycles)\n\t\tif count := reloadCount.Load(); count != 2 {\n\t\t\tt.Errorf(\"Expected 2 reloads (separate cycles), got %d\", count)\n\t\t}\n\t})\n\n\tt.Run(\"ErrorDuringReload\", func(t *testing.T) {\n\t\tvar reloadCount atomic.Int32\n\t\tvar shouldFail atomic.Bool\n\t\tshouldFail.Store(true)\n\n\t\tholder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {\n\t\t\treloadCount.Add(1)\n\t\t\tif shouldFail.Load() {\n\t\t\t\treturn nil, context.DeadlineExceeded\n\t\t\t}\n\t\t\treturn &clusterState{}, nil\n\t\t}, 10*time.Second)\n\n\t\t// Trigger reload that will fail\n\t\tholder.LazyReload()\n\n\t\t// Wait for failed reload\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t// Trigger another reload (should succeed now)\n\t\tshouldFail.Store(false)\n\t\tholder.LazyReload()\n\n\t\t// Wait for successful reload\n\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t// Should have attempted reload twice (first failed, second succeeded)\n\t\tif count := reloadCount.Load(); count != 2 {\n\t\t\tt.Errorf(\"Expected 2 reload attempts, got %d\", count)\n\t\t}\n\t})\n\n\tt.Run(\"CascadingSMIGRATEDScenario\", func(t *testing.T) {\n\t\t// Simulate the real-world scenario: multiple SMIGRATED notifications\n\t\t// arriving in quick succession from different node clients\n\t\tvar reloadCount atomic.Int32\n\n\t\tholder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {\n\t\t\treloadCount.Add(1)\n\t\t\ttime.Sleep(20 * time.Millisecond) // Simulate realistic reload time\n\t\t\treturn &clusterState{}, nil\n\t\t}, 10*time.Second)\n\n\t\t// Simulate 5 SMIGRATED notifications arriving within 100ms\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tgo holder.LazyReload()\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t}\n\n\t\t// Wait for all reloads to complete\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t// Should reload at most 2 times:\n\t\t// 1. First notification triggers reload\n\t\t// 2. Notifications 2-5 collapse into one pending reload\n\t\tcount := reloadCount.Load()\n\t\tif count < 1 || count > 2 {\n\t\t\tt.Errorf(\"Expected 1-2 reloads for cascading scenario, got %d\", count)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "osscluster_maintnotifications_test.go",
    "content": "package redis_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// multiNodeProxy represents a cae-resp-proxy instance that can mimic multiple cluster nodes\ntype multiNodeProxy struct {\n\tapiPort    int\n\tapiBaseURL string\n\tcmd        *exec.Cmd\n\thttpClient *http.Client\n\tnodes      []proxyNode\n}\n\n// proxyNode represents a single node in the multi-node proxy\ntype proxyNode struct {\n\tlistenPort int\n\ttargetHost string\n\ttargetPort int\n\tproxyAddr  string\n\tnodeID     string\n}\n\n// newMultiNodeProxy creates a new multi-node proxy instance\nfunc newMultiNodeProxy(apiPort int) *multiNodeProxy {\n\treturn &multiNodeProxy{\n\t\tapiPort:    apiPort,\n\t\tapiBaseURL: fmt.Sprintf(\"http://localhost:%d\", apiPort),\n\t\thttpClient: &http.Client{\n\t\t\tTimeout: 5 * time.Second,\n\t\t},\n\t\tnodes: make([]proxyNode, 0),\n\t}\n}\n\n// start starts the proxy server with initial node\nfunc (mp *multiNodeProxy) start(initialListenPort, targetPort int, targetHost string) error {\n\t// Start cae-resp-proxy with just the API port\n\t// We'll add nodes dynamically via the API\n\tmp.cmd = exec.Command(\"cae-resp-proxy\",\n\t\t\"--api-port\", fmt.Sprintf(\"%d\", mp.apiPort),\n\t\t\"--listen-port\", fmt.Sprintf(\"%d\", initialListenPort),\n\t\t\"--target-host\", targetHost,\n\t\t\"--target-port\", fmt.Sprintf(\"%d\", targetPort),\n\t)\n\n\tif err := mp.cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to start proxy: %w\", err)\n\t}\n\n\t// Wait for proxy to be ready\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Verify proxy is responding\n\tfor i := 0; i < 10; i++ {\n\t\tresp, err := mp.httpClient.Get(mp.apiBaseURL + \"/stats\")\n\t\tif err == nil {\n\t\t\tresp.Body.Close()\n\t\t\tif resp.StatusCode == 200 {\n\t\t\t\t// Add the initial node to our tracking\n\t\t\t\tmp.nodes = append(mp.nodes, proxyNode{\n\t\t\t\t\tlistenPort: initialListenPort,\n\t\t\t\t\ttargetHost: targetHost,\n\t\t\t\t\ttargetPort: targetPort,\n\t\t\t\t\tproxyAddr:  fmt.Sprintf(\"localhost:%d\", initialListenPort),\n\t\t\t\t\tnodeID:     fmt.Sprintf(\"localhost:%d:%d\", initialListenPort, targetPort),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(200 * time.Millisecond)\n\t}\n\n\treturn fmt.Errorf(\"proxy did not become ready\")\n}\n\n// addNode adds a new proxy node dynamically\nfunc (mp *multiNodeProxy) addNode(listenPort, targetPort int, targetHost string) (*proxyNode, error) {\n\t// Use the /nodes API to add a new proxy node\n\tnodeConfig := map[string]interface{}{\n\t\t\"listenPort\": listenPort,\n\t\t\"targetHost\": targetHost,\n\t\t\"targetPort\": targetPort,\n\t}\n\n\tjsonData, err := json.Marshal(nodeConfig)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal node config: %w\", err)\n\t}\n\n\tresp, err := mp.httpClient.Post(\n\t\tmp.apiBaseURL+\"/nodes\",\n\t\t\"application/json\",\n\t\tbytes.NewReader(jsonData),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to add node: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"failed to add node, status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\t// Create node tracking\n\tnode := proxyNode{\n\t\tlistenPort: listenPort,\n\t\ttargetHost: targetHost,\n\t\ttargetPort: targetPort,\n\t\tproxyAddr:  fmt.Sprintf(\"localhost:%d\", listenPort),\n\t\tnodeID:     fmt.Sprintf(\"localhost:%d:%d\", listenPort, targetPort),\n\t}\n\n\tmp.nodes = append(mp.nodes, node)\n\n\t// Wait a bit for the node to be ready\n\ttime.Sleep(200 * time.Millisecond)\n\n\treturn &node, nil\n}\n\n// getNodes returns all proxy nodes\nfunc (mp *multiNodeProxy) getNodes() []proxyNode {\n\treturn mp.nodes\n}\n\n// getNodeAddrs returns all proxy node addresses for cluster client\nfunc (mp *multiNodeProxy) getNodeAddrs() []string {\n\taddrs := make([]string, len(mp.nodes))\n\tfor i, node := range mp.nodes {\n\t\taddrs[i] = node.proxyAddr\n\t}\n\treturn addrs\n}\n\n// stop stops the proxy server\nfunc (mp *multiNodeProxy) stop() error {\n\tif mp.cmd != nil && mp.cmd.Process != nil {\n\t\treturn mp.cmd.Process.Kill()\n\t}\n\treturn nil\n}\n\n// injectNotification injects a RESP3 push notification to all connected clients\nfunc (mp *multiNodeProxy) injectNotification(notification string) error {\n\turl := mp.apiBaseURL + \"/send-to-all-clients?encoding=raw\"\n\tresp, err := mp.httpClient.Post(url, \"application/octet-stream\", strings.NewReader(notification))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to inject notification: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"injection failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\treturn nil\n}\n\n// injectNotificationToNode injects a notification to clients connected to a specific node\nfunc (mp *multiNodeProxy) injectNotificationToNode(nodeAddr string, notification string) error {\n\t// Get all connections\n\tresp, err := mp.httpClient.Get(mp.apiBaseURL + \"/connections\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get connections: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar connResp struct {\n\t\tConnectionIDs []string `json:\"connectionIds\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&connResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode connections: %w\", err)\n\t}\n\n\t// For simplicity, inject to all clients\n\t// In a real scenario, you'd filter by node\n\treturn mp.injectNotification(notification)\n}\n\n// formatSMigratingNotification creates a SMIGRATING push notification in RESP3 format\n// Format: [\"SMIGRATING\", SeqID, slot/range1-range2, ...]\nfunc formatSMigratingNotification(seqID int64, slots ...string) string {\n\t// >N\\r\\n where N is the number of elements\n\tparts := []string{fmt.Sprintf(\">%d\\r\\n\", 2+len(slots))}\n\n\t// $10\\r\\nSMIGRATING\\r\\n\n\tparts = append(parts, \"$10\\r\\nSMIGRATING\\r\\n\")\n\n\t// :seqID\\r\\n\n\tparts = append(parts, fmt.Sprintf(\":%d\\r\\n\", seqID))\n\n\t// Add each slot/range as bulk string\n\tfor _, slot := range slots {\n\t\tparts = append(parts, fmt.Sprintf(\"$%d\\r\\n%s\\r\\n\", len(slot), slot))\n\t}\n\n\treturn strings.Join(parts, \"\")\n}\n\n// formatSMigratedNotification creates a SMIGRATED push notification in RESP3 format\n// RESP3 wire format:\n//\n//\t>3                      <- push frame with 3 top-level elements\n//\t+SMIGRATED              <- message name\n//\t:SeqID                  <- sequence id integer\n//\t*<num_entries>          <- array of triplet arrays\n//\t  *3                    <- each triplet is a 3-element array\n//\t    +<source>           <- node from which slots are migrating FROM\n//\t    +<destination>      <- node to which slots are migrating TO\n//\t    +<slots>            <- comma-separated slots and/or ranges\n//\n// Each triplet is formatted as: \"source target slots\"\n// Example: \"abc.com:6789 abc.com:6790 123,789-1000\"\nfunc formatSMigratedNotification(seqID int64, triplets ...string) string {\n\tparts := []string{\">3\\r\\n\"}\n\n\t// +SMIGRATED\\r\\n\n\tparts = append(parts, \"+SMIGRATED\\r\\n\")\n\n\t// :seqID\\r\\n\n\tparts = append(parts, fmt.Sprintf(\":%d\\r\\n\", seqID))\n\n\t// Outer array containing all triplets\n\tparts = append(parts, fmt.Sprintf(\"*%d\\r\\n\", len(triplets)))\n\n\tfor _, triplet := range triplets {\n\t\t// Split triplet into source, target, and slots\n\t\ttripletParts := strings.SplitN(triplet, \" \", 3)\n\t\tif len(tripletParts) != 3 {\n\t\t\tcontinue\n\t\t}\n\t\tsource := tripletParts[0]\n\t\ttarget := tripletParts[1]\n\t\tslots := tripletParts[2]\n\n\t\t// Each triplet is a 3-element array\n\t\tparts = append(parts, \"*3\\r\\n\")\n\t\tparts = append(parts, fmt.Sprintf(\"+%s\\r\\n\", source))\n\t\tparts = append(parts, fmt.Sprintf(\"+%s\\r\\n\", target))\n\t\tparts = append(parts, fmt.Sprintf(\"+%s\\r\\n\", slots))\n\t}\n\n\treturn strings.Join(parts, \"\")\n}\n\n// TestClusterMaintNotifications_SMIGRATING tests SMIGRATING notification handling\nfunc TestClusterMaintNotifications_SMIGRATING(t *testing.T) {\n\tif os.Getenv(\"CLUSTER_MAINT_INTEGRATION_TEST\") != \"true\" {\n\t\tt.Skip(\"Skipping cluster maintnotifications integration test. Set CLUSTER_MAINT_INTEGRATION_TEST=true to run\")\n\t}\n\n\tctx := context.Background()\n\n\t// Create multi-node proxy that mimics a 3-node cluster\n\tproxy := newMultiNodeProxy(8000)\n\tif err := proxy.start(7000, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to start proxy: %v\", err)\n\t}\n\tdefer proxy.stop()\n\n\t// Add two more nodes to mimic a 3-node cluster\n\tif _, err := proxy.addNode(7001, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 2: %v\", err)\n\t}\n\tif _, err := proxy.addNode(7002, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 3: %v\", err)\n\t}\n\n\tt.Logf(\"Started proxy with %d nodes: %v\", len(proxy.getNodes()), proxy.getNodeAddrs())\n\n\t// Create cluster client pointing to all proxy nodes\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    proxy.getNodeAddrs(),\n\t\tProtocol: 3, // RESP3 required for push notifications\n\t})\n\tdefer client.Close()\n\n\t// Verify connection works\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"Failed to connect to cluster via proxy: %v\", err)\n\t}\n\n\t// Perform some operations to establish connections\n\tfor i := 0; i < 5; i++ {\n\t\tif err := client.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err(); err != nil {\n\t\t\tt.Logf(\"Warning: Failed to set key: %v\", err)\n\t\t}\n\t}\n\n\t// Inject SMIGRATING notification to all nodes\n\tnotification := formatSMigratingNotification(12345, \"1000\", \"2000-3000\")\n\tif err := proxy.injectNotification(notification); err != nil {\n\t\tt.Fatalf(\"Failed to inject SMIGRATING notification: %v\", err)\n\t}\n\n\t// Wait for notification processing\n\ttime.Sleep(200 * time.Millisecond)\n\n\t// Verify operations still work (timeouts should be relaxed)\n\tif err := client.Set(ctx, \"test-key-during-migration\", \"value\", 0).Err(); err != nil {\n\t\tt.Errorf(\"Expected operations to work during migration, got error: %v\", err)\n\t}\n\n\tt.Log(\"SMIGRATING notification test passed\")\n}\n\n// TestClusterMaintNotifications_SMIGRATED tests SMIGRATED notification handling and cluster state reload\nfunc TestClusterMaintNotifications_SMIGRATED(t *testing.T) {\n\tif os.Getenv(\"CLUSTER_MAINT_INTEGRATION_TEST\") != \"true\" {\n\t\tt.Skip(\"Skipping cluster maintnotifications integration test. Set CLUSTER_MAINT_INTEGRATION_TEST=true to run\")\n\t}\n\n\tctx := context.Background()\n\n\t// Create multi-node proxy\n\tproxy := newMultiNodeProxy(8001)\n\tif err := proxy.start(7010, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to start proxy: %v\", err)\n\t}\n\tdefer proxy.stop()\n\n\t// Add more nodes\n\tif _, err := proxy.addNode(7011, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 2: %v\", err)\n\t}\n\tif _, err := proxy.addNode(7012, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 3: %v\", err)\n\t}\n\n\tt.Logf(\"Started proxy with %d nodes: %v\", len(proxy.getNodes()), proxy.getNodeAddrs())\n\n\t// Track cluster state reloads\n\tvar reloadCount atomic.Int32\n\n\t// Create cluster client\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    proxy.getNodeAddrs(),\n\t\tProtocol: 3,\n\t})\n\tdefer client.Close()\n\n\t// Hook to track state reloads via callback\n\tclient.OnNewNode(func(nodeClient *redis.Client) {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\treloadCount.Add(1)\n\t\t\t})\n\t\t}\n\t})\n\n\t// Verify connection\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"Failed to connect: %v\", err)\n\t}\n\n\t// Perform operations to establish connections\n\tfor i := 0; i < 5; i++ {\n\t\tclient.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0)\n\t}\n\n\tinitialReloads := reloadCount.Load()\n\n\t// Inject SMIGRATED notification with new format\n\t// Simulate migration from node 1 to node 2\n\tnotification := formatSMigratedNotification(12346, \"127.0.0.1:7011 1000,2000-3000\")\n\tif err := proxy.injectNotification(notification); err != nil {\n\t\tt.Fatalf(\"Failed to inject SMIGRATED notification: %v\", err)\n\t}\n\n\t// Wait for notification processing and state reload\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Verify cluster state was reloaded\n\tfinalReloads := reloadCount.Load()\n\tif finalReloads <= initialReloads {\n\t\tt.Errorf(\"Expected cluster state reload after SMIGRATED, reloads: initial=%d, final=%d\",\n\t\t\tinitialReloads, finalReloads)\n\t}\n\n\t// Verify operations still work\n\tif err := client.Set(ctx, \"test-key-after-smigrated\", \"value\", 0).Err(); err != nil {\n\t\tt.Errorf(\"Expected operations to work after SMIGRATED, got error: %v\", err)\n\t}\n\n\tt.Log(\"SMIGRATED notification test passed\")\n}\n\n// TestClusterMaintNotifications_Deduplication tests that SMIGRATED notifications are deduplicated by SeqID\nfunc TestClusterMaintNotifications_Deduplication(t *testing.T) {\n\tif os.Getenv(\"CLUSTER_MAINT_INTEGRATION_TEST\") != \"true\" {\n\t\tt.Skip(\"Skipping cluster maintnotifications integration test. Set CLUSTER_MAINT_INTEGRATION_TEST=true to run\")\n\t}\n\n\tctx := context.Background()\n\n\t// Create multi-node proxy\n\tproxy := newMultiNodeProxy(8002)\n\tif err := proxy.start(7020, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to start proxy: %v\", err)\n\t}\n\tdefer proxy.stop()\n\n\t// Add more nodes\n\tif _, err := proxy.addNode(7021, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 2: %v\", err)\n\t}\n\tif _, err := proxy.addNode(7022, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 3: %v\", err)\n\t}\n\n\tt.Logf(\"Started proxy with %d nodes: %v\", len(proxy.getNodes()), proxy.getNodeAddrs())\n\n\tvar reloadCount atomic.Int32\n\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    proxy.getNodeAddrs(),\n\t\tProtocol: 3,\n\t})\n\tdefer client.Close()\n\n\t// Track reloads via callback\n\tclient.OnNewNode(func(nodeClient *redis.Client) {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\treloadCount.Add(1)\n\t\t\t})\n\t\t}\n\t})\n\n\t// Verify connection\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"Failed to connect: %v\", err)\n\t}\n\n\t// Perform operations\n\tfor i := 0; i < 5; i++ {\n\t\tclient.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0)\n\t}\n\n\tinitialReloads := reloadCount.Load()\n\n\t// Inject the same SMIGRATED notification multiple times (same SeqID)\n\t// This simulates receiving the same notification from multiple nodes\n\tseqID := int64(99999)\n\tnotification := formatSMigratedNotification(seqID, \"127.0.0.1:7021 5000-6000\")\n\n\t// Inject 5 times to all nodes\n\tfor i := 0; i < 5; i++ {\n\t\tif err := proxy.injectNotification(notification); err != nil {\n\t\t\tt.Fatalf(\"Failed to inject notification: %v\", err)\n\t\t}\n\t\ttime.Sleep(50 * time.Millisecond)\n\t}\n\n\t// Wait for processing\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Verify only ONE reload happened (deduplication by SeqID)\n\tfinalReloads := reloadCount.Load()\n\treloadDiff := finalReloads - initialReloads\n\tif reloadDiff != 1 {\n\t\tt.Errorf(\"Expected exactly 1 reload due to deduplication, got %d reloads\", reloadDiff)\n\t}\n\n\tt.Log(\"Deduplication test passed\")\n}\n\n// TestClusterMaintNotifications_MultiNode tests notifications across multiple cluster nodes\nfunc TestClusterMaintNotifications_MultiNode(t *testing.T) {\n\tif os.Getenv(\"CLUSTER_MAINT_INTEGRATION_TEST\") != \"true\" {\n\t\tt.Skip(\"Skipping cluster maintnotifications integration test. Set CLUSTER_MAINT_INTEGRATION_TEST=true to run\")\n\t}\n\n\tctx := context.Background()\n\n\t// Create multi-node proxy with 5 nodes to mimic a real cluster\n\tproxy := newMultiNodeProxy(8003)\n\tif err := proxy.start(7030, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to start proxy: %v\", err)\n\t}\n\tdefer proxy.stop()\n\n\t// Add 4 more nodes for a 5-node cluster\n\tfor i := 1; i < 5; i++ {\n\t\tif _, err := proxy.addNode(7030+i, 6379, \"localhost\"); err != nil {\n\t\t\tt.Fatalf(\"Failed to add node %d: %v\", i+1, err)\n\t\t}\n\t}\n\n\tnodes := proxy.getNodes()\n\tt.Logf(\"Started proxy with %d nodes: %v\", len(nodes), proxy.getNodeAddrs())\n\n\t// Track notifications received\n\tvar migratingCount atomic.Int32\n\tvar migratedCount atomic.Int32\n\tvar reloadCount atomic.Int32\n\n\t// Create cluster client\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    proxy.getNodeAddrs(),\n\t\tProtocol: 3,\n\t})\n\tdefer client.Close()\n\n\t// Set up tracking\n\tclient.OnNewNode(func(nodeClient *redis.Client) {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\treloadCount.Add(1)\n\t\t\t\tt.Logf(\"Cluster state reload triggered for %s, slots: %v\", hostPort, slotRanges)\n\t\t\t})\n\t\t}\n\t})\n\n\t// Verify connection\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"Failed to connect: %v\", err)\n\t}\n\n\t// Perform operations to establish connections to all nodes\n\tfor i := 0; i < 20; i++ {\n\t\tclient.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0)\n\t}\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Simulate slot migration scenario:\n\t// 1. SMIGRATING: Slots 0-5000 are migrating from node 1 to node 2\n\tt.Log(\"Injecting SMIGRATING notification...\")\n\tmigratingNotif := formatSMigratingNotification(10001, \"0-5000\")\n\tif err := proxy.injectNotification(migratingNotif); err != nil {\n\t\tt.Fatalf(\"Failed to inject SMIGRATING: %v\", err)\n\t}\n\tmigratingCount.Add(1)\n\n\ttime.Sleep(300 * time.Millisecond)\n\n\t// 2. SMIGRATED: Migration completed, slots now on node 2\n\tt.Log(\"Injecting SMIGRATED notification...\")\n\tmigratedNotif := formatSMigratedNotification(10002,\n\t\tfmt.Sprintf(\"127.0.0.1:%d 0-5000\", nodes[1].listenPort))\n\tif err := proxy.injectNotification(migratedNotif); err != nil {\n\t\tt.Fatalf(\"Failed to inject SMIGRATED: %v\", err)\n\t}\n\tmigratedCount.Add(1)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// 3. Another migration: Slots 5001-10000 from node 2 to node 3\n\tt.Log(\"Injecting second SMIGRATING notification...\")\n\tmigratingNotif2 := formatSMigratingNotification(10003, \"5001-10000\")\n\tif err := proxy.injectNotification(migratingNotif2); err != nil {\n\t\tt.Fatalf(\"Failed to inject second SMIGRATING: %v\", err)\n\t}\n\tmigratingCount.Add(1)\n\n\ttime.Sleep(300 * time.Millisecond)\n\n\t// 4. Second migration completed\n\tt.Log(\"Injecting second SMIGRATED notification...\")\n\tmigratedNotif2 := formatSMigratedNotification(10004,\n\t\tfmt.Sprintf(\"127.0.0.1:%d 5001-10000\", nodes[2].listenPort))\n\tif err := proxy.injectNotification(migratedNotif2); err != nil {\n\t\tt.Fatalf(\"Failed to inject second SMIGRATED: %v\", err)\n\t}\n\tmigratedCount.Add(1)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Verify cluster state was reloaded for each SMIGRATED\n\tfinalReloads := reloadCount.Load()\n\tif finalReloads < 2 {\n\t\tt.Errorf(\"Expected at least 2 cluster state reloads, got %d\", finalReloads)\n\t}\n\n\t// Verify operations still work after migrations\n\tfor i := 0; i < 10; i++ {\n\t\tif err := client.Set(ctx, fmt.Sprintf(\"post-migration-key%d\", i), \"value\", 0).Err(); err != nil {\n\t\t\tt.Errorf(\"Expected operations to work after migrations, got error: %v\", err)\n\t\t}\n\t}\n\n\tt.Logf(\"Multi-node test passed: SMIGRATING=%d, SMIGRATED=%d, Reloads=%d\",\n\t\tmigratingCount.Load(), migratedCount.Load(), finalReloads)\n}\n\n// TestClusterMaintNotifications_ComplexMigration tests complex multi-endpoint migration\nfunc TestClusterMaintNotifications_ComplexMigration(t *testing.T) {\n\tif os.Getenv(\"CLUSTER_MAINT_INTEGRATION_TEST\") != \"true\" {\n\t\tt.Skip(\"Skipping cluster maintnotifications integration test. Set CLUSTER_MAINT_INTEGRATION_TEST=true to run\")\n\t}\n\n\tctx := context.Background()\n\n\t// Create multi-node proxy\n\tproxy := newMultiNodeProxy(8004)\n\tif err := proxy.start(7040, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to start proxy: %v\", err)\n\t}\n\tdefer proxy.stop()\n\n\t// Add 2 more nodes\n\tif _, err := proxy.addNode(7041, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 2: %v\", err)\n\t}\n\tif _, err := proxy.addNode(7042, 6379, \"localhost\"); err != nil {\n\t\tt.Fatalf(\"Failed to add node 3: %v\", err)\n\t}\n\n\tnodes := proxy.getNodes()\n\tt.Logf(\"Started proxy with %d nodes: %v\", len(nodes), proxy.getNodeAddrs())\n\n\tvar reloadCount atomic.Int32\n\n\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\tAddrs:    proxy.getNodeAddrs(),\n\t\tProtocol: 3,\n\t})\n\tdefer client.Close()\n\n\tclient.OnNewNode(func(nodeClient *redis.Client) {\n\t\tmanager := nodeClient.GetMaintNotificationsManager()\n\t\tif manager != nil {\n\t\t\tmanager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\t\t\treloadCount.Add(1)\n\t\t\t\tt.Logf(\"Reload for %s, slots: %v\", hostPort, slotRanges)\n\t\t\t})\n\t\t}\n\t})\n\n\tif err := client.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"Failed to connect: %v\", err)\n\t}\n\n\t// Perform operations\n\tfor i := 0; i < 10; i++ {\n\t\tclient.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0)\n\t}\n\n\tinitialReloads := reloadCount.Load()\n\n\t// Test new SMIGRATED format with multiple endpoints\n\t// Simulate a complex resharding where slots are distributed to multiple nodes\n\tt.Log(\"Injecting complex SMIGRATED notification with multiple endpoints...\")\n\tnotification := formatSMigratedNotification(20001,\n\t\tfmt.Sprintf(\"127.0.0.1:%d 0-5000,10000-12000\", nodes[0].listenPort),\n\t\tfmt.Sprintf(\"127.0.0.1:%d 5001-9999\", nodes[1].listenPort),\n\t\tfmt.Sprintf(\"127.0.0.1:%d 12001-16383\", nodes[2].listenPort),\n\t)\n\n\tif err := proxy.injectNotification(notification); err != nil {\n\t\tt.Fatalf(\"Failed to inject complex SMIGRATED: %v\", err)\n\t}\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Verify reload happened\n\tfinalReloads := reloadCount.Load()\n\tif finalReloads <= initialReloads {\n\t\tt.Errorf(\"Expected cluster state reload, reloads: initial=%d, final=%d\",\n\t\t\tinitialReloads, finalReloads)\n\t}\n\n\t// Verify operations work\n\tfor i := 0; i < 10; i++ {\n\t\tif err := client.Set(ctx, fmt.Sprintf(\"complex-key%d\", i), \"value\", 0).Err(); err != nil {\n\t\t\tt.Errorf(\"Operations failed after complex migration: %v\", err)\n\t\t}\n\t}\n\n\tt.Log(\"Complex migration test passed\")\n}\n"
  },
  {
    "path": "osscluster_maintnotifications_unit_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n)\n\n// TestClusterMaintNotifications_CallbackSetup tests that the cluster state reload callback is properly set up\nfunc TestClusterMaintNotifications_CallbackSetup(t *testing.T) {\n\t// Create a mock cluster with maintnotifications enabled\n\topt := &redis.ClusterOptions{\n\t\tAddrs:    []string{\"localhost:6379\"},\n\t\tProtocol: 3,\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeEnabled,\n\t\t},\n\t\t// Use a custom ClusterSlots function to avoid needing a real cluster\n\t\tClusterSlots: func(ctx context.Context) ([]redis.ClusterSlot, error) {\n\t\t\treturn []redis.ClusterSlot{\n\t\t\t\t{\n\t\t\t\t\tStart: 0,\n\t\t\t\t\tEnd:   16383,\n\t\t\t\t\tNodes: []redis.ClusterNode{\n\t\t\t\t\t\t{Addr: \"localhost:6379\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil\n\t\t},\n\t}\n\n\tclient := redis.NewClusterClient(opt)\n\tdefer client.Close()\n\n\t// Give time for initialization\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Verify that maintnotifications manager is available on node clients\n\t// We can't directly access the manager, but we can verify the setup worked\n\t// by checking that the client was created successfully\n\tif client == nil {\n\t\tt.Fatal(\"Expected cluster client to be created\")\n\t}\n\n\tt.Log(\"Cluster maintnotifications callback setup test passed\")\n}\n\n// TestClusterMaintNotifications_SMigratingHandler tests SMIGRATING notification handling\nfunc TestClusterMaintNotifications_SMigratingHandler(t *testing.T) {\n\t// Simulate receiving a SMIGRATING notification\n\tnotification := []interface{}{\n\t\t\"SMIGRATING\",\n\t\tint64(12345),\n\t\t\"1000\",\n\t\t\"2000-3000\",\n\t}\n\n\t// In a real scenario, this would be handled by the NotificationHandler\n\t// For unit testing, we verify the notification format is correct\n\tif len(notification) < 3 {\n\t\tt.Fatal(\"SMIGRATING notification should have at least 3 elements\")\n\t}\n\n\tnotifType, ok := notification[0].(string)\n\tif !ok || notifType != \"SMIGRATING\" {\n\t\tt.Fatalf(\"Expected notification type SMIGRATING, got %v\", notification[0])\n\t}\n\n\tseqID, ok := notification[1].(int64)\n\tif !ok {\n\t\tt.Fatalf(\"Expected SeqID to be int64, got %T\", notification[1])\n\t}\n\tif seqID != 12345 {\n\t\tt.Errorf(\"Expected SeqID 12345, got %d\", seqID)\n\t}\n\n\t// Verify slot ranges\n\tif len(notification) < 3 {\n\t\tt.Fatal(\"Expected at least one slot range\")\n\t}\n\n\tslot1, ok := notification[2].(string)\n\tif !ok || slot1 != \"1000\" {\n\t\tt.Errorf(\"Expected first slot to be '1000', got %v\", notification[2])\n\t}\n\n\tif len(notification) >= 4 {\n\t\tslot2, ok := notification[3].(string)\n\t\tif !ok || slot2 != \"2000-3000\" {\n\t\t\tt.Errorf(\"Expected second slot range to be '2000-3000', got %v\", notification[3])\n\t\t}\n\t}\n\n\tt.Log(\"SMIGRATING notification format validation passed\")\n}\n\n// TestClusterMaintNotifications_SMigratedHandler tests SMIGRATED notification handling\nfunc TestClusterMaintNotifications_SMigratedHandler(t *testing.T) {\n\t// Simulate receiving a SMIGRATED notification with nested array format\n\t// Format: [\"SMIGRATED\", SeqID, [[source, target, slots], [source, target, slots], ...]]\n\tnotification := []interface{}{\n\t\t\"SMIGRATED\",\n\t\tint64(12346),\n\t\t[]interface{}{\n\t\t\t[]interface{}{\"127.0.0.1:6379\", \"127.0.0.1:6380\", \"123,456,789-1000\"},\n\t\t\t[]interface{}{\"127.0.0.1:6379\", \"127.0.0.1:6381\", \"124,457,300-500\"},\n\t\t},\n\t}\n\n\t// Verify notification format: SMIGRATED + SeqID + triplets array = 3 elements\n\tif len(notification) != 3 {\n\t\tt.Fatalf(\"SMIGRATED notification should have exactly 3 elements, got %d\", len(notification))\n\t}\n\n\tnotifType, ok := notification[0].(string)\n\tif !ok || notifType != \"SMIGRATED\" {\n\t\tt.Fatalf(\"Expected notification type SMIGRATED, got %v\", notification[0])\n\t}\n\n\tseqID, ok := notification[1].(int64)\n\tif !ok {\n\t\tt.Fatalf(\"Expected SeqID to be int64, got %T\", notification[1])\n\t}\n\tif seqID != 12346 {\n\t\tt.Errorf(\"Expected SeqID 12346, got %d\", seqID)\n\t}\n\n\t// Verify triplets array\n\ttriplets, ok := notification[2].([]interface{})\n\tif !ok {\n\t\tt.Fatalf(\"Expected triplets to be array, got %T\", notification[2])\n\t}\n\tif len(triplets) != 2 {\n\t\tt.Fatalf(\"Expected 2 triplets, got %d\", len(triplets))\n\t}\n\n\t// Verify first triplet (source, target, slots)\n\ttriplet1, ok := triplets[0].([]interface{})\n\tif !ok || len(triplet1) != 3 {\n\t\tt.Fatalf(\"Expected first triplet to be 3-element array, got %v\", triplets[0])\n\t}\n\tsource1, ok := triplet1[0].(string)\n\tif !ok || source1 != \"127.0.0.1:6379\" {\n\t\tt.Errorf(\"Expected first triplet source '127.0.0.1:6379', got %v\", triplet1[0])\n\t}\n\ttarget1, ok := triplet1[1].(string)\n\tif !ok || target1 != \"127.0.0.1:6380\" {\n\t\tt.Errorf(\"Expected first triplet target '127.0.0.1:6380', got %v\", triplet1[1])\n\t}\n\tslots1, ok := triplet1[2].(string)\n\tif !ok || slots1 != \"123,456,789-1000\" {\n\t\tt.Errorf(\"Expected first triplet slots '123,456,789-1000', got %v\", triplet1[2])\n\t}\n\n\t// Verify second triplet\n\ttriplet2, ok := triplets[1].([]interface{})\n\tif !ok || len(triplet2) != 3 {\n\t\tt.Fatalf(\"Expected second triplet to be 3-element array, got %v\", triplets[1])\n\t}\n\tsource2, ok := triplet2[0].(string)\n\tif !ok || source2 != \"127.0.0.1:6379\" {\n\t\tt.Errorf(\"Expected second triplet source '127.0.0.1:6379', got %v\", triplet2[0])\n\t}\n\ttarget2, ok := triplet2[1].(string)\n\tif !ok || target2 != \"127.0.0.1:6381\" {\n\t\tt.Errorf(\"Expected second triplet target '127.0.0.1:6381', got %v\", triplet2[1])\n\t}\n\tslots2, ok := triplet2[2].(string)\n\tif !ok || slots2 != \"124,457,300-500\" {\n\t\tt.Errorf(\"Expected second triplet slots '124,457,300-500', got %v\", triplet2[2])\n\t}\n\n\tt.Log(\"SMIGRATED notification format validation passed\")\n}\n\n// TestClusterMaintNotifications_DeduplicationLogic tests the deduplication logic for SMIGRATED\nfunc TestClusterMaintNotifications_DeduplicationLogic(t *testing.T) {\n\t// This test verifies the deduplication concept\n\t// In the actual implementation, SMIGRATED notifications with the same SeqID\n\t// should only trigger cluster state reload once\n\n\tprocessedSeqIDs := make(map[int64]bool)\n\tvar reloadCount int\n\n\t// Simulate receiving multiple SMIGRATED notifications with same SeqID\n\tnotifications := []int64{12345, 12345, 12345, 12346, 12346, 12347}\n\n\tfor _, seqID := range notifications {\n\t\t// Check if already processed\n\t\tif !processedSeqIDs[seqID] {\n\t\t\tprocessedSeqIDs[seqID] = true\n\t\t\treloadCount++\n\t\t}\n\t}\n\n\t// Should have 3 unique SeqIDs (12345, 12346, 12347)\n\tif reloadCount != 3 {\n\t\tt.Errorf(\"Expected 3 unique reloads, got %d\", reloadCount)\n\t}\n\n\tif len(processedSeqIDs) != 3 {\n\t\tt.Errorf(\"Expected 3 unique SeqIDs, got %d\", len(processedSeqIDs))\n\t}\n\n\tt.Log(\"Deduplication logic test passed\")\n}\n\n// TestClusterMaintNotifications_NotificationTypes tests that cluster notification types are defined\nfunc TestClusterMaintNotifications_NotificationTypes(t *testing.T) {\n\t// Verify that SMIGRATING and SMIGRATED constants exist\n\t// These are defined in maintnotifications package\n\n\texpectedTypes := []string{\n\t\tmaintnotifications.NotificationSMigrating,\n\t\tmaintnotifications.NotificationSMigrated,\n\t}\n\n\tfor _, notifType := range expectedTypes {\n\t\tif notifType == \"\" {\n\t\t\tt.Errorf(\"Notification type should not be empty\")\n\t\t}\n\t}\n\n\t// Verify the values\n\tif maintnotifications.NotificationSMigrating != \"SMIGRATING\" {\n\t\tt.Errorf(\"Expected SMIGRATING, got %s\", maintnotifications.NotificationSMigrating)\n\t}\n\n\tif maintnotifications.NotificationSMigrated != \"SMIGRATED\" {\n\t\tt.Errorf(\"Expected SMIGRATED, got %s\", maintnotifications.NotificationSMigrated)\n\t}\n\n\tt.Log(\"Notification types test passed\")\n}\n\n// TestClusterMaintNotifications_ConfigValidation tests maintnotifications config for cluster\nfunc TestClusterMaintNotifications_ConfigValidation(t *testing.T) {\n\t// Test valid config\n\tconfig := &maintnotifications.Config{\n\t\tMode:           maintnotifications.ModeEnabled,\n\t\tRelaxedTimeout: 10 * time.Second,\n\t}\n\n\tif err := config.ApplyDefaults().Validate(); err != nil {\n\t\tt.Errorf(\"Valid config should pass validation: %v\", err)\n\t}\n\n\t// Test that config can be cloned (important for cluster where each node gets a copy)\n\tcloned := config.Clone()\n\tif cloned.Mode != config.Mode {\n\t\tt.Error(\"Cloned config should have same mode\")\n\t}\n\tif cloned.RelaxedTimeout != config.RelaxedTimeout {\n\t\tt.Error(\"Cloned config should have same relaxed timeout\")\n\t}\n\n\t// Modify original to ensure clone is independent\n\tconfig.RelaxedTimeout = 20 * time.Second\n\tif cloned.RelaxedTimeout == config.RelaxedTimeout {\n\t\tt.Error(\"Clone should be independent of original\")\n\t}\n\n\tt.Log(\"Config validation test passed\")\n}\n\n// TestClusterMaintNotifications_StateReloadCallback tests the callback mechanism\nfunc TestClusterMaintNotifications_StateReloadCallback(t *testing.T) {\n\tvar callbackInvoked atomic.Bool\n\tvar receivedHostPort atomic.Value\n\tvar receivedSlots atomic.Value\n\n\t// Simulate the callback that would be set on the manager\n\tcallback := func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\tcallbackInvoked.Store(true)\n\t\treceivedHostPort.Store(hostPort)\n\t\treceivedSlots.Store(slotRanges)\n\t}\n\n\t// Simulate invoking the callback (as would happen when SMIGRATED is received)\n\tctx := context.Background()\n\tcallback(ctx, \"127.0.0.1:6380\", []string{\"1000\", \"2000-3000\"})\n\n\t// Verify callback was invoked\n\tif !callbackInvoked.Load() {\n\t\tt.Error(\"Expected callback to be invoked\")\n\t}\n\n\t// Verify parameters\n\thostPort := receivedHostPort.Load().(string)\n\tif hostPort != \"127.0.0.1:6380\" {\n\t\tt.Errorf(\"Expected host:port '127.0.0.1:6380', got %s\", hostPort)\n\t}\n\n\tslots := receivedSlots.Load().([]string)\n\tif len(slots) != 2 {\n\t\tt.Errorf(\"Expected 2 slot ranges, got %d\", len(slots))\n\t}\n\tif slots[0] != \"1000\" {\n\t\tt.Errorf(\"Expected first slot '1000', got %s\", slots[0])\n\t}\n\tif slots[1] != \"2000-3000\" {\n\t\tt.Errorf(\"Expected second slot range '2000-3000', got %s\", slots[1])\n\t}\n\n\tt.Log(\"State reload callback test passed\")\n}\n\n// TestClusterMaintNotifications_ConcurrentCallbacks tests concurrent callback invocations\nfunc TestClusterMaintNotifications_ConcurrentCallbacks(t *testing.T) {\n\tvar callbackCount atomic.Int32\n\n\tcallback := func(ctx context.Context, hostPort string, slotRanges []string) {\n\t\tcallbackCount.Add(1)\n\t\t// Simulate some processing time\n\t\ttime.Sleep(10 * time.Millisecond)\n\t}\n\n\t// Invoke callback concurrently\n\tctx := context.Background()\n\tconst numConcurrent = 10\n\n\tdone := make(chan bool, numConcurrent)\n\tfor i := 0; i < numConcurrent; i++ {\n\t\tgo func(idx int) {\n\t\t\tcallback(ctx, \"127.0.0.1:6380\", []string{string(rune(idx))})\n\t\t\tdone <- true\n\t\t}(i)\n\t}\n\n\t// Wait for all to complete\n\tfor i := 0; i < numConcurrent; i++ {\n\t\t<-done\n\t}\n\n\t// Verify all callbacks were invoked\n\tif callbackCount.Load() != numConcurrent {\n\t\tt.Errorf(\"Expected %d callback invocations, got %d\", numConcurrent, callbackCount.Load())\n\t}\n\n\tt.Log(\"Concurrent callbacks test passed\")\n}\n"
  },
  {
    "path": "osscluster_router.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n\t\"github.com/redis/go-redis/v9/internal/routing\"\n)\n\nvar (\n\terrInvalidCmdPointer         = errors.New(\"redis: invalid command pointer\")\n\terrNoCmdsToAggregate         = errors.New(\"redis: no commands to aggregate\")\n\terrNoResToAggregate          = errors.New(\"redis: no results to aggregate\")\n\terrInvalidCursorCmdArgsCount = errors.New(\"redis: FT.CURSOR command requires at least 3 arguments\")\n\terrInvalidCursorIdType       = errors.New(\"redis: invalid cursor ID type\")\n)\n\n// slotResult represents the result of executing a command on a specific slot\ntype slotResult struct {\n\tcmd  Cmder\n\tkeys []string\n\terr  error\n}\n\n// routeAndRun routes a command to the appropriate cluster nodes and executes it\nfunc (c *ClusterClient) routeAndRun(ctx context.Context, cmd Cmder, node *clusterNode) error {\n\tvar policy *routing.CommandPolicy\n\tif c.cmdInfoResolver != nil {\n\t\tpolicy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)\n\t}\n\n\t// Set stepCount from cmdInfo if not already set\n\tif cmd.stepCount() == 0 {\n\t\tif cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.StepCount > 0 {\n\t\t\tcmd.SetStepCount(cmdInfo.StepCount)\n\t\t}\n\t}\n\n\tif policy == nil {\n\t\treturn c.executeDefault(ctx, cmd, policy, node)\n\t}\n\tswitch policy.Request {\n\tcase routing.ReqAllNodes:\n\t\treturn c.executeOnAllNodes(ctx, cmd, policy)\n\tcase routing.ReqAllShards:\n\t\treturn c.executeOnAllShards(ctx, cmd, policy)\n\tcase routing.ReqMultiShard:\n\t\treturn c.executeMultiShard(ctx, cmd, policy)\n\tcase routing.ReqSpecial:\n\t\treturn c.executeSpecialCommand(ctx, cmd, policy, node)\n\tdefault:\n\t\treturn c.executeDefault(ctx, cmd, policy, node)\n\t}\n}\n\n// executeDefault handles standard command routing based on keys\nfunc (c *ClusterClient) executeDefault(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error {\n\tif policy != nil && !c.hasKeys(cmd) {\n\t\tif c.readOnlyEnabled() && policy.IsReadOnly() {\n\t\t\treturn c.executeOnArbitraryNode(ctx, cmd)\n\t\t}\n\t}\n\n\treturn node.Client.Process(ctx, cmd)\n}\n\n// executeOnArbitraryNode routes command to an arbitrary node\nfunc (c *ClusterClient) executeOnArbitraryNode(ctx context.Context, cmd Cmder) error {\n\tnode := c.pickArbitraryNode(ctx)\n\tif node == nil {\n\t\treturn errClusterNoNodes\n\t}\n\treturn node.Client.Process(ctx, cmd)\n}\n\n// executeOnAllNodes executes command on all nodes (masters and replicas)\nfunc (c *ClusterClient) executeOnAllNodes(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnodes := append(state.Masters, state.Slaves...)\n\tif len(nodes) == 0 {\n\t\treturn errClusterNoNodes\n\t}\n\n\treturn c.executeParallel(ctx, cmd, nodes, policy)\n}\n\n// executeOnAllShards executes command on all master shards\nfunc (c *ClusterClient) executeOnAllShards(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(state.Masters) == 0 {\n\t\treturn errClusterNoNodes\n\t}\n\n\treturn c.executeParallel(ctx, cmd, state.Masters, policy)\n}\n\n// executeMultiShard handles commands that operate on multiple keys across shards\nfunc (c *ClusterClient) executeMultiShard(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {\n\targs := cmd.Args()\n\tfirstKeyPos := int(cmdFirstKeyPos(cmd))\n\tstepCount := int(cmd.stepCount())\n\tif stepCount == 0 {\n\t\tstepCount = 1 // Default to 1 if not set\n\t}\n\n\tif firstKeyPos == 0 || firstKeyPos >= len(args) {\n\t\treturn fmt.Errorf(\"redis: multi-shard command %s has no key arguments\", cmd.Name())\n\t}\n\n\t// Group keys by slot\n\tslotMap := make(map[int][]string)\n\tkeyOrder := make([]string, 0)\n\n\tfor i := firstKeyPos; i < len(args); i += stepCount {\n\t\tkey, ok := args[i].(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"redis: non-string key at position %d: %v\", i, args[i])\n\t\t}\n\n\t\tslot := hashtag.Slot(key)\n\t\tslotMap[slot] = append(slotMap[slot], key)\n\t\tfor j := 1; j < stepCount; j++ {\n\t\t\tif i+j >= len(args) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tslotMap[slot] = append(slotMap[slot], args[i+j].(string))\n\t\t}\n\t\tkeyOrder = append(keyOrder, key)\n\t}\n\n\treturn c.executeMultiSlot(ctx, cmd, slotMap, keyOrder, policy)\n}\n\n// executeMultiSlot executes commands across multiple slots concurrently\nfunc (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap map[int][]string, keyOrder []string, policy *routing.CommandPolicy) error {\n\tresults := make(chan slotResult, len(slotMap))\n\tvar wg sync.WaitGroup\n\n\t// Execute on each slot concurrently\n\tfor slot, keys := range slotMap {\n\t\twg.Add(1)\n\t\tgo func(slot int, keys []string) {\n\t\t\tdefer wg.Done()\n\n\t\t\tnode, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)\n\t\t\tif err != nil {\n\t\t\t\tresults <- slotResult{nil, keys, err}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Create a command for this specific slot's keys\n\t\t\tsubCmd := c.createSlotSpecificCommand(ctx, cmd, keys)\n\t\t\terr = node.Client.Process(ctx, subCmd)\n\t\t\tresults <- slotResult{subCmd, keys, err}\n\t\t}(slot, keys)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(results)\n\t}()\n\n\treturn c.aggregateMultiSlotResults(ctx, cmd, results, keyOrder, policy)\n}\n\n// createSlotSpecificCommand creates a new command for a specific slot's keys\nfunc (c *ClusterClient) createSlotSpecificCommand(ctx context.Context, originalCmd Cmder, keys []string) Cmder {\n\toriginalArgs := originalCmd.Args()\n\tfirstKeyPos := int(cmdFirstKeyPos(originalCmd))\n\n\t// Build new args with only the specified keys\n\tnewArgs := make([]interface{}, 0, firstKeyPos+len(keys))\n\n\t// Copy command name and arguments before the keys\n\tnewArgs = append(newArgs, originalArgs[:firstKeyPos]...)\n\n\t// Add the slot-specific keys\n\tfor _, key := range keys {\n\t\tnewArgs = append(newArgs, key)\n\t}\n\n\t// Create a new command of the same type using the helper function\n\treturn createCommandByType(ctx, originalCmd.GetCmdType(), newArgs...)\n}\n\n// createCommandByType creates a new command of the specified type with the given arguments\nfunc createCommandByType(ctx context.Context, cmdType CmdType, args ...interface{}) Cmder {\n\tswitch cmdType {\n\tcase CmdTypeString:\n\t\treturn NewStringCmd(ctx, args...)\n\tcase CmdTypeInt:\n\t\treturn NewIntCmd(ctx, args...)\n\tcase CmdTypeBool:\n\t\treturn NewBoolCmd(ctx, args...)\n\tcase CmdTypeFloat:\n\t\treturn NewFloatCmd(ctx, args...)\n\tcase CmdTypeStringSlice:\n\t\treturn NewStringSliceCmd(ctx, args...)\n\tcase CmdTypeIntSlice:\n\t\treturn NewIntSliceCmd(ctx, args...)\n\tcase CmdTypeFloatSlice:\n\t\treturn NewFloatSliceCmd(ctx, args...)\n\tcase CmdTypeBoolSlice:\n\t\treturn NewBoolSliceCmd(ctx, args...)\n\tcase CmdTypeStatus:\n\t\treturn NewStatusCmd(ctx, args...)\n\tcase CmdTypeTime:\n\t\treturn NewTimeCmd(ctx, args...)\n\tcase CmdTypeMapStringString:\n\t\treturn NewMapStringStringCmd(ctx, args...)\n\tcase CmdTypeMapStringInt:\n\t\treturn NewMapStringIntCmd(ctx, args...)\n\tcase CmdTypeMapStringInterface:\n\t\treturn NewMapStringInterfaceCmd(ctx, args...)\n\tcase CmdTypeMapStringInterfaceSlice:\n\t\treturn NewMapStringInterfaceSliceCmd(ctx, args...)\n\tcase CmdTypeSlice:\n\t\treturn NewSliceCmd(ctx, args...)\n\tcase CmdTypeStringStructMap:\n\t\treturn NewStringStructMapCmd(ctx, args...)\n\tcase CmdTypeXMessageSlice:\n\t\treturn NewXMessageSliceCmd(ctx, args...)\n\tcase CmdTypeXStreamSlice:\n\t\treturn NewXStreamSliceCmd(ctx, args...)\n\tcase CmdTypeXPending:\n\t\treturn NewXPendingCmd(ctx, args...)\n\tcase CmdTypeXPendingExt:\n\t\treturn NewXPendingExtCmd(ctx, args...)\n\tcase CmdTypeXAutoClaim:\n\t\treturn NewXAutoClaimCmd(ctx, args...)\n\tcase CmdTypeXAutoClaimJustID:\n\t\treturn NewXAutoClaimJustIDCmd(ctx, args...)\n\tcase CmdTypeXInfoStreamFull:\n\t\treturn NewXInfoStreamFullCmd(ctx, args...)\n\tcase CmdTypeZSlice:\n\t\treturn NewZSliceCmd(ctx, args...)\n\tcase CmdTypeZWithKey:\n\t\treturn NewZWithKeyCmd(ctx, args...)\n\tcase CmdTypeClusterSlots:\n\t\treturn NewClusterSlotsCmd(ctx, args...)\n\tcase CmdTypeGeoPos:\n\t\treturn NewGeoPosCmd(ctx, args...)\n\tcase CmdTypeCommandsInfo:\n\t\treturn NewCommandsInfoCmd(ctx, args...)\n\tcase CmdTypeSlowLog:\n\t\treturn NewSlowLogCmd(ctx, args...)\n\tcase CmdTypeKeyValues:\n\t\treturn NewKeyValuesCmd(ctx, args...)\n\tcase CmdTypeZSliceWithKey:\n\t\treturn NewZSliceWithKeyCmd(ctx, args...)\n\tcase CmdTypeFunctionList:\n\t\treturn NewFunctionListCmd(ctx, args...)\n\tcase CmdTypeFunctionStats:\n\t\treturn NewFunctionStatsCmd(ctx, args...)\n\tcase CmdTypeKeyFlags:\n\t\treturn NewKeyFlagsCmd(ctx, args...)\n\tcase CmdTypeDuration:\n\t\treturn NewDurationCmd(ctx, time.Millisecond, args...)\n\t}\n\treturn NewCmd(ctx, args...)\n}\n\n// executeSpecialCommand handles commands with special routing requirements\nfunc (c *ClusterClient) executeSpecialCommand(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error {\n\tswitch cmd.Name() {\n\tcase \"ft.cursor\":\n\t\treturn c.executeCursorCommand(ctx, cmd)\n\tdefault:\n\t\treturn c.executeDefault(ctx, cmd, policy, node)\n\t}\n}\n\n// executeCursorCommand handles FT.CURSOR commands with sticky routing\nfunc (c *ClusterClient) executeCursorCommand(ctx context.Context, cmd Cmder) error {\n\targs := cmd.Args()\n\tif len(args) < 4 {\n\t\treturn errInvalidCursorCmdArgsCount\n\t}\n\n\tcursorID, ok := args[3].(string)\n\tif !ok {\n\t\treturn errInvalidCursorIdType\n\t}\n\n\t// Route based on cursor ID to maintain stickiness\n\tslot := hashtag.Slot(cursorID)\n\tnode, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn node.Client.Process(ctx, cmd)\n}\n\n// executeParallel executes a command on multiple nodes concurrently\nfunc (c *ClusterClient) executeParallel(ctx context.Context, cmd Cmder, nodes []*clusterNode, policy *routing.CommandPolicy) error {\n\tif len(nodes) == 0 {\n\t\treturn errClusterNoNodes\n\t}\n\n\tif len(nodes) == 1 {\n\t\treturn nodes[0].Client.Process(ctx, cmd)\n\t}\n\n\ttype nodeResult struct {\n\t\tcmd Cmder\n\t\terr error\n\t}\n\n\tresults := make(chan nodeResult, len(nodes))\n\tvar wg sync.WaitGroup\n\n\tfor _, node := range nodes {\n\t\twg.Add(1)\n\t\tgo func(n *clusterNode) {\n\t\t\tdefer wg.Done()\n\t\t\tcmdCopy := cmd.Clone()\n\t\t\terr := n.Client.Process(ctx, cmdCopy)\n\t\t\tresults <- nodeResult{cmdCopy, err}\n\t\t}(node)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(results)\n\t}()\n\n\t// Collect results and check for errors\n\tcmds := make([]Cmder, 0, len(nodes))\n\tvar firstErr error\n\n\tfor result := range results {\n\t\tif result.err != nil && firstErr == nil {\n\t\t\tfirstErr = result.err\n\t\t}\n\t\tcmds = append(cmds, result.cmd)\n\t}\n\n\t// If there was an error and no policy specified, fail fast\n\tif firstErr != nil && (policy == nil || policy.Response == routing.RespDefaultKeyless) {\n\t\tcmd.SetErr(firstErr)\n\t\treturn firstErr\n\t}\n\n\treturn c.aggregateResponses(cmd, cmds, policy)\n}\n\n// aggregateMultiSlotResults aggregates results from multi-slot execution\nfunc (c *ClusterClient) aggregateMultiSlotResults(ctx context.Context, cmd Cmder, results <-chan slotResult, keyOrder []string, policy *routing.CommandPolicy) error {\n\tkeyedResults := make(map[string]routing.AggregatorResErr)\n\tvar firstErr error\n\n\tfor result := range results {\n\t\tif result.err != nil && firstErr == nil {\n\t\t\tfirstErr = result.err\n\t\t}\n\t\tif result.cmd != nil && result.err == nil {\n\t\t\tvalue, err := ExtractCommandValue(result.cmd)\n\n\t\t\t// Check if the result is a slice (e.g., from MGET)\n\t\t\tif sliceValue, ok := value.([]interface{}); ok {\n\t\t\t\t// Map each element to its corresponding key\n\t\t\t\tfor i, key := range result.keys {\n\t\t\t\t\tif i < len(sliceValue) {\n\t\t\t\t\t\tkeyedResults[key] = routing.AggregatorResErr{Result: sliceValue[i], Err: err}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tkeyedResults[key] = routing.AggregatorResErr{Result: nil, Err: err}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// For non-slice results, map the entire result to each key\n\t\t\t\tfor _, key := range result.keys {\n\t\t\t\t\tkeyedResults[key] = routing.AggregatorResErr{Result: value, Err: err}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// TODO: return multiple errors by order when we will implement multiple errors returning\n\t\tif result.err != nil {\n\t\t\tfirstErr = result.err\n\t\t}\n\t}\n\n\treturn c.aggregateKeyedValues(cmd, keyedResults, keyOrder, policy)\n}\n\n// aggregateKeyedValues aggregates individual key-value pairs while preserving key order\nfunc (c *ClusterClient) aggregateKeyedValues(cmd Cmder, keyedResults map[string]routing.AggregatorResErr, keyOrder []string, policy *routing.CommandPolicy) error {\n\tif len(keyedResults) == 0 {\n\t\treturn errNoResToAggregate\n\t}\n\n\taggregator := c.createAggregator(policy, cmd, true)\n\n\t// Set key order for keyed aggregators\n\tvar keyedAgg *routing.DefaultKeyedAggregator\n\tvar isKeyedAgg bool\n\tvar err error\n\tif keyedAgg, isKeyedAgg = aggregator.(*routing.DefaultKeyedAggregator); isKeyedAgg {\n\t\terr = keyedAgg.BatchAddWithKeyOrder(keyedResults, keyOrder)\n\t} else {\n\t\terr = aggregator.BatchAdd(keyedResults)\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.finishAggregation(cmd, aggregator)\n}\n\n// aggregateResponses aggregates multiple shard responses\nfunc (c *ClusterClient) aggregateResponses(cmd Cmder, cmds []Cmder, policy *routing.CommandPolicy) error {\n\tif len(cmds) == 0 {\n\t\treturn errNoCmdsToAggregate\n\t}\n\n\tif len(cmds) == 1 {\n\t\tshardCmd := cmds[0]\n\t\tif err := shardCmd.Err(); err != nil {\n\t\t\tcmd.SetErr(err)\n\t\t\treturn err\n\t\t}\n\t\tvalue, _ := ExtractCommandValue(shardCmd)\n\t\treturn c.setCommandValue(cmd, value)\n\t}\n\n\taggregator := c.createAggregator(policy, cmd, false)\n\n\tbatchWithErrs := []routing.AggregatorResErr{}\n\t// Add all results to aggregator\n\tfor _, shardCmd := range cmds {\n\t\tvalue, err := ExtractCommandValue(shardCmd)\n\t\tbatchWithErrs = append(batchWithErrs, routing.AggregatorResErr{\n\t\t\tResult: value,\n\t\t\tErr:    err,\n\t\t})\n\t}\n\n\terr := aggregator.BatchSlice(batchWithErrs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.finishAggregation(cmd, aggregator)\n}\n\n// createAggregator creates the appropriate response aggregator\nfunc (c *ClusterClient) createAggregator(policy *routing.CommandPolicy, cmd Cmder, isKeyed bool) routing.ResponseAggregator {\n\tif policy != nil {\n\t\treturn routing.NewResponseAggregator(policy.Response, cmd.Name())\n\t}\n\n\tif !isKeyed {\n\t\tfirstKeyPos := cmdFirstKeyPos(cmd)\n\t\tisKeyed = firstKeyPos > 0\n\t}\n\n\treturn routing.NewDefaultAggregator(isKeyed)\n}\n\n// finishAggregation completes the aggregation process and sets the result\nfunc (c *ClusterClient) finishAggregation(cmd Cmder, aggregator routing.ResponseAggregator) error {\n\tfinalValue, finalErr := aggregator.Result()\n\tif finalErr != nil {\n\t\tcmd.SetErr(finalErr)\n\t\treturn finalErr\n\t}\n\n\treturn c.setCommandValue(cmd, finalValue)\n}\n\n// pickArbitraryNode selects a master or slave shard using the configured ShardPicker\nfunc (c *ClusterClient) pickArbitraryNode(ctx context.Context) *clusterNode {\n\tstate, err := c.state.Get(ctx)\n\tif err != nil || len(state.Masters) == 0 {\n\t\treturn nil\n\t}\n\n\tallNodes := append(state.Masters, state.Slaves...)\n\n\tidx := c.opt.ShardPicker.Next(len(allNodes))\n\treturn allNodes[idx]\n}\n\n// hasKeys checks if a command operates on keys\nfunc (c *ClusterClient) hasKeys(cmd Cmder) bool {\n\tfirstKeyPos := cmdFirstKeyPos(cmd)\n\treturn firstKeyPos > 0\n}\n\nfunc (c *ClusterClient) readOnlyEnabled() bool {\n\treturn c.opt.ReadOnly\n}\n\n// setCommandValue sets the aggregated value on a command using the enum-based approach\nfunc (c *ClusterClient) setCommandValue(cmd Cmder, value interface{}) error {\n\t// If value is nil, it might mean ExtractCommandValue couldn't extract the value\n\t// but the command might have executed successfully. In this case, don't set an error.\n\tif value == nil {\n\t\t// ExtractCommandValue returned nil - this means the command type is not supported\n\t\t// in the aggregation flow. This is a programming error, not a runtime error.\n\t\tif cmd.Err() != nil {\n\t\t\t// Command already has an error, preserve it\n\t\t\treturn cmd.Err()\n\t\t}\n\t\t// Command executed successfully but we can't extract/set the aggregated value\n\t\t// This indicates the command type needs to be added to ExtractCommandValue\n\t\treturn fmt.Errorf(\"redis: cannot aggregate command %s: unsupported command type %d\",\n\t\t\tcmd.Name(), cmd.GetCmdType())\n\t}\n\n\tswitch cmd.GetCmdType() {\n\tcase CmdTypeGeneric:\n\t\tif c, ok := cmd.(*Cmd); ok {\n\t\t\tc.SetVal(value)\n\t\t}\n\tcase CmdTypeString:\n\t\tif c, ok := cmd.(*StringCmd); ok {\n\t\t\tif v, ok := value.(string); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeInt:\n\t\tif c, ok := cmd.(*IntCmd); ok {\n\t\t\tif v, ok := value.(int64); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t} else if v, ok := value.(float64); ok {\n\t\t\t\tc.SetVal(int64(v))\n\t\t\t}\n\t\t}\n\tcase CmdTypeBool:\n\t\tif c, ok := cmd.(*BoolCmd); ok {\n\t\t\tif v, ok := value.(bool); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeFloat:\n\t\tif c, ok := cmd.(*FloatCmd); ok {\n\t\t\tif v, ok := value.(float64); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeStringSlice:\n\t\tif c, ok := cmd.(*StringSliceCmd); ok {\n\t\t\tif v, ok := value.([]string); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeIntSlice:\n\t\tif c, ok := cmd.(*IntSliceCmd); ok {\n\t\t\tif v, ok := value.([]int64); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t} else if v, ok := value.([]float64); ok {\n\t\t\t\tels := len(v)\n\t\t\t\tintSlc := make([]int, els)\n\t\t\t\tfor i := range v {\n\t\t\t\t\tintSlc[i] = int(v[i])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase CmdTypeFloatSlice:\n\t\tif c, ok := cmd.(*FloatSliceCmd); ok {\n\t\t\tif v, ok := value.([]float64); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeBoolSlice:\n\t\tif c, ok := cmd.(*BoolSliceCmd); ok {\n\t\t\tif v, ok := value.([]bool); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeMapStringString:\n\t\tif c, ok := cmd.(*MapStringStringCmd); ok {\n\t\t\tif v, ok := value.(map[string]string); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeMapStringInt:\n\t\tif c, ok := cmd.(*MapStringIntCmd); ok {\n\t\t\tif v, ok := value.(map[string]int64); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeMapStringInterface:\n\t\tif c, ok := cmd.(*MapStringInterfaceCmd); ok {\n\t\t\tif v, ok := value.(map[string]interface{}); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeSlice:\n\t\tif c, ok := cmd.(*SliceCmd); ok {\n\t\t\tif v, ok := value.([]interface{}); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeStatus:\n\t\tif c, ok := cmd.(*StatusCmd); ok {\n\t\t\tif v, ok := value.(string); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeDuration:\n\t\tif c, ok := cmd.(*DurationCmd); ok {\n\t\t\tif v, ok := value.(time.Duration); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeTime:\n\t\tif c, ok := cmd.(*TimeCmd); ok {\n\t\t\tif v, ok := value.(time.Time); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeKeyValueSlice:\n\t\tif c, ok := cmd.(*KeyValueSliceCmd); ok {\n\t\t\tif v, ok := value.([]KeyValue); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeStringStructMap:\n\t\tif c, ok := cmd.(*StringStructMapCmd); ok {\n\t\t\tif v, ok := value.(map[string]struct{}); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXMessageSlice:\n\t\tif c, ok := cmd.(*XMessageSliceCmd); ok {\n\t\t\tif v, ok := value.([]XMessage); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXStreamSlice:\n\t\tif c, ok := cmd.(*XStreamSliceCmd); ok {\n\t\t\tif v, ok := value.([]XStream); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXPending:\n\t\tif c, ok := cmd.(*XPendingCmd); ok {\n\t\t\tif v, ok := value.(*XPending); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXPendingExt:\n\t\tif c, ok := cmd.(*XPendingExtCmd); ok {\n\t\t\tif v, ok := value.([]XPendingExt); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXAutoClaim:\n\t\tif c, ok := cmd.(*XAutoClaimCmd); ok {\n\t\t\tif v, ok := value.(CmdTypeXAutoClaimValue); ok {\n\t\t\t\tc.SetVal(v.messages, v.start)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXAutoClaimJustID:\n\t\tif c, ok := cmd.(*XAutoClaimJustIDCmd); ok {\n\t\t\tif v, ok := value.(CmdTypeXAutoClaimJustIDValue); ok {\n\t\t\t\tc.SetVal(v.ids, v.start)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXInfoConsumers:\n\t\tif c, ok := cmd.(*XInfoConsumersCmd); ok {\n\t\t\tif v, ok := value.([]XInfoConsumer); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXInfoGroups:\n\t\tif c, ok := cmd.(*XInfoGroupsCmd); ok {\n\t\t\tif v, ok := value.([]XInfoGroup); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXInfoStream:\n\t\tif c, ok := cmd.(*XInfoStreamCmd); ok {\n\t\t\tif v, ok := value.(*XInfoStream); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeXInfoStreamFull:\n\t\tif c, ok := cmd.(*XInfoStreamFullCmd); ok {\n\t\t\tif v, ok := value.(*XInfoStreamFull); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeZSlice:\n\t\tif c, ok := cmd.(*ZSliceCmd); ok {\n\t\t\tif v, ok := value.([]Z); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeZWithKey:\n\t\tif c, ok := cmd.(*ZWithKeyCmd); ok {\n\t\t\tif v, ok := value.(*ZWithKey); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeScan:\n\t\tif c, ok := cmd.(*ScanCmd); ok {\n\t\t\tif v, ok := value.(CmdTypeScanValue); ok {\n\t\t\t\tc.SetVal(v.keys, v.cursor)\n\t\t\t}\n\t\t}\n\tcase CmdTypeClusterSlots:\n\t\tif c, ok := cmd.(*ClusterSlotsCmd); ok {\n\t\t\tif v, ok := value.([]ClusterSlot); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeGeoLocation:\n\t\tif c, ok := cmd.(*GeoLocationCmd); ok {\n\t\t\tif v, ok := value.([]GeoLocation); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeGeoSearchLocation:\n\t\tif c, ok := cmd.(*GeoSearchLocationCmd); ok {\n\t\t\tif v, ok := value.([]GeoLocation); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeGeoPos:\n\t\tif c, ok := cmd.(*GeoPosCmd); ok {\n\t\t\tif v, ok := value.([]*GeoPos); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeCommandsInfo:\n\t\tif c, ok := cmd.(*CommandsInfoCmd); ok {\n\t\t\tif v, ok := value.(map[string]*CommandInfo); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeSlowLog:\n\t\tif c, ok := cmd.(*SlowLogCmd); ok {\n\t\t\tif v, ok := value.([]SlowLog); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeMapStringStringSlice:\n\t\tif c, ok := cmd.(*MapStringStringSliceCmd); ok {\n\t\t\tif v, ok := value.([]map[string]string); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeMapMapStringInterface:\n\t\tif c, ok := cmd.(*MapMapStringInterfaceCmd); ok {\n\t\t\tif v, ok := value.(map[string]interface{}); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeMapStringInterfaceSlice:\n\t\tif c, ok := cmd.(*MapStringInterfaceSliceCmd); ok {\n\t\t\tif v, ok := value.([]map[string]interface{}); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeKeyValues:\n\t\tif c, ok := cmd.(*KeyValuesCmd); ok {\n\t\t\t// KeyValuesCmd needs a key string and values slice\n\t\t\tif v, ok := value.(CmdTypeKeyValuesValue); ok {\n\t\t\t\tc.SetVal(v.key, v.values)\n\t\t\t}\n\t\t}\n\tcase CmdTypeZSliceWithKey:\n\t\tif c, ok := cmd.(*ZSliceWithKeyCmd); ok {\n\t\t\t// ZSliceWithKeyCmd needs a key string and Z slice\n\t\t\tif v, ok := value.(CmdTypeZSliceWithKeyValue); ok {\n\t\t\t\tc.SetVal(v.key, v.zSlice)\n\t\t\t}\n\t\t}\n\tcase CmdTypeFunctionList:\n\t\tif c, ok := cmd.(*FunctionListCmd); ok {\n\t\t\tif v, ok := value.([]Library); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeFunctionStats:\n\t\tif c, ok := cmd.(*FunctionStatsCmd); ok {\n\t\t\tif v, ok := value.(FunctionStats); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeLCS:\n\t\tif c, ok := cmd.(*LCSCmd); ok {\n\t\t\tif v, ok := value.(*LCSMatch); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeKeyFlags:\n\t\tif c, ok := cmd.(*KeyFlagsCmd); ok {\n\t\t\tif v, ok := value.([]KeyFlags); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeClusterLinks:\n\t\tif c, ok := cmd.(*ClusterLinksCmd); ok {\n\t\t\tif v, ok := value.([]ClusterLink); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeClusterShards:\n\t\tif c, ok := cmd.(*ClusterShardsCmd); ok {\n\t\t\tif v, ok := value.([]ClusterShard); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeRankWithScore:\n\t\tif c, ok := cmd.(*RankWithScoreCmd); ok {\n\t\t\tif v, ok := value.(RankScore); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeClientInfo:\n\t\tif c, ok := cmd.(*ClientInfoCmd); ok {\n\t\t\tif v, ok := value.(*ClientInfo); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeACLLog:\n\t\tif c, ok := cmd.(*ACLLogCmd); ok {\n\t\t\tif v, ok := value.([]*ACLLogEntry); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeInfo:\n\t\tif c, ok := cmd.(*InfoCmd); ok {\n\t\t\tif v, ok := value.(map[string]map[string]string); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeMonitor:\n\t\t// MonitorCmd doesn't have SetVal method\n\t\t// Skip setting value for MonitorCmd\n\tcase CmdTypeJSON:\n\t\tif c, ok := cmd.(*JSONCmd); ok {\n\t\t\tif v, ok := value.(string); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeJSONSlice:\n\t\tif c, ok := cmd.(*JSONSliceCmd); ok {\n\t\t\tif v, ok := value.([]interface{}); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeIntPointerSlice:\n\t\tif c, ok := cmd.(*IntPointerSliceCmd); ok {\n\t\t\tif v, ok := value.([]*int64); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeScanDump:\n\t\tif c, ok := cmd.(*ScanDumpCmd); ok {\n\t\t\tif v, ok := value.(ScanDump); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeBFInfo:\n\t\tif c, ok := cmd.(*BFInfoCmd); ok {\n\t\t\tif v, ok := value.(BFInfo); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeCFInfo:\n\t\tif c, ok := cmd.(*CFInfoCmd); ok {\n\t\t\tif v, ok := value.(CFInfo); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeCMSInfo:\n\t\tif c, ok := cmd.(*CMSInfoCmd); ok {\n\t\t\tif v, ok := value.(CMSInfo); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeTopKInfo:\n\t\tif c, ok := cmd.(*TopKInfoCmd); ok {\n\t\t\tif v, ok := value.(TopKInfo); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeTDigestInfo:\n\t\tif c, ok := cmd.(*TDigestInfoCmd); ok {\n\t\t\tif v, ok := value.(TDigestInfo); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeFTSynDump:\n\t\tif c, ok := cmd.(*FTSynDumpCmd); ok {\n\t\t\tif v, ok := value.([]FTSynDumpResult); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeAggregate:\n\t\tif c, ok := cmd.(*AggregateCmd); ok {\n\t\t\tif v, ok := value.(*FTAggregateResult); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeFTInfo:\n\t\tif c, ok := cmd.(*FTInfoCmd); ok {\n\t\t\tif v, ok := value.(FTInfoResult); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeFTSpellCheck:\n\t\tif c, ok := cmd.(*FTSpellCheckCmd); ok {\n\t\t\tif v, ok := value.([]SpellCheckResult); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeFTSearch:\n\t\tif c, ok := cmd.(*FTSearchCmd); ok {\n\t\t\tif v, ok := value.(FTSearchResult); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeTSTimestampValue:\n\t\tif c, ok := cmd.(*TSTimestampValueCmd); ok {\n\t\t\tif v, ok := value.(TSTimestampValue); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tcase CmdTypeTSTimestampValueSlice:\n\t\tif c, ok := cmd.(*TSTimestampValueSliceCmd); ok {\n\t\t\tif v, ok := value.([]TSTimestampValue); ok {\n\t\t\t\tc.SetVal(v)\n\t\t\t}\n\t\t}\n\tdefault:\n\t\t// Fallback to reflection for unknown types\n\t\treturn c.setCommandValueReflection(cmd, value)\n\t}\n\n\treturn nil\n}\n\n// setCommandValueReflection is a fallback function that uses reflection\nfunc (c *ClusterClient) setCommandValueReflection(cmd Cmder, value interface{}) error {\n\tcmdValue := reflect.ValueOf(cmd)\n\tif cmdValue.Kind() != reflect.Ptr || cmdValue.IsNil() {\n\t\treturn errInvalidCmdPointer\n\t}\n\n\tsetValMethod := cmdValue.MethodByName(\"SetVal\")\n\tif !setValMethod.IsValid() {\n\t\treturn fmt.Errorf(\"redis: command %T does not have SetVal method\", cmd)\n\t}\n\n\targs := []reflect.Value{reflect.ValueOf(value)}\n\n\tswitch cmd.(type) {\n\tcase *XAutoClaimCmd, *XAutoClaimJustIDCmd:\n\t\targs = append(args, reflect.ValueOf(\"\"))\n\tcase *ScanCmd:\n\t\targs = append(args, reflect.ValueOf(uint64(0)))\n\tcase *KeyValuesCmd, *ZSliceWithKeyCmd:\n\t\tif key, ok := value.(string); ok {\n\t\t\targs = []reflect.Value{reflect.ValueOf(key)}\n\t\t\tif _, ok := cmd.(*ZSliceWithKeyCmd); ok {\n\t\t\t\targs = append(args, reflect.ValueOf([]Z{}))\n\t\t\t} else {\n\t\t\t\targs = append(args, reflect.ValueOf([]string{}))\n\t\t\t}\n\t\t}\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tcmd.SetErr(fmt.Errorf(\"redis: failed to set command value: %v\", r))\n\t\t}\n\t}()\n\n\tsetValMethod.Call(args)\n\treturn nil\n}\n"
  },
  {
    "path": "osscluster_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n\t\"github.com/redis/go-redis/v9/internal/routing\"\n)\n\ntype clusterScenario struct {\n\tports   []string\n\tnodeIDs []string\n\tclients map[string]*redis.Client\n}\n\nfunc (s *clusterScenario) slots() []int {\n\treturn []int{0, 5461, 10923, 16384}\n}\n\nfunc (s *clusterScenario) masters() []*redis.Client {\n\tresult := make([]*redis.Client, 3)\n\tfor pos, port := range s.ports[:3] {\n\t\tresult[pos] = s.clients[port]\n\t}\n\treturn result\n}\n\nfunc (s *clusterScenario) slaves() []*redis.Client {\n\tresult := make([]*redis.Client, 3)\n\tfor pos, port := range s.ports[3:] {\n\t\tresult[pos] = s.clients[port]\n\t}\n\treturn result\n}\n\nfunc (s *clusterScenario) addrs() []string {\n\taddrs := make([]string, len(s.ports))\n\tfor i, port := range s.ports {\n\t\taddrs[i] = net.JoinHostPort(\"127.0.0.1\", port)\n\t}\n\treturn addrs\n}\n\nfunc (s *clusterScenario) newClusterClientUnstable(opt *redis.ClusterOptions) *redis.ClusterClient {\n\topt.Addrs = s.addrs()\n\treturn redis.NewClusterClient(opt)\n}\n\nfunc (s *clusterScenario) newClusterClient(\n\tctx context.Context, opt *redis.ClusterOptions,\n) *redis.ClusterClient {\n\tclient := s.newClusterClientUnstable(opt)\n\tclient.SetCommandInfoResolver(client.NewDynamicResolver())\n\terr := eventually(func() error {\n\t\tif opt.ClusterSlots != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tstate, err := client.LoadState(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !state.IsConsistent(ctx) {\n\t\t\treturn fmt.Errorf(\"cluster state is not consistent\")\n\t\t}\n\n\t\treturn nil\n\t}, 30*time.Second)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn client\n}\n\nfunc (s *clusterScenario) Close() error {\n\tctx := context.TODO()\n\tfor _, master := range s.masters() {\n\t\tif master == nil {\n\t\t\tcontinue\n\t\t}\n\t\terr := master.FlushAll(ctx).Err()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// since 7.2 forget calls should be propagated, calling only master\n\t\t// nodes should be sufficient.\n\t\tfor _, nID := range s.nodeIDs {\n\t\t\tmaster.ClusterForget(ctx, nID)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc configureClusterTopology(ctx context.Context, scenario *clusterScenario) error {\n\tallowErrs := []string{\n\t\t\"ERR Slot 0 is already busy\",\n\t\t\"ERR Slot 5461 is already busy\",\n\t\t\"ERR Slot 10923 is already busy\",\n\t\t\"ERR Slot 16384 is already busy\",\n\t}\n\n\terr := collectNodeInformation(ctx, scenario)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Meet cluster nodes.\n\tfor _, client := range scenario.clients {\n\t\terr := client.ClusterMeet(ctx, \"127.0.0.1\", scenario.ports[0]).Err()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tslots := scenario.slots()\n\tfor pos, master := range scenario.masters() {\n\t\terr := master.ClusterAddSlotsRange(ctx, slots[pos], slots[pos+1]-1).Err()\n\t\tif err != nil && slices.Contains(allowErrs, err.Error()) == false {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Bootstrap slaves.\n\tfor idx, slave := range scenario.slaves() {\n\t\tmasterID := scenario.nodeIDs[idx]\n\n\t\t// Wait until master is available\n\t\terr := eventually(func() error {\n\t\t\ts := slave.ClusterNodes(ctx).Val()\n\t\t\twanted := masterID\n\t\t\tif !strings.Contains(s, wanted) {\n\t\t\t\treturn fmt.Errorf(\"%q does not contain %q\", s, wanted)\n\t\t\t}\n\t\t\treturn nil\n\t\t}, 10*time.Second)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\terr = slave.ClusterReplicate(ctx, masterID).Err()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Wait until all nodes have consistent info.\n\twanted := []redis.ClusterSlot{{\n\t\tStart: 0,\n\t\tEnd:   5460,\n\t\tNodes: []redis.ClusterNode{{\n\t\t\tID:   \"\",\n\t\t\tAddr: \"127.0.0.1:16600\",\n\t\t}, {\n\t\t\tID:   \"\",\n\t\t\tAddr: \"127.0.0.1:16603\",\n\t\t}},\n\t}, {\n\t\tStart: 5461,\n\t\tEnd:   10922,\n\t\tNodes: []redis.ClusterNode{{\n\t\t\tID:   \"\",\n\t\t\tAddr: \"127.0.0.1:16601\",\n\t\t}, {\n\t\t\tID:   \"\",\n\t\t\tAddr: \"127.0.0.1:16604\",\n\t\t}},\n\t}, {\n\t\tStart: 10923,\n\t\tEnd:   16383,\n\t\tNodes: []redis.ClusterNode{{\n\t\t\tID:   \"\",\n\t\t\tAddr: \"127.0.0.1:16602\",\n\t\t}, {\n\t\t\tID:   \"\",\n\t\t\tAddr: \"127.0.0.1:16605\",\n\t\t}},\n\t}}\n\n\tfor _, client := range scenario.clients {\n\t\terr := eventually(func() error {\n\t\t\tres, err := client.ClusterSlots(ctx).Result()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn assertSlotsEqual(res, wanted)\n\t\t}, 90*time.Second)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc collectNodeInformation(ctx context.Context, scenario *clusterScenario) error {\n\tfor pos, port := range scenario.ports {\n\t\tclient := redis.NewClient(&redis.Options{\n\t\t\tAddr: \":\" + port,\n\t\t})\n\n\t\tmyID, err := client.ClusterMyID(ctx).Result()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tscenario.clients[port] = client\n\t\tscenario.nodeIDs[pos] = myID\n\t}\n\treturn nil\n}\n\nfunc assertSlotsEqual(slots, wanted []redis.ClusterSlot) error {\nouterLoop:\n\tfor _, s2 := range wanted {\n\t\tfor _, s1 := range slots {\n\t\t\tif slotEqual(s1, s2) {\n\t\t\t\tcontinue outerLoop\n\t\t\t}\n\t\t}\n\t\treturn fmt.Errorf(\"%v not found in %v\", s2, slots)\n\t}\n\treturn nil\n}\n\nfunc slotEqual(s1, s2 redis.ClusterSlot) bool {\n\tif s1.Start != s2.Start {\n\t\treturn false\n\t}\n\tif s1.End != s2.End {\n\t\treturn false\n\t}\n\tif len(s1.Nodes) != len(s2.Nodes) {\n\t\treturn false\n\t}\n\tfor i, n1 := range s1.Nodes {\n\t\tif n1.Addr != s2.Nodes[i].Addr {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// ------------------------------------------------------------------------------\n\nvar _ = Describe(\"ClusterClient\", func() {\n\tvar failover bool\n\tvar opt *redis.ClusterOptions\n\tvar client *redis.ClusterClient\n\n\tassertClusterClient := func() {\n\t\tIt(\"do\", func() {\n\t\t\tval, err := client.Do(ctx, \"ping\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should GET/SET/DEL\", func() {\n\t\t\terr := client.Get(ctx, \"A\").Err()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\n\t\t\terr = client.Set(ctx, \"A\", \"VALUE\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tEventually(func() string {\n\t\t\t\treturn client.Get(ctx, \"A\").Val()\n\t\t\t}, 30*time.Second).Should(Equal(\"VALUE\"))\n\n\t\t\tcnt, err := client.Del(ctx, \"A\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cnt).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should follow redirects for GET\", func() {\n\t\t\terr := client.Set(ctx, \"A\", \"VALUE\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tif !failover {\n\t\t\t\tEventually(func() int64 {\n\t\t\t\t\tnodes, err := client.Nodes(ctx, \"A\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn 0\n\t\t\t\t\t}\n\t\t\t\t\treturn nodes[1].Client.DBSize(ctx).Val()\n\t\t\t\t}, 30*time.Second).Should(Equal(int64(1)))\n\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn client.SwapNodes(ctx, \"A\")\n\t\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\t\t\t}\n\n\t\t\tv, err := client.Get(ctx, \"A\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(\"VALUE\"))\n\t\t})\n\n\t\tIt(\"should follow redirects for SET\", func() {\n\t\t\tif !failover {\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn client.SwapNodes(ctx, \"A\")\n\t\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\t\t\t}\n\n\t\t\terr := client.Set(ctx, \"A\", \"VALUE\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.Get(ctx, \"A\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(\"VALUE\"))\n\t\t})\n\n\t\tIt(\"should distribute keys\", func() {\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\terr := client.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tEventually(func() string {\n\t\t\t\t\treturn master.Info(ctx, \"keyspace\").Val()\n\t\t\t\t}, 30*time.Second).Should(Or(\n\t\t\t\t\tContainSubstring(\"keys=32\"),\n\t\t\t\t\tContainSubstring(\"keys=36\"),\n\t\t\t\t\tContainSubstring(\"keys=32\"),\n\t\t\t\t))\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should distribute keys when using EVAL\", func() {\n\t\t\tscript := redis.NewScript(`\n\t\t\t\tlocal r = redis.call('SET', KEYS[1], ARGV[1])\n\t\t\t\treturn r\n\t\t\t`)\n\n\t\t\tvar key string\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tkey = fmt.Sprintf(\"key%d\", i)\n\t\t\t\terr := script.Run(ctx, client, []string{key}, \"value\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tEventually(func() string {\n\t\t\t\t\treturn master.Info(ctx, \"keyspace\").Val()\n\t\t\t\t}, 30*time.Second).Should(Or(\n\t\t\t\t\tContainSubstring(\"keys=32\"),\n\t\t\t\t\tContainSubstring(\"keys=36\"),\n\t\t\t\t\tContainSubstring(\"keys=32\"),\n\t\t\t\t))\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should distribute scripts when using Script Load\", func() {\n\t\t\tclient.ScriptFlush(ctx)\n\n\t\t\tscript := redis.NewScript(`return 'Unique script'`)\n\n\t\t\tscript.Load(ctx, client)\n\n\t\t\terr := client.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\tval, _ := script.Exists(ctx, shard).Result()\n\t\t\t\tExpect(val[0]).To(Equal(true))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should check all shards when using Script Exists\", func() {\n\t\t\tclient.ScriptFlush(ctx)\n\n\t\t\tscript := redis.NewScript(`return 'First script'`)\n\t\t\tlostScriptSrc := `return 'Lost script'`\n\t\t\tlostScript := redis.NewScript(lostScriptSrc)\n\n\t\t\tscript.Load(ctx, client)\n\t\t\tclient.Do(ctx, \"script\", \"load\", lostScriptSrc)\n\n\t\t\tval, _ := client.ScriptExists(ctx, script.Hash(), lostScript.Hash()).Result()\n\n\t\t\tExpect(val).To(Equal([]bool{true, false}))\n\t\t})\n\n\t\tIt(\"should flush scripts from all shards when using ScriptFlush\", func() {\n\t\t\tscript := redis.NewScript(`return 'Unnecessary script'`)\n\t\t\tscript.Load(ctx, client)\n\n\t\t\tval, _ := client.ScriptExists(ctx, script.Hash()).Result()\n\t\t\tExpect(val).To(Equal([]bool{true}))\n\n\t\t\tclient.ScriptFlush(ctx)\n\n\t\t\tval, _ = client.ScriptExists(ctx, script.Hash()).Result()\n\t\t\tExpect(val).To(Equal([]bool{false}))\n\t\t})\n\n\t\tIt(\"should support Watch\", func() {\n\t\t\tvar incr func(string) error\n\n\t\t\t// Transactionally increments key using GET and SET commands.\n\t\t\tincr = func(key string) error {\n\t\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t\tn, err := tx.Get(ctx, key).Int64()\n\t\t\t\t\tif err != nil && err != redis.Nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\t_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\t\tpipe.Set(ctx, key, strconv.FormatInt(n+1, 10), 0)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\t\t\t\t\treturn err\n\t\t\t\t}, key)\n\t\t\t\tif err == redis.TxFailedErr {\n\t\t\t\t\treturn incr(key)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tvar wg sync.WaitGroup\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func() {\n\t\t\t\t\tdefer GinkgoRecover()\n\t\t\t\t\tdefer wg.Done()\n\n\t\t\t\t\terr := incr(\"key\")\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}()\n\t\t\t}\n\t\t\twg.Wait()\n\n\t\t\tEventually(func() string {\n\t\t\t\treturn client.Get(ctx, \"key\").Val()\n\t\t\t}, 30*time.Second).Should(Equal(\"100\"))\n\t\t})\n\n\t\tDescribe(\"pipelining\", func() {\n\t\t\tvar pipe *redis.Pipeline\n\n\t\t\tassertPipeline := func(keys []string) {\n\n\t\t\t\tIt(\"should follow redirects\", func() {\n\t\t\t\t\tif !failover {\n\t\t\t\t\t\tfor _, key := range keys {\n\t\t\t\t\t\t\tEventually(func() error {\n\t\t\t\t\t\t\t\treturn client.SwapNodes(ctx, key)\n\t\t\t\t\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor i, key := range keys {\n\t\t\t\t\t\tpipe.Set(ctx, key, key+\"_value\", 0)\n\t\t\t\t\t\tpipe.Expire(ctx, key, time.Duration(i+1)*time.Hour)\n\t\t\t\t\t}\n\t\t\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(cmds).To(HaveLen(14))\n\n\t\t\t\t\t// Check that all keys are set.\n\t\t\t\t\tfor _, key := range keys {\n\t\t\t\t\t\tEventually(func() string {\n\t\t\t\t\t\t\treturn client.Get(ctx, key).Val()\n\t\t\t\t\t\t}, 30*time.Second).Should(Equal(key + \"_value\"))\n\t\t\t\t\t}\n\n\t\t\t\t\tif !failover {\n\t\t\t\t\t\tfor _, key := range keys {\n\t\t\t\t\t\t\tEventually(func() error {\n\t\t\t\t\t\t\t\treturn client.SwapNodes(ctx, key)\n\t\t\t\t\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, key := range keys {\n\t\t\t\t\t\tpipe.Get(ctx, key)\n\t\t\t\t\t\tpipe.TTL(ctx, key)\n\t\t\t\t\t}\n\t\t\t\t\tcmds, err = pipe.Exec(ctx)\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(cmds).To(HaveLen(14))\n\n\t\t\t\t\tfor i, key := range keys {\n\t\t\t\t\t\tget := cmds[i*2].(*redis.StringCmd)\n\t\t\t\t\t\tExpect(get.Val()).To(Equal(key + \"_value\"))\n\n\t\t\t\t\t\tttl := cmds[(i*2)+1].(*redis.DurationCmd)\n\t\t\t\t\t\tdur := time.Duration(i+1) * time.Hour\n\t\t\t\t\t\tExpect(ttl.Val()).To(BeNumerically(\"~\", dur, 30*time.Second))\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\tIt(\"should work with missing keys\", func() {\n\t\t\t\t\tpipe.Set(ctx, \"A{s}\", \"A_value\", 0)\n\t\t\t\t\tpipe.Set(ctx, \"C{s}\", \"C_value\", 0)\n\t\t\t\t\t_, err := pipe.Exec(ctx)\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\ta := pipe.Get(ctx, \"A{s}\")\n\t\t\t\t\tb := pipe.Get(ctx, \"B{s}\")\n\t\t\t\t\tc := pipe.Get(ctx, \"C{s}\")\n\t\t\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\t\t\tExpect(cmds).To(HaveLen(3))\n\n\t\t\t\t\tExpect(a.Err()).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(a.Val()).To(Equal(\"A_value\"))\n\n\t\t\t\t\tExpect(b.Err()).To(Equal(redis.Nil))\n\t\t\t\t\tExpect(b.Val()).To(Equal(\"\"))\n\n\t\t\t\t\tExpect(c.Err()).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(c.Val()).To(Equal(\"C_value\"))\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tDescribe(\"with Pipeline\", func() {\n\t\t\t\tBeforeEach(func() {\n\t\t\t\t\tpipe = client.Pipeline().(*redis.Pipeline)\n\t\t\t\t})\n\n\t\t\t\tAfterEach(func() {})\n\n\t\t\t\tkeys := []string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\"}\n\t\t\t\tassertPipeline(keys)\n\n\t\t\t\tIt(\"should not fail node with context.Canceled error\", func() {\n\t\t\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\t\t\tcancel()\n\t\t\t\t\tpipe.Set(ctx, \"A\", \"A_value\", 0)\n\t\t\t\t\t_, err := pipe.Exec(ctx)\n\n\t\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\t\tExpect(errors.Is(err, context.Canceled)).To(BeTrue())\n\n\t\t\t\t\tclientNodes, _ := client.Nodes(ctx, \"A\")\n\n\t\t\t\t\tfor _, node := range clientNodes {\n\t\t\t\t\t\tExpect(node.Failing()).To(BeFalse())\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\tIt(\"should not fail node with context.DeadlineExceeded error\", func() {\n\t\t\t\t\tctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)\n\t\t\t\t\tdefer cancel()\n\n\t\t\t\t\tpipe.Set(ctx, \"A\", \"A_value\", 0)\n\t\t\t\t\t_, err := pipe.Exec(ctx)\n\n\t\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\t\tExpect(errors.Is(err, context.DeadlineExceeded)).To(BeTrue())\n\n\t\t\t\t\tclientNodes, _ := client.Nodes(ctx, \"A\")\n\n\t\t\t\t\tfor _, node := range clientNodes {\n\t\t\t\t\t\tExpect(node.Failing()).To(BeFalse())\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tDescribe(\"with TxPipeline\", func() {\n\t\t\t\tBeforeEach(func() {\n\t\t\t\t\tpipe = client.TxPipeline().(*redis.Pipeline)\n\t\t\t\t})\n\n\t\t\t\tAfterEach(func() {})\n\n\t\t\t\t// TxPipeline doesn't support cross slot commands.\n\t\t\t\t// Use hashtag to force all keys to the same slot.\n\t\t\t\tkeys := []string{\"A{s}\", \"B{s}\", \"C{s}\", \"D{s}\", \"E{s}\", \"F{s}\", \"G{s}\"}\n\t\t\t\tassertPipeline(keys)\n\n\t\t\t\t// make sure CrossSlot error is returned\n\t\t\t\tIt(\"returns CrossSlot error\", func() {\n\t\t\t\t\tpipe.Set(ctx, \"A{s}\", \"A_value\", 0)\n\t\t\t\t\tpipe.Set(ctx, \"B{t}\", \"B_value\", 0)\n\t\t\t\t\tExpect(hashtag.Slot(\"A{s}\")).NotTo(Equal(hashtag.Slot(\"B{t}\")))\n\t\t\t\t\t_, err := pipe.Exec(ctx)\n\t\t\t\t\tExpect(err).To(MatchError(redis.ErrCrossSlot))\n\t\t\t\t})\n\n\t\t\t\tIt(\"works normally with keyless commands and no CrossSlot error\", func() {\n\t\t\t\t\tpipe.Set(ctx, \"A{s}\", \"A_value\", 0)\n\t\t\t\t\tpipe.Ping(ctx)\n\t\t\t\t\tpipe.Set(ctx, \"B{s}\", \"B_value\", 0)\n\t\t\t\t\tpipe.Ping(ctx)\n\t\t\t\t\t_, err := pipe.Exec(ctx)\n\t\t\t\t\tExpect(err).To(Not(HaveOccurred()))\n\t\t\t\t})\n\n\t\t\t\t// doesn't fail when no commands are queued\n\t\t\t\tIt(\"returns no error when there are no commands\", func() {\n\t\t\t\t\t_, err := pipe.Exec(ctx)\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\t\tIt(\"should support PubSub\", func() {\n\t\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\t\tdefer pubsub.Close()\n\n\t\t\tEventually(func() error {\n\t\t\t\t_, err := client.Publish(ctx, \"mychannel\", \"hello\").Result()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tmsg, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t_, ok := msg.(*redis.Message)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"got %T, wanted *redis.Message\", msg)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should support sharded PubSub\", func() {\n\t\t\tpubsub := client.SSubscribe(ctx, \"mychannel\")\n\t\t\tdefer pubsub.Close()\n\n\t\t\tEventually(func() error {\n\t\t\t\t_, err := client.SPublish(ctx, \"mychannel\", \"hello\").Result()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tmsg, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t_, ok := msg.(*redis.Message)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"got %T, wanted *redis.Message\", msg)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should support PubSub.Ping without channels\", func() {\n\t\t\tpubsub := client.Subscribe(ctx)\n\t\t\tdefer pubsub.Close()\n\n\t\t\terr := pubsub.Ping(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\t}\n\n\tDescribe(\"ClusterClient PROTO 2\", func() {\n\t\tBeforeEach(func() {\n\t\t\topt = redisClusterOptions()\n\t\t\topt.Protocol = 2\n\t\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\t_ = client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should CLUSTER PROTO 2\", func() {\n\t\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\t\tval, err := c.Do(ctx, \"HELLO\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).Should(ContainElements(\"proto\", int64(2)))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t})\n\t})\n\n\tDescribe(\"ClusterClient\", func() {\n\t\tBeforeEach(func() {\n\t\t\topt = redisClusterOptions()\n\t\t\topt.ClientName = \"cluster_hi\"\n\t\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\t_ = client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should return pool stats\", func() {\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats).To(BeAssignableToTypeOf(&redis.PoolStats{}))\n\t\t})\n\n\t\tIt(\"should return an error when there are no attempts left\", func() {\n\t\t\topt := redisClusterOptions()\n\t\t\topt.MaxRedirects = -1\n\t\t\tclient := cluster.newClusterClient(ctx, opt)\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.SwapNodes(ctx, \"A\")\n\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\n\t\t\terr := client.Get(ctx, \"A\").Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(ContainSubstring(\"MOVED\"))\n\n\t\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should determine hash slots correctly for generic commands\", func() {\n\t\t\topt := redisClusterOptions()\n\t\t\topt.MaxRedirects = -1\n\t\t\tclient := cluster.newClusterClient(ctx, opt)\n\n\t\t\terr := client.Do(ctx, \"GET\", \"A\").Err()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\n\t\t\terr = client.Do(ctx, []byte(\"GET\"), []byte(\"A\")).Err()\n\t\t\tExpect(err).To(Equal(redis.Nil))\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.SwapNodes(ctx, \"A\")\n\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\n\t\t\terr = client.Do(ctx, \"GET\", \"A\").Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(ContainSubstring(\"MOVED\"))\n\n\t\t\terr = client.Do(ctx, []byte(\"GET\"), []byte(\"A\")).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(ContainSubstring(\"MOVED\"))\n\n\t\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should follow node redirection immediately\", func() {\n\t\t\t// Configure retry backoffs far in excess of the expected duration of redirection\n\t\t\topt := redisClusterOptions()\n\t\t\topt.MinRetryBackoff = 10 * time.Minute\n\t\t\topt.MaxRetryBackoff = 20 * time.Minute\n\t\t\tclient := cluster.newClusterClient(ctx, opt)\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.SwapNodes(ctx, \"A\")\n\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\n\t\t\t// Note that this context sets a deadline more aggressive than the lowest possible bound\n\t\t\t// of the retry backoff; this verifies that redirection completes immediately.\n\t\t\tredirCtx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\t\t\tdefer cancel()\n\n\t\t\terr := client.Set(redirCtx, \"A\", \"VALUE\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tv, err := client.Get(redirCtx, \"A\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(v).To(Equal(\"VALUE\"))\n\n\t\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should call fn for every master node\", func() {\n\t\t\tfor i := 0; i < 10; i++ {\n\t\t\t\tExpect(client.Set(ctx, strconv.Itoa(i), \"\", 0).Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tsize, err := client.DBSize(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(size).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should CLUSTER SLOTS\", func() {\n\t\t\tres, err := client.ClusterSlots(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(HaveLen(3))\n\n\t\t\twanted := []redis.ClusterSlot{{\n\t\t\t\tStart: 0,\n\t\t\t\tEnd:   5460,\n\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\tID:   \"\",\n\t\t\t\t\tAddr: \"127.0.0.1:16600\",\n\t\t\t\t}, {\n\t\t\t\t\tID:   \"\",\n\t\t\t\t\tAddr: \"127.0.0.1:16603\",\n\t\t\t\t}},\n\t\t\t}, {\n\t\t\t\tStart: 5461,\n\t\t\t\tEnd:   10922,\n\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\tID:   \"\",\n\t\t\t\t\tAddr: \"127.0.0.1:16601\",\n\t\t\t\t}, {\n\t\t\t\t\tID:   \"\",\n\t\t\t\t\tAddr: \"127.0.0.1:16604\",\n\t\t\t\t}},\n\t\t\t}, {\n\t\t\t\tStart: 10923,\n\t\t\t\tEnd:   16383,\n\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\tID:   \"\",\n\t\t\t\t\tAddr: \"127.0.0.1:16602\",\n\t\t\t\t}, {\n\t\t\t\t\tID:   \"\",\n\t\t\t\t\tAddr: \"127.0.0.1:16605\",\n\t\t\t\t}},\n\t\t\t}}\n\t\t\tExpect(assertSlotsEqual(res, wanted)).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should CLUSTER SHARDS\", func() {\n\t\t\tres, err := client.ClusterShards(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).NotTo(BeEmpty())\n\n\t\t\t// Iterate over the ClusterShard results and validate the fields.\n\t\t\tfor _, shard := range res {\n\t\t\t\tExpect(shard.Slots).NotTo(BeEmpty())\n\t\t\t\tfor _, slotRange := range shard.Slots {\n\t\t\t\t\tExpect(slotRange.Start).To(BeNumerically(\">=\", 0))\n\t\t\t\t\tExpect(slotRange.End).To(BeNumerically(\">=\", slotRange.Start))\n\t\t\t\t}\n\n\t\t\t\tExpect(shard.Nodes).NotTo(BeEmpty())\n\t\t\t\tfor _, node := range shard.Nodes {\n\t\t\t\t\tExpect(node.ID).NotTo(BeEmpty())\n\t\t\t\t\tExpect(node.Endpoint).NotTo(BeEmpty())\n\t\t\t\t\tExpect(node.IP).NotTo(BeEmpty())\n\t\t\t\t\tExpect(node.Port).To(BeNumerically(\">\", 0))\n\n\t\t\t\t\tvalidRoles := []string{\"master\", \"slave\", \"replica\"}\n\t\t\t\t\tExpect(validRoles).To(ContainElement(node.Role))\n\n\t\t\t\t\tExpect(node.ReplicationOffset).To(BeNumerically(\">=\", 0))\n\n\t\t\t\t\tvalidHealthStatuses := []string{\"online\", \"failed\", \"loading\"}\n\t\t\t\t\tExpect(validHealthStatuses).To(ContainElement(node.Health))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should CLUSTER LINKS\", func() {\n\t\t\tres, err := client.ClusterLinks(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).NotTo(BeEmpty())\n\n\t\t\t// Iterate over the ClusterLink results and validate the map keys.\n\t\t\tfor _, link := range res {\n\n\t\t\t\tExpect(link.Direction).NotTo(BeEmpty())\n\t\t\t\tExpect([]string{\"from\", \"to\"}).To(ContainElement(link.Direction))\n\t\t\t\tExpect(link.Node).NotTo(BeEmpty())\n\t\t\t\tExpect(link.CreateTime).To(BeNumerically(\">\", 0))\n\n\t\t\t\tExpect(link.Events).NotTo(BeEmpty())\n\t\t\t\tvalidEventChars := []rune{'r', 'w'}\n\t\t\t\tfor _, eventChar := range link.Events {\n\t\t\t\t\tExpect(validEventChars).To(ContainElement(eventChar))\n\t\t\t\t}\n\n\t\t\t\tExpect(link.SendBufferAllocated).To(BeNumerically(\">=\", 0))\n\t\t\t\tExpect(link.SendBufferUsed).To(BeNumerically(\">=\", 0))\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should cluster client setname\", func() {\n\t\t\terr := client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\t\treturn c.Ping(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\t\tval, err := c.ClientList(ctx).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).Should(ContainSubstring(\"name=cluster_hi\"))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t})\n\n\t\tIt(\"should CLUSTER PROTO 3\", func() {\n\t\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\t\tval, err := c.Do(ctx, \"HELLO\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).Should(HaveKeyWithValue(\"proto\", int64(3)))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t})\n\n\t\tIt(\"should CLUSTER MYSHARDID\", func() {\n\t\t\tshardID, err := client.ClusterMyShardID(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(shardID).ToNot(BeEmpty())\n\t\t})\n\n\t\tIt(\"should CLUSTER NODES\", func() {\n\t\t\tres, err := client.ClusterNodes(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(res)).To(BeNumerically(\">\", 400))\n\t\t})\n\n\t\tIt(\"should CLUSTER INFO\", func() {\n\t\t\tres, err := client.ClusterInfo(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(ContainSubstring(\"cluster_known_nodes:6\"))\n\t\t})\n\n\t\tIt(\"should CLUSTER KEYSLOT\", func() {\n\t\t\thashSlot, err := client.ClusterKeySlot(ctx, \"somekey\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(hashSlot).To(Equal(int64(hashtag.Slot(\"somekey\"))))\n\t\t})\n\n\t\tIt(\"should CLUSTER GETKEYSINSLOT\", func() {\n\t\t\tkeys, err := client.ClusterGetKeysInSlot(ctx, hashtag.Slot(\"somekey\"), 1).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(keys)).To(Equal(0))\n\t\t})\n\n\t\tIt(\"should CLUSTER COUNT-FAILURE-REPORTS\", func() {\n\t\t\tn, err := client.ClusterCountFailureReports(ctx, cluster.nodeIDs[0]).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should CLUSTER COUNTKEYSINSLOT\", func() {\n\t\t\tn, err := client.ClusterCountKeysInSlot(ctx, 10).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(n).To(Equal(int64(0)))\n\t\t})\n\n\t\tIt(\"should CLUSTER SAVECONFIG\", func() {\n\t\t\tres, err := client.ClusterSaveConfig(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).To(Equal(\"OK\"))\n\t\t})\n\n\t\tIt(\"should CLUSTER SLAVES\", func() {\n\t\t\tnodesList, err := client.ClusterSlaves(ctx, cluster.nodeIDs[0]).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(nodesList).Should(ContainElement(ContainSubstring(\"slave\")))\n\t\t\tExpect(nodesList).Should(HaveLen(1))\n\t\t})\n\n\t\tIt(\"should RANDOMKEY\", func() {\n\t\t\tconst nkeys = 100\n\n\t\t\tfor i := 0; i < nkeys; i++ {\n\t\t\t\terr := client.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tvar keys []string\n\t\t\taddKey := func(key string) {\n\t\t\t\tfor _, k := range keys {\n\t\t\t\t\tif k == key {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tkeys = append(keys, key)\n\t\t\t}\n\n\t\t\tfor i := 0; i < nkeys*10; i++ {\n\t\t\t\tkey := client.RandomKey(ctx).Val()\n\t\t\t\taddKey(key)\n\t\t\t}\n\n\t\t\tExpect(len(keys)).To(BeNumerically(\"~\", nkeys, nkeys/10))\n\t\t})\n\n\t\tIt(\"should support Process hook\", func() {\n\t\t\ttestCtx, cancel := context.WithCancel(ctx)\n\t\t\tdefer cancel()\n\n\t\t\terr := client.Ping(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error {\n\t\t\t\treturn node.Ping(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvar mu sync.Mutex\n\t\t\tvar stack []string\n\n\t\t\tclusterHook := &hook{\n\t\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase <-testCtx.Done():\n\t\t\t\t\t\t\treturn hook(ctx, cmd)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tstack = append(stack, \"cluster.BeforeProcess\")\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\terr := hook(ctx, cmd)\n\n\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tstack = append(stack, \"cluster.AfterProcess\")\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t\tclient.AddHook(clusterHook)\n\n\t\t\tnodeHook := &hook{\n\t\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase <-testCtx.Done():\n\t\t\t\t\t\t\treturn hook(ctx, cmd)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tstack = append(stack, \"shard.BeforeProcess\")\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\terr := hook(ctx, cmd)\n\n\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tstack = append(stack, \"shard.AfterProcess\")\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error {\n\t\t\t\tnode.AddHook(nodeHook)\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\terr = client.Ping(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmu.Lock()\n\t\t\tfinalStack := make([]string, len(stack))\n\t\t\tcopy(finalStack, stack)\n\t\t\tmu.Unlock()\n\n\t\t\tExpect(finalStack).To(ContainElements([]string{\n\t\t\t\t\"cluster.BeforeProcess\",\n\t\t\t\t\"shard.BeforeProcess\",\n\t\t\t\t\"shard.AfterProcess\",\n\t\t\t\t\"cluster.AfterProcess\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should support Pipeline hook\", func() {\n\t\t\terr := client.Ping(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error {\n\t\t\t\treturn node.Ping(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvar mu sync.Mutex\n\t\t\tvar stack []string\n\n\t\t\tclient.AddHook(&hook{\n\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\t\t\tcmdStr := cmds[0].String()\n\n\t\t\t\t\t\t// Handle SET command (should succeed)\n\t\t\t\t\t\tif cmdStr == \"set pipeline_test_key pipeline_test_value: \" {\n\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\tstack = append(stack, \"cluster.BeforeProcessPipeline\")\n\t\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\t\t\t\tExpect(cmds[0].String()).To(Equal(\"set pipeline_test_key pipeline_test_value: OK\"))\n\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\tstack = append(stack, \"cluster.AfterProcessPipeline\")\n\t\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// For other commands (like ping), just pass through without expectations\n\t\t\t\t\t\t// since they might fail before reaching this point\n\t\t\t\t\t\treturn hook(ctx, cmds)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error {\n\t\t\t\tnode.AddHook(&hook{\n\t\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\t\t\t\tcmdStr := cmds[0].String()\n\n\t\t\t\t\t\t\t// Handle SET command (should succeed)\n\t\t\t\t\t\t\tif cmdStr == \"set pipeline_test_key pipeline_test_value: \" {\n\t\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\t\tstack = append(stack, \"shard.BeforeProcessPipeline\")\n\t\t\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\t\t\t\t\tExpect(cmds[0].String()).To(Equal(\"set pipeline_test_key pipeline_test_value: OK\"))\n\t\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\t\tstack = append(stack, \"shard.AfterProcessPipeline\")\n\t\t\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// For other commands (like ping), just pass through without expectations\n\t\t\t\t\t\t\treturn hook(ctx, cmds)\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\t_, err = client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Set(ctx, \"pipeline_test_key\", \"pipeline_test_value\", 0)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmu.Lock()\n\t\t\tfinalStack := make([]string, len(stack))\n\t\t\tcopy(finalStack, stack)\n\t\t\tmu.Unlock()\n\n\t\t\tExpect(finalStack).To(Equal([]string{\n\t\t\t\t\"cluster.BeforeProcessPipeline\",\n\t\t\t\t\"shard.BeforeProcessPipeline\",\n\t\t\t\t\"shard.AfterProcessPipeline\",\n\t\t\t\t\"cluster.AfterProcessPipeline\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should reject ping command in pipeline\", func() {\n\t\t\t// Test that ping command fails in pipeline as expected\n\t\t\t_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(ContainSubstring(\"redis: cannot pipeline command \\\"ping\\\" with request policy ReqAllNodes/ReqAllShards/ReqMultiShard\"))\n\t\t})\n\n\t\tIt(\"should support TxPipeline hook\", func() {\n\t\t\terr := client.Ping(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error {\n\t\t\t\treturn node.Ping(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvar mu sync.Mutex\n\t\t\tvar stack []string\n\n\t\t\tclient.AddHook(&hook{\n\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\tExpect(cmds).To(HaveLen(3))\n\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tstack = append(stack, \"cluster.BeforeProcessPipeline\")\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\tExpect(cmds).To(HaveLen(3))\n\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tstack = append(stack, \"cluster.AfterProcessPipeline\")\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error {\n\t\t\t\tnode.AddHook(&hook{\n\t\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\t\tExpect(cmds).To(HaveLen(3))\n\t\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\tstack = append(stack, \"shard.BeforeProcessPipeline\")\n\t\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\t\tExpect(cmds).To(HaveLen(3))\n\t\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\tstack = append(stack, \"shard.AfterProcessPipeline\")\n\t\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\t_, err = client.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tmu.Lock()\n\t\t\tfinalStack := make([]string, len(stack))\n\t\t\tcopy(finalStack, stack)\n\t\t\tmu.Unlock()\n\n\t\t\tExpect(finalStack).To(Equal([]string{\n\t\t\t\t\"cluster.BeforeProcessPipeline\",\n\t\t\t\t\"shard.BeforeProcessPipeline\",\n\t\t\t\t\"shard.AfterProcessPipeline\",\n\t\t\t\t\"cluster.AfterProcessPipeline\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"should return correct replica for key\", func() {\n\t\t\tclient, err := client.SlaveForKey(ctx, \"test\")\n\t\t\tExpect(err).ToNot(HaveOccurred())\n\t\t\tinfo := client.Info(ctx, \"server\")\n\t\t\tExpect(info.Val()).Should(ContainSubstring(\"tcp_port:16604\"))\n\t\t})\n\n\t\tIt(\"should return correct master for key\", func() {\n\t\t\tclient, err := client.MasterForKey(ctx, \"test\")\n\t\t\tExpect(err).ToNot(HaveOccurred())\n\t\t\tinfo := client.Info(ctx, \"server\")\n\t\t\tExpect(info.Val()).Should(ContainSubstring(\"tcp_port:16601\"))\n\t\t})\n\n\t\tassertClusterClient()\n\t})\n\n\tDescribe(\"ClusterClient with RouteByLatency\", func() {\n\t\tBeforeEach(func() {\n\t\t\topt = redisClusterOptions()\n\t\t\topt.RouteByLatency = true\n\t\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ForEachSlave(ctx, func(ctx context.Context, slave *redis.Client) error {\n\t\t\t\tEventually(func() int64 {\n\t\t\t\t\treturn client.DBSize(ctx).Val()\n\t\t\t\t}, 30*time.Second).Should(Equal(int64(0)))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\terr := client.ForEachSlave(ctx, func(ctx context.Context, slave *redis.Client) error {\n\t\t\t\treturn slave.ReadWrite(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.Close()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tassertClusterClient()\n\t})\n\n\tDescribe(\"ClusterClient with ClusterSlots\", func() {\n\t\tBeforeEach(func() {\n\t\t\tfailover = true\n\n\t\t\topt = redisClusterOptions()\n\t\t\topt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) {\n\t\t\t\tslots := []redis.ClusterSlot{{\n\t\t\t\t\tStart: 0,\n\t\t\t\t\tEnd:   5460,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":\" + ringShard1Port,\n\t\t\t\t\t}},\n\t\t\t\t}, {\n\t\t\t\t\tStart: 5461,\n\t\t\t\t\tEnd:   10922,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":\" + ringShard2Port,\n\t\t\t\t\t}},\n\t\t\t\t}, {\n\t\t\t\t\tStart: 10923,\n\t\t\t\t\tEnd:   16383,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":\" + ringShard3Port,\n\t\t\t\t\t}},\n\t\t\t\t}}\n\t\t\t\treturn slots, nil\n\t\t\t}\n\t\t\tclient = cluster.newClusterClient(ctx, opt)\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ForEachSlave(ctx, func(ctx context.Context, slave *redis.Client) error {\n\t\t\t\tEventually(func() int64 {\n\t\t\t\t\treturn client.DBSize(ctx).Val()\n\t\t\t\t}, 30*time.Second).Should(Equal(int64(0)))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\tfailover = false\n\n\t\t\terr := client.Close()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tassertClusterClient()\n\t})\n\n\tDescribe(\"ClusterClient with RouteRandomly and ClusterSlots\", func() {\n\t\tBeforeEach(func() {\n\t\t\tfailover = true\n\n\t\t\topt = redisClusterOptions()\n\t\t\topt.RouteRandomly = true\n\t\t\topt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) {\n\t\t\t\tslots := []redis.ClusterSlot{{\n\t\t\t\t\tStart: 0,\n\t\t\t\t\tEnd:   5460,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":\" + ringShard1Port,\n\t\t\t\t\t}},\n\t\t\t\t}, {\n\t\t\t\t\tStart: 5461,\n\t\t\t\t\tEnd:   10922,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":\" + ringShard2Port,\n\t\t\t\t\t}},\n\t\t\t\t}, {\n\t\t\t\t\tStart: 10923,\n\t\t\t\t\tEnd:   16383,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":\" + ringShard3Port,\n\t\t\t\t\t}},\n\t\t\t\t}}\n\t\t\t\treturn slots, nil\n\t\t\t}\n\t\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ForEachSlave(ctx, func(ctx context.Context, slave *redis.Client) error {\n\t\t\t\tEventually(func() int64 {\n\t\t\t\t\treturn client.DBSize(ctx).Val()\n\t\t\t\t}, 30*time.Second).Should(Equal(int64(0)))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\tfailover = false\n\n\t\t\terr := client.Close()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tassertClusterClient()\n\t})\n\n\tDescribe(\"ClusterClient with ClusterSlots with multiple nodes per slot\", func() {\n\t\tBeforeEach(func() {\n\t\t\tfailover = true\n\n\t\t\topt = redisClusterOptions()\n\t\t\topt.ReadOnly = true\n\t\t\topt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) {\n\t\t\t\tslots := []redis.ClusterSlot{{\n\t\t\t\t\tStart: 0,\n\t\t\t\t\tEnd:   5460,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":16600\",\n\t\t\t\t\t}, {\n\t\t\t\t\t\tAddr: \":16603\",\n\t\t\t\t\t}},\n\t\t\t\t}, {\n\t\t\t\t\tStart: 5461,\n\t\t\t\t\tEnd:   10922,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":16601\",\n\t\t\t\t\t}, {\n\t\t\t\t\t\tAddr: \":16604\",\n\t\t\t\t\t}},\n\t\t\t\t}, {\n\t\t\t\t\tStart: 10923,\n\t\t\t\t\tEnd:   16383,\n\t\t\t\t\tNodes: []redis.ClusterNode{{\n\t\t\t\t\t\tAddr: \":16602\",\n\t\t\t\t\t}, {\n\t\t\t\t\t\tAddr: \":16605\",\n\t\t\t\t\t}},\n\t\t\t\t}}\n\t\t\t\treturn slots, nil\n\t\t\t}\n\t\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.FlushDB(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = client.ForEachSlave(ctx, func(ctx context.Context, slave *redis.Client) error {\n\t\t\t\tEventually(func() int64 {\n\t\t\t\t\treturn client.DBSize(ctx).Val()\n\t\t\t\t}, 30*time.Second).Should(Equal(int64(0)))\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\tfailover = false\n\n\t\t\terr := client.Close()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tassertClusterClient()\n\t})\n})\n\nvar _ = Describe(\"ClusterClient without nodes\", func() {\n\tvar client *redis.ClusterClient\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClusterClient(&redis.ClusterOptions{})\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should return an error for Ping\", func() {\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).To(MatchError(\"redis: cluster has no nodes\"))\n\t})\n\n\tIt(\"should return an error for pipeline\", func() {\n\t\t_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\tpipe.Ping(ctx)\n\t\t\treturn nil\n\t\t})\n\t\tExpect(err).To(MatchError(\"redis: cluster has no nodes\"))\n\t})\n})\n\nvar _ = Describe(\"ClusterClient without valid nodes\", func() {\n\tvar client *redis.ClusterClient\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\tAddrs: []string{redisAddr},\n\t\t})\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should return an error when cluster support is disabled\", func() {\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).To(MatchError(\"ERR This instance has cluster support disabled\"))\n\t})\n\n\tIt(\"should return an error for pipeline when cluster support is disabled\", func() {\n\t\t_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\tpipe.Ping(ctx)\n\t\t\treturn nil\n\t\t})\n\t\tExpect(err).To(MatchError(\"ERR This instance has cluster support disabled\"))\n\t})\n})\n\nvar _ = Describe(\"ClusterClient with unavailable Cluster\", func() {\n\tvar client *redis.ClusterClient\n\n\tBeforeEach(func() {\n\t\topt := redisClusterOptions()\n\t\topt.ReadTimeout = 250 * time.Millisecond\n\t\topt.WriteTimeout = 250 * time.Millisecond\n\t\topt.MaxRedirects = 1\n\t\tclient = cluster.newClusterClientUnstable(opt)\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\tfor _, node := range cluster.clients {\n\t\t\terr := node.ClientPause(ctx, 5*time.Second).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should recover when Cluster recovers\", func() {\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).To(HaveOccurred())\n\n\t\tEventually(func() error {\n\t\t\treturn client.Ping(ctx).Err()\n\t\t}, \"30s\").ShouldNot(HaveOccurred())\n\t})\n})\n\nvar _ = Describe(\"ClusterClient timeout\", func() {\n\tvar client *redis.ClusterClient\n\n\tAfterEach(func() {\n\t\t_ = client.Close()\n\t})\n\n\ttestTimeout := func() {\n\t\tIt(\"should timeout Ping\", func() {\n\t\t\terr := client.Ping(ctx).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"should timeout Pipeline\", func() {\n\t\t\t_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"should timeout Tx\", func() {\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\treturn tx.Ping(ctx).Err()\n\t\t\t}, \"foo\")\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"should timeout Tx Pipeline\", func() {\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Ping(ctx)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn err\n\t\t\t}, \"foo\")\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\t}\n\n\tconst pause = 5 * time.Second\n\n\tContext(\"read/write timeout\", func() {\n\t\tBeforeEach(func() {\n\t\t\topt := redisClusterOptions()\n\t\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t\terr := client.ForEachShard(ctx, func(ctx context.Context, client *redis.Client) error {\n\t\t\t\terr := client.ClientPause(ctx, pause).Err()\n\n\t\t\t\topt := client.Options()\n\t\t\t\topt.ReadTimeout = time.Nanosecond\n\t\t\t\topt.WriteTimeout = time.Nanosecond\n\n\t\t\t\treturn err\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Overwrite timeouts after the client is initialized.\n\t\t\topt.ReadTimeout = time.Nanosecond\n\t\t\topt.WriteTimeout = time.Nanosecond\n\t\t\topt.MaxRedirects = 0\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, client *redis.Client) error {\n\t\t\t\tdefer GinkgoRecover()\n\n\t\t\t\topt := client.Options()\n\t\t\t\topt.ReadTimeout = time.Second\n\t\t\t\topt.WriteTimeout = time.Second\n\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t\t}, 2*pause).ShouldNot(HaveOccurred())\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\terr := client.Close()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\ttestTimeout()\n\t})\n})\n\nvar _ = Describe(\"Command Tips tests\", func() {\n\tvar client *redis.ClusterClient\n\n\tBeforeEach(func() {\n\t\topt := redisClusterOptions()\n\t\tclient = cluster.newClusterClient(ctx, opt)\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should verify COMMAND tips match router policy types\", func() {\n\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\t\texpectedPolicies := map[string]struct {\n\t\t\tRequestPolicy  string\n\t\t\tResponsePolicy string\n\t\t}{\n\t\t\t\"touch\": {\n\t\t\t\tRequestPolicy:  \"multi_shard\",\n\t\t\t\tResponsePolicy: \"agg_sum\",\n\t\t\t},\n\t\t\t\"flushall\": {\n\t\t\t\tRequestPolicy:  \"all_shards\",\n\t\t\t\tResponsePolicy: \"all_succeeded\",\n\t\t\t},\n\t\t}\n\n\t\tcmds, err := client.Command(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tfor cmdName, expected := range expectedPolicies {\n\t\t\tactualCmd := cmds[cmdName]\n\n\t\t\tExpect(actualCmd.CommandPolicy).NotTo(BeNil())\n\n\t\t\t// Verify request_policy from COMMAND matches router policy\n\t\t\tactualRequestPolicy := actualCmd.CommandPolicy.Request.String()\n\t\t\tExpect(actualRequestPolicy).To(Equal(expected.RequestPolicy))\n\n\t\t\t// Verify response_policy from COMMAND matches router policy\n\t\t\tactualResponsePolicy := actualCmd.CommandPolicy.Response.String()\n\t\t\tExpect(actualResponsePolicy).To(Equal(expected.ResponsePolicy))\n\t\t}\n\t})\n\n\tDescribe(\"Explicit Routing Policy Tests\", func() {\n\t\tIt(\"should test explicit routing policy for TOUCH\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t// Verify TOUCH command has multi_shard policy\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\ttouchCmd := cmds[\"touch\"]\n\n\t\t\tExpect(touchCmd.CommandPolicy).NotTo(BeNil())\n\t\t\tExpect(touchCmd.CommandPolicy.Request.String()).To(Equal(\"multi_shard\"))\n\t\t\tExpect(touchCmd.CommandPolicy.Response.String()).To(Equal(\"agg_sum\"))\n\n\t\t\tkeys := []string{\"key1\", \"key2\", \"key3\", \"key4\", \"key5\"}\n\t\t\tfor _, key := range keys {\n\t\t\t\terr := client.Set(ctx, key, \"value\", 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tresult := client.Touch(ctx, keys...)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(int64(len(keys))))\n\t\t})\n\n\t\tIt(\"should test explicit routing policy for FLUSHALL\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t// Verify FLUSHALL command has all_shards policy\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tflushallCmd := cmds[\"flushall\"]\n\n\t\t\tExpect(flushallCmd.CommandPolicy).NotTo(BeNil())\n\t\t\tExpect(flushallCmd.CommandPolicy.Request.String()).To(Equal(\"all_shards\"))\n\t\t\tExpect(flushallCmd.CommandPolicy.Response.String()).To(Equal(\"all_succeeded\"))\n\n\t\t\ttestKeys := []string{\"test1\", \"test2\", \"test3\"}\n\t\t\tfor _, key := range testKeys {\n\t\t\t\terr := client.Set(ctx, key, \"value\", 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\terr = client.FlushAll(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tfor _, key := range testKeys {\n\t\t\t\texists := client.Exists(ctx, key)\n\t\t\t\tExpect(exists.Val()).To(Equal(int64(0)))\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should test explicit routing policy for PING\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t// Verify PING command has all_shards policy\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tpingCmd := cmds[\"ping\"]\n\t\t\tExpect(pingCmd.CommandPolicy).NotTo(BeNil())\n\t\t\tExpect(pingCmd.CommandPolicy.Request.String()).To(Equal(\"all_shards\"))\n\t\t\tExpect(pingCmd.CommandPolicy.Response.String()).To(Equal(\"all_succeeded\"))\n\n\t\t\tresult := client.Ping(ctx)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should test explicit routing policy for DBSIZE\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t// Verify DBSIZE command has all_shards policy with agg_sum response\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tdbsizeCmd := cmds[\"dbsize\"]\n\t\t\tExpect(dbsizeCmd.CommandPolicy).NotTo(BeNil())\n\t\t\tExpect(dbsizeCmd.CommandPolicy.Request.String()).To(Equal(\"all_shards\"))\n\t\t\tExpect(dbsizeCmd.CommandPolicy.Response.String()).To(Equal(\"agg_sum\"))\n\n\t\t\ttestKeys := []string{\"dbsize_test1\", \"dbsize_test2\", \"dbsize_test3\"}\n\t\t\tfor _, key := range testKeys {\n\t\t\t\terr := client.Set(ctx, key, \"value\", 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tsize := client.DBSize(ctx)\n\t\t\tExpect(size.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(size.Val()).To(BeNumerically(\">=\", int64(len(testKeys))))\n\t\t})\n\t})\n\n\tDescribe(\"DDL Commands Routing Policy Tests\", func() {\n\t\tBeforeEach(func() {\n\t\t\tinfo := client.Info(ctx, \"modules\")\n\t\t\tif info.Err() != nil || !strings.Contains(info.Val(), \"search\") {\n\t\t\t\tSkip(\"Search module not available\")\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should test DDL commands routing policy for FT.CREATE\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t// Verify FT.CREATE command routing policy\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tftCreateCmd, exists := cmds[\"ft.create\"]\n\t\t\tif !exists || ftCreateCmd.CommandPolicy == nil {\n\t\t\t\tSkip(\"FT.CREATE command or tips not available\")\n\t\t\t}\n\n\t\t\t// DDL commands should NOT be broadcasted - they should go to coordinator only\n\t\t\tExpect(ftCreateCmd.CommandPolicy).NotTo(BeNil())\n\t\t\trequestPolicy := ftCreateCmd.CommandPolicy.Request.String()\n\t\t\tExpect(requestPolicy).NotTo(Equal(\"all_shards\"))\n\t\t\tExpect(requestPolicy).NotTo(Equal(\"all_nodes\"))\n\n\t\t\tindexName := \"test_index_create\"\n\t\t\tclient.FTDropIndex(ctx, indexName)\n\n\t\t\tresult := client.FTCreate(ctx, indexName,\n\t\t\t\t&redis.FTCreateOptions{\n\t\t\t\t\tOnHash: true,\n\t\t\t\t\tPrefix: []interface{}{\"doc:\"},\n\t\t\t\t},\n\t\t\t\t&redis.FieldSchema{\n\t\t\t\t\tFieldName: \"title\",\n\t\t\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t\t\t})\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\tinfoResult := client.FTInfo(ctx, indexName)\n\t\t\tExpect(infoResult.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(infoResult.Val().IndexName).To(Equal(indexName))\n\t\t\tclient.FTDropIndex(ctx, indexName)\n\t\t})\n\n\t\tIt(\"should test DDL commands routing policy for FT.ALTER\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t// Verify FT.ALTER command routing policy\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tftAlterCmd, exists := cmds[\"ft.alter\"]\n\t\t\tif !exists || ftAlterCmd.CommandPolicy == nil {\n\t\t\t\tSkip(\"FT.ALTER command or tips not available\")\n\t\t\t}\n\n\t\t\tExpect(ftAlterCmd.CommandPolicy).NotTo(BeNil())\n\t\t\trequestPolicy := ftAlterCmd.CommandPolicy.Request.String()\n\t\t\tExpect(requestPolicy).NotTo(Equal(\"all_shards\"))\n\t\t\tExpect(requestPolicy).NotTo(Equal(\"all_nodes\"))\n\n\t\t\tindexName := \"test_index_alter\"\n\t\t\tclient.FTDropIndex(ctx, indexName)\n\n\t\t\tresult := client.FTCreate(ctx, indexName,\n\t\t\t\t&redis.FTCreateOptions{\n\t\t\t\t\tOnHash: true,\n\t\t\t\t\tPrefix: []interface{}{\"doc:\"},\n\t\t\t\t},\n\t\t\t\t&redis.FieldSchema{\n\t\t\t\t\tFieldName: \"title\",\n\t\t\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t\t\t})\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\n\t\t\talterResult := client.FTAlter(ctx, indexName, false,\n\t\t\t\t[]interface{}{\"description\", redis.SearchFieldTypeText.String()})\n\t\t\tExpect(alterResult.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(alterResult.Val()).To(Equal(\"OK\"))\n\t\t\tclient.FTDropIndex(ctx, indexName)\n\t\t})\n\n\t\tIt(\"should route keyed commands to correct shard based on hash slot\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\ttype masterNode struct {\n\t\t\t\tclient *redis.Client\n\t\t\t\taddr   string\n\t\t\t}\n\t\t\tvar masterNodes []masterNode\n\t\t\tvar mu sync.Mutex\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\taddr := master.Options().Addr\n\t\t\t\tmu.Lock()\n\t\t\t\tmasterNodes = append(masterNodes, masterNode{\n\t\t\t\t\tclient: master,\n\t\t\t\t\taddr:   addr,\n\t\t\t\t})\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(masterNodes)).To(BeNumerically(\">\", 1))\n\n\t\t\t// Single keyed command should go to exactly one shard - determined by hash slot\n\t\t\ttestKey := \"test_key_12345\"\n\t\t\ttestValue := \"test_value\"\n\n\t\t\tresult := client.Set(ctx, testKey, testValue, 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\n\t\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t\tvar targetNodeAddr string\n\t\t\tfoundNodes := 0\n\n\t\t\tfor _, node := range masterNodes {\n\t\t\t\tgetResult := node.client.Get(ctx, testKey)\n\t\t\t\tif getResult.Err() == nil && getResult.Val() == testValue {\n\t\t\t\t\tfoundNodes++\n\t\t\t\t\ttargetNodeAddr = node.addr\n\t\t\t\t} else {\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tExpect(foundNodes).To(Equal(1))\n\t\t\tExpect(targetNodeAddr).NotTo(BeEmpty())\n\n\t\t\t// Multiple commands with same key should go to same shard\n\t\t\tfinalValue := \"\"\n\t\t\tfor i := 0; i < 5; i++ {\n\t\t\t\tfinalValue = fmt.Sprintf(\"value_%d\", i)\n\t\t\t\tresult := client.Set(ctx, testKey, finalValue, 0)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Val()).To(Equal(\"OK\"))\n\t\t\t}\n\n\t\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t\tvar currentTargetNode string\n\t\t\tfoundNodesAfterUpdate := 0\n\n\t\t\tfor _, node := range masterNodes {\n\t\t\t\tgetResult := node.client.Get(ctx, testKey)\n\t\t\t\tif getResult.Err() == nil && getResult.Val() == finalValue {\n\t\t\t\t\tfoundNodesAfterUpdate++\n\t\t\t\t\tcurrentTargetNode = node.addr\n\t\t\t\t} else {\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// All commands with same key should go to same shard\n\t\t\tExpect(foundNodesAfterUpdate).To(Equal(1))\n\t\t\tExpect(currentTargetNode).To(Equal(targetNodeAddr))\n\t\t})\n\n\t\tIt(\"should aggregate responses according to explicit aggregation policies\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\ttype masterNode struct {\n\t\t\t\tclient *redis.Client\n\t\t\t\taddr   string\n\t\t\t}\n\t\t\tvar masterNodes []masterNode\n\t\t\tvar mu sync.Mutex\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\taddr := master.Options().Addr\n\t\t\t\tmu.Lock()\n\t\t\t\tmasterNodes = append(masterNodes, masterNode{\n\t\t\t\t\tclient: master,\n\t\t\t\t\taddr:   addr,\n\t\t\t\t})\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(masterNodes)).To(BeNumerically(\">\", 1))\n\n\t\t\t// verify TOUCH command has agg_sum policy\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\ttouchCmd, exists := cmds[\"touch\"]\n\t\t\tif !exists || touchCmd.CommandPolicy == nil {\n\t\t\t\tSkip(\"TOUCH command or tips not available\")\n\t\t\t}\n\n\t\t\tExpect(touchCmd.CommandPolicy.Response.String()).To(Equal(\"agg_sum\"))\n\n\t\t\ttestKeys := []string{\n\t\t\t\t\"touch_test_key_1111\", // These keys should map to different hash slots\n\t\t\t\t\"touch_test_key_2222\",\n\t\t\t\t\"touch_test_key_3333\",\n\t\t\t\t\"touch_test_key_4444\",\n\t\t\t\t\"touch_test_key_5555\",\n\t\t\t}\n\n\t\t\t// Set keys on different shards\n\t\t\tkeysPerShard := make(map[string][]string)\n\t\t\tfor _, key := range testKeys {\n\t\t\t\tresult := client.Set(ctx, key, \"test_value\", 0)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\n\t\t\t\t// Find which shard contains this key\n\t\t\t\tfor _, node := range masterNodes {\n\t\t\t\t\tgetResult := node.client.Get(ctx, key)\n\t\t\t\t\tif getResult.Err() == nil {\n\t\t\t\t\t\tkeysPerShard[node.addr] = append(keysPerShard[node.addr], key)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify keys are distributed across multiple shards\n\t\t\tshardsWithKeys := len(keysPerShard)\n\t\t\tExpect(shardsWithKeys).To(BeNumerically(\">\", 1))\n\n\t\t\t// Execute TOUCH command on all keys - this should aggregate results using agg_sum\n\t\t\ttouchResult := client.Touch(ctx, testKeys...)\n\t\t\tExpect(touchResult.Err()).NotTo(HaveOccurred())\n\n\t\t\ttotalTouched := touchResult.Val()\n\t\t\tExpect(totalTouched).To(Equal(int64(len(testKeys))))\n\n\t\t\ttotalKeysOnShards := 0\n\t\t\tfor _, keys := range keysPerShard {\n\t\t\t\ttotalKeysOnShards += len(keys)\n\t\t\t}\n\n\t\t\tExpect(totalKeysOnShards).To(Equal(len(testKeys)))\n\n\t\t\t// FLUSHALL command with all_succeeded aggregation policy\n\t\t\tflushallCmd, exists := cmds[\"flushall\"]\n\t\t\tif !exists || flushallCmd.CommandPolicy == nil {\n\t\t\t\tSkip(\"FLUSHALL command or tips not available\")\n\t\t\t}\n\n\t\t\tExpect(flushallCmd.CommandPolicy.Response.String()).To(Equal(\"all_succeeded\"))\n\n\t\t\tfor i := 0; i < len(masterNodes); i++ {\n\t\t\t\ttestKey := fmt.Sprintf(\"flush_test_key_%d_%d\", i, time.Now().UnixNano())\n\t\t\t\tresult := client.Set(ctx, testKey, \"test_data\", 0)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tflushResult := client.FlushAll(ctx)\n\t\t\tExpect(flushResult.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(flushResult.Val()).To(Equal(\"OK\"))\n\n\t\t\tfor _, node := range masterNodes {\n\t\t\t\tdbSizeResult := node.client.DBSize(ctx)\n\t\t\t\tExpect(dbSizeResult.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(dbSizeResult.Val()).To(Equal(int64(0)))\n\t\t\t}\n\n\t\t\t// WAIT command aggregation policy - verify agg_min policy\n\t\t\twaitCmd, exists := cmds[\"wait\"]\n\t\t\tif !exists || waitCmd.CommandPolicy == nil {\n\t\t\t\tSkip(\"WAIT command or tips not available\")\n\t\t\t}\n\n\t\t\tExpect(waitCmd.CommandPolicy.Response.String()).To(Equal(\"agg_min\"))\n\n\t\t\t// Set up some data to replicate\n\t\t\ttestKey := \"wait_test_key_1111\"\n\t\t\tresult := client.Set(ctx, testKey, \"test_value\", 0)\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\n\t\t\t// Execute WAIT command - should aggregate using agg_min across all shards\n\t\t\t// WAIT waits for a given number of replicas to acknowledge writes\n\t\t\t// With agg_min policy, it returns the minimum number of replicas that acknowledged\n\t\t\twaitResult := client.Wait(ctx, 0, 1000) // Wait for 0 replicas with 1 second timeout\n\t\t\tExpect(waitResult.Err()).NotTo(HaveOccurred())\n\n\t\t\t// The result should be the minimum number of replicas across all shards\n\t\t\t// Since we're asking for 0 replicas, all shards should return 0, so min is 0\n\t\t\tminReplicas := waitResult.Val()\n\t\t\tExpect(minReplicas).To(BeNumerically(\">=\", 0))\n\n\t\t\t// SCRIPT EXISTS command aggregation policy - verify agg_logical_and policy\n\t\t\tscriptExistsCmd, exists := cmds[\"script exists\"]\n\t\t\tif !exists || scriptExistsCmd.CommandPolicy == nil {\n\t\t\t\tSkip(\"SCRIPT EXISTS command or tips not available\")\n\t\t\t}\n\n\t\t\tExpect(scriptExistsCmd.CommandPolicy.Response.String()).To(Equal(\"agg_logical_and\"))\n\n\t\t\t// Load a script on all shards\n\t\t\ttestScript := \"return 'hello'\"\n\t\t\tscriptLoadResult := client.ScriptLoad(ctx, testScript)\n\t\t\tExpect(scriptLoadResult.Err()).NotTo(HaveOccurred())\n\t\t\tscriptSHA := scriptLoadResult.Val()\n\n\t\t\t// Verify script exists on all shards using SCRIPT EXISTS\n\t\t\t// With agg_logical_and policy, it should return true only if script exists on ALL shards\n\t\t\tscriptExistsResult := client.ScriptExists(ctx, scriptSHA)\n\t\t\tExpect(scriptExistsResult.Err()).NotTo(HaveOccurred())\n\n\t\t\texistsResults := scriptExistsResult.Val()\n\t\t\tExpect(len(existsResults)).To(Equal(1))\n\t\t\tExpect(existsResults[0]).To(BeTrue()) // Script should exist on all shards\n\n\t\t\t// Test with a non-existent script SHA\n\t\t\tnonExistentSHA := \"0000000000000000000000000000000000000000\"\n\t\t\tscriptExistsResult2 := client.ScriptExists(ctx, nonExistentSHA)\n\t\t\tExpect(scriptExistsResult2.Err()).NotTo(HaveOccurred())\n\n\t\t\texistsResults2 := scriptExistsResult2.Val()\n\t\t\tExpect(len(existsResults2)).To(Equal(1))\n\t\t\tExpect(existsResults2[0]).To(BeFalse()) // Script should not exist on any shard\n\n\t\t\t// Test with mixed scenario - flush scripts from one shard manually\n\t\t\t// This is harder to test in practice since SCRIPT FLUSH affects all shards\n\t\t\t// So we'll just verify the basic functionality works\n\t\t})\n\n\t\tIt(\"should verify command aggregation policies\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tcommandPolicies := map[string]string{\n\t\t\t\t\"touch\":         \"agg_sum\",\n\t\t\t\t\"flushall\":      \"all_succeeded\",\n\t\t\t\t\"pfcount\":       \"default(hashslot)\",\n\t\t\t\t\"exists\":        \"agg_sum\",\n\t\t\t\t\"script exists\": \"agg_logical_and\",\n\t\t\t\t\"wait\":          \"agg_min\",\n\t\t\t}\n\n\t\t\tfor cmdName, expectedPolicy := range commandPolicies {\n\t\t\t\tcmd, exists := cmds[cmdName]\n\t\t\t\tif !exists {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif cmd.CommandPolicy == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tactualPolicy := cmd.CommandPolicy.Response.String()\n\t\t\t\tExpect(actualPolicy).To(Equal(expectedPolicy))\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should properly aggregate responses from keyless commands executed on multiple shards\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\ttype masterNode struct {\n\t\t\t\tclient *redis.Client\n\t\t\t\taddr   string\n\t\t\t}\n\t\t\tvar masterNodes []masterNode\n\t\t\tvar mu sync.Mutex\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\taddr := master.Options().Addr\n\t\t\t\tmu.Lock()\n\t\t\t\tmasterNodes = append(masterNodes, masterNode{\n\t\t\t\t\tclient: master,\n\t\t\t\t\taddr:   addr,\n\t\t\t\t})\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(masterNodes)).To(BeNumerically(\">\", 1))\n\n\t\t\t// PING command with all_shards policy - should aggregate responses\n\t\t\tcmds, err := client.Command(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tpingCmd, exists := cmds[\"ping\"]\n\t\t\tif exists && pingCmd.CommandPolicy != nil {\n\t\t\t}\n\n\t\t\tpingResult := client.Ping(ctx)\n\t\t\tExpect(pingResult.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(pingResult.Val()).To(Equal(\"PONG\"))\n\n\t\t\t// Verify PING was executed on all shards by checking individual nodes\n\t\t\tfor _, node := range masterNodes {\n\t\t\t\tnodePingResult := node.client.Ping(ctx)\n\t\t\t\tExpect(nodePingResult.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(nodePingResult.Val()).To(Equal(\"PONG\"))\n\t\t\t}\n\n\t\t\t// Test 2: DBSIZE command aggregation across shards - verify agg_sum policy\n\t\t\ttestKeys := []string{\n\t\t\t\t\"dbsize_test_key_1111\",\n\t\t\t\t\"dbsize_test_key_2222\",\n\t\t\t\t\"dbsize_test_key_3333\",\n\t\t\t\t\"dbsize_test_key_4444\",\n\t\t\t}\n\n\t\t\tfor _, key := range testKeys {\n\t\t\t\tresult := client.Set(ctx, key, \"test_value\", 0)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tdbSizeResult := client.DBSize(ctx)\n\t\t\tExpect(dbSizeResult.Err()).NotTo(HaveOccurred())\n\n\t\t\ttotalSize := dbSizeResult.Val()\n\t\t\tExpect(totalSize).To(BeNumerically(\">=\", int64(len(testKeys))))\n\n\t\t\t// Verify aggregation by manually getting sizes from each shard\n\t\t\ttotalManualSize := int64(0)\n\n\t\t\tfor _, node := range masterNodes {\n\t\t\t\tnodeDbSizeResult := node.client.DBSize(ctx)\n\t\t\t\tExpect(nodeDbSizeResult.Err()).NotTo(HaveOccurred())\n\n\t\t\t\tnodeSize := nodeDbSizeResult.Val()\n\t\t\t\ttotalManualSize += nodeSize\n\t\t\t}\n\n\t\t\t// Verify aggregation worked correctly\n\t\t\tExpect(totalSize).To(Equal(totalManualSize))\n\t\t})\n\n\t\tIt(\"should properly aggregate responses from keyed commands executed on multiple shards\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\t\t\ttype masterNode struct {\n\t\t\t\tclient *redis.Client\n\t\t\t\taddr   string\n\t\t\t}\n\t\t\tvar masterNodes []masterNode\n\t\t\tvar mu sync.Mutex\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\taddr := master.Options().Addr\n\t\t\t\tmu.Lock()\n\t\t\t\tmasterNodes = append(masterNodes, masterNode{\n\t\t\t\t\tclient: master,\n\t\t\t\t\taddr:   addr,\n\t\t\t\t})\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(masterNodes)).To(BeNumerically(\">\", 1))\n\n\t\t\t// MGET command with keys on different shards\n\t\t\ttestData := map[string]string{\n\t\t\t\t\"mget_test_key_1111\": \"value1\",\n\t\t\t\t\"mget_test_key_2222\": \"value2\",\n\t\t\t\t\"mget_test_key_3333\": \"value3\",\n\t\t\t\t\"mget_test_key_4444\": \"value4\",\n\t\t\t\t\"mget_test_key_5555\": \"value5\",\n\t\t\t}\n\n\t\t\tkeyLocations := make(map[string]string)\n\t\t\tfor key, value := range testData {\n\t\t\t\tresult := client.Set(ctx, key, value, 0)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\n\t\t\t\tfor _, node := range masterNodes {\n\t\t\t\t\tgetResult := node.client.Get(ctx, key)\n\t\t\t\t\tif getResult.Err() == nil && getResult.Val() == value {\n\t\t\t\t\t\tkeyLocations[key] = node.addr\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tshardsUsed := make(map[string]bool)\n\t\t\tfor _, shardAddr := range keyLocations {\n\t\t\t\tshardsUsed[shardAddr] = true\n\t\t\t}\n\t\t\tExpect(len(shardsUsed)).To(BeNumerically(\">\", 1))\n\n\t\t\tkeys := make([]string, 0, len(testData))\n\t\t\texpectedValues := make([]interface{}, 0, len(testData))\n\t\t\tfor key, value := range testData {\n\t\t\t\tkeys = append(keys, key)\n\t\t\t\texpectedValues = append(expectedValues, value)\n\t\t\t}\n\n\t\t\tmgetResult := client.MGet(ctx, keys...)\n\t\t\tExpect(mgetResult.Err()).NotTo(HaveOccurred())\n\n\t\t\tactualValues := mgetResult.Val()\n\t\t\tExpect(len(actualValues)).To(Equal(len(expectedValues)))\n\t\t\tfor i, value := range actualValues {\n\t\t\t\tif value != nil {\n\t\t\t\t\tExpect(value).To(Equal(expectedValues[i]))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(value).To(BeNil())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// EXISTS command aggregation across multiple keys\n\t\t\texistsTestData := map[string]string{\n\t\t\t\t\"exists_agg_key_1111\": \"value1\",\n\t\t\t\t\"exists_agg_key_2222\": \"value2\",\n\t\t\t\t\"exists_agg_key_3333\": \"value3\",\n\t\t\t}\n\n\t\t\texistsKeys := make([]string, 0, len(existsTestData))\n\t\t\tfor key, value := range existsTestData {\n\t\t\t\tresult := client.Set(ctx, key, value, 0)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t\texistsKeys = append(existsKeys, key)\n\t\t\t}\n\n\t\t\t// Add a non-existent key to the list\n\t\t\tnonExistentKey := \"non_existent_key_9999\"\n\t\t\texistsKeys = append(existsKeys, nonExistentKey)\n\n\t\t\texistsResult := client.Exists(ctx, existsKeys...)\n\t\t\tExpect(existsResult.Err()).NotTo(HaveOccurred())\n\n\t\t\texistsCount := existsResult.Val()\n\t\t\tExpect(existsCount).To(Equal(int64(len(existsTestData))))\n\t\t})\n\n\t\tIt(\"should propagate coordinator errors to client without modification\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\ttype masterNode struct {\n\t\t\t\tclient *redis.Client\n\t\t\t\taddr   string\n\t\t\t}\n\t\t\tvar masterNodes []masterNode\n\t\t\tvar mu sync.Mutex\n\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\taddr := master.Options().Addr\n\t\t\t\tmu.Lock()\n\t\t\t\tmasterNodes = append(masterNodes, masterNode{\n\t\t\t\t\tclient: master,\n\t\t\t\t\taddr:   addr,\n\t\t\t\t})\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(masterNodes)).To(BeNumerically(\">\", 0))\n\n\t\t\tinvalidSlotResult := client.ClusterAddSlotsRange(ctx, 99999, 100000)\n\t\t\tcoordinatorErr := invalidSlotResult.Err()\n\n\t\t\tif coordinatorErr != nil {\n\t\t\t\t// Verify the error is a Redis error\n\t\t\t\tvar redisErr redis.Error\n\t\t\t\tExpect(errors.As(coordinatorErr, &redisErr)).To(BeTrue())\n\n\t\t\t\t// Verify error message is preserved exactly as returned by coordinator\n\t\t\t\terrorMsg := coordinatorErr.Error()\n\t\t\t\tExpect(errorMsg).To(SatisfyAny(\n\t\t\t\t\tContainSubstring(\"slot\"),\n\t\t\t\t\tContainSubstring(\"ERR\"),\n\t\t\t\t\tContainSubstring(\"Invalid\"),\n\t\t\t\t))\n\n\t\t\t\t// Test that the same error occurs when calling coordinator directly\n\t\t\t\tcoordinatorNode := masterNodes[0]\n\t\t\t\tdirectResult := coordinatorNode.client.ClusterAddSlotsRange(ctx, 99999, 100000)\n\t\t\t\tdirectErr := directResult.Err()\n\n\t\t\t\tif directErr != nil {\n\t\t\t\t\tExpect(coordinatorErr.Error()).To(Equal(directErr.Error()))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Try cluster forget with invalid node ID\n\t\t\tinvalidNodeID := \"invalid_node_id_12345\"\n\t\t\tforgetResult := client.ClusterForget(ctx, invalidNodeID)\n\t\t\tforgetErr := forgetResult.Err()\n\n\t\t\tif forgetErr != nil {\n\t\t\t\tvar redisErr redis.Error\n\t\t\t\tExpect(errors.As(forgetErr, &redisErr)).To(BeTrue())\n\n\t\t\t\terrorMsg := forgetErr.Error()\n\t\t\t\tExpect(errorMsg).To(SatisfyAny(\n\t\t\t\t\tContainSubstring(\"Unknown node\"),\n\t\t\t\t\tContainSubstring(\"Invalid node\"),\n\t\t\t\t\tContainSubstring(\"ERR\"),\n\t\t\t\t))\n\n\t\t\t\tcoordinatorNode := masterNodes[0]\n\t\t\t\tdirectForgetResult := coordinatorNode.client.ClusterForget(ctx, invalidNodeID)\n\t\t\t\tdirectForgetErr := directForgetResult.Err()\n\n\t\t\t\tif directForgetErr != nil {\n\t\t\t\t\tExpect(forgetErr.Error()).To(Equal(directForgetErr.Error()))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Test error type preservation and format\n\t\t\tkeySlotResult := client.ClusterKeySlot(ctx, \"\")\n\t\t\tkeySlotErr := keySlotResult.Err()\n\n\t\t\tif keySlotErr != nil {\n\t\t\t\tvar redisErr redis.Error\n\t\t\t\tExpect(errors.As(keySlotErr, &redisErr)).To(BeTrue())\n\n\t\t\t\terrorMsg := keySlotErr.Error()\n\t\t\t\tExpect(len(errorMsg)).To(BeNumerically(\">\", 0))\n\t\t\t\tExpect(errorMsg).NotTo(ContainSubstring(\"wrapped\"))\n\t\t\t\tExpect(errorMsg).NotTo(ContainSubstring(\"context\"))\n\t\t\t}\n\n\t\t\t// Verify error propagation consistency\n\t\t\tclusterInfoResult := client.ClusterInfo(ctx)\n\t\t\tclusterInfoErr := clusterInfoResult.Err()\n\n\t\t\tif clusterInfoErr != nil {\n\t\t\t\tvar redisErr redis.Error\n\t\t\t\tExpect(errors.As(clusterInfoErr, &redisErr)).To(BeTrue())\n\n\t\t\t\tcoordinatorNode := masterNodes[0]\n\t\t\t\tdirectInfoResult := coordinatorNode.client.ClusterInfo(ctx)\n\t\t\t\tdirectInfoErr := directInfoResult.Err()\n\n\t\t\t\tif directInfoErr != nil {\n\t\t\t\t\tExpect(clusterInfoErr.Error()).To(Equal(directInfoErr.Error()))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify no error modification in router\n\t\t\tinvalidReplicateResult := client.ClusterReplicate(ctx, \"00000000000000000000000000000000invalid00\")\n\t\t\tinvalidReplicateErr := invalidReplicateResult.Err()\n\n\t\t\tif invalidReplicateErr != nil {\n\t\t\t\tvar redisErr redis.Error\n\t\t\t\tExpect(errors.As(invalidReplicateErr, &redisErr)).To(BeTrue())\n\n\t\t\t\terrorMsg := invalidReplicateErr.Error()\n\t\t\t\tExpect(errorMsg).NotTo(ContainSubstring(\"router\"))\n\t\t\t\tExpect(errorMsg).NotTo(ContainSubstring(\"cluster client\"))\n\t\t\t\tExpect(errorMsg).NotTo(ContainSubstring(\"failed to execute\"))\n\n\t\t\t\tExpect(errorMsg).To(SatisfyAny(\n\t\t\t\t\tHavePrefix(\"ERR\"),\n\t\t\t\t\tContainSubstring(\"Invalid\"),\n\t\t\t\t\tContainSubstring(\"Unknown\"),\n\t\t\t\t))\n\t\t\t}\n\t\t})\n\n\t\tDescribe(\"Routing Policies Comprehensive Test Suite\", func() {\n\t\t\tIt(\"should test MGET command with multi-slot routing and key order preservation\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Set up test data across multiple shards\n\t\t\t\ttestData := map[string]string{\n\t\t\t\t\t\"mget_test_key_1111\": \"value1\",\n\t\t\t\t\t\"mget_test_key_2222\": \"value2\",\n\t\t\t\t\t\"mget_test_key_3333\": \"value3\",\n\t\t\t\t\t\"mget_test_key_4444\": \"value4\",\n\t\t\t\t\t\"mget_test_key_5555\": \"value5\",\n\t\t\t\t}\n\n\t\t\t\t// Set all keys\n\t\t\t\tfor key, value := range testData {\n\t\t\t\t\tExpect(client.Set(ctx, key, value, 0).Err()).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\t// Verify keys are distributed across multiple shards\n\t\t\t\tslotMap := make(map[int]bool)\n\t\t\t\tfor key := range testData {\n\t\t\t\t\tslot := hashtag.Slot(key)\n\t\t\t\t\tslotMap[slot] = true\n\t\t\t\t}\n\t\t\t\tExpect(len(slotMap)).To(BeNumerically(\">\", 1))\n\n\t\t\t\t// Test MGET with specific key order\n\t\t\t\tkeys := []string{\n\t\t\t\t\t\"mget_test_key_3333\",\n\t\t\t\t\t\"mget_test_key_1111\",\n\t\t\t\t\t\"mget_test_key_5555\",\n\t\t\t\t\t\"mget_test_key_2222\",\n\t\t\t\t\t\"mget_test_key_4444\",\n\t\t\t\t}\n\n\t\t\t\tresult := client.MGet(ctx, keys...)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\n\t\t\t\t// Verify values are returned in the same order as keys\n\t\t\t\tvalues := result.Val()\n\t\t\t\tExpect(len(values)).To(Equal(len(keys)))\n\t\t\t\tfor i, key := range keys {\n\t\t\t\t\tExpect(values[i]).To(Equal(testData[key]))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should test MGET with non-existent keys across multiple shards\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Set up some keys\n\t\t\t\tExpect(client.Set(ctx, \"mget_exists_1111\", \"value1\", 0).Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.Set(ctx, \"mget_exists_3333\", \"value3\", 0).Err()).NotTo(HaveOccurred())\n\n\t\t\t\t// MGET with mix of existing and non-existing keys\n\t\t\t\tkeys := []string{\n\t\t\t\t\t\"mget_exists_1111\",\n\t\t\t\t\t\"mget_nonexist_2222\",\n\t\t\t\t\t\"mget_exists_3333\",\n\t\t\t\t\t\"mget_nonexist_4444\",\n\t\t\t\t}\n\n\t\t\t\tresult := client.MGet(ctx, keys...)\n\t\t\t\tExpect(result.Err()).ToNot(HaveOccurred())\n\n\t\t\t\t// MGET returns nil for non-existent keys (not errors)\n\t\t\t\t// Values should be in the same order as requested keys\n\t\t\t\tvalues := result.Val()\n\t\t\t\tExpect(len(values)).To(Equal(4))\n\t\t\t\tExpect(values[0]).To(Equal(\"value1\")) // existing key\n\t\t\t\tExpect(values[1]).To(BeNil())         // non-existent key returns nil\n\t\t\t\tExpect(values[2]).To(Equal(\"value3\")) // existing key\n\t\t\t\tExpect(values[3]).To(BeNil())         // non-existent key returns nil\n\t\t\t})\n\n\t\t\tIt(\"should test TOUCH command with multi-slot routing\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Set up keys across multiple shards\n\t\t\t\tkeys := []string{\n\t\t\t\t\t\"touch_test_key_1111\",\n\t\t\t\t\t\"touch_test_key_2222\",\n\t\t\t\t\t\"touch_test_key_3333\",\n\t\t\t\t\t\"touch_test_key_4444\",\n\t\t\t\t}\n\n\t\t\t\t// Set all keys\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tExpect(client.Set(ctx, key, \"value\", 0).Err()).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\t// Verify keys are on different shards\n\t\t\t\tslotMap := make(map[int]bool)\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tslot := hashtag.Slot(key)\n\t\t\t\t\tslotMap[slot] = true\n\t\t\t\t}\n\t\t\t\tExpect(len(slotMap)).To(BeNumerically(\">\", 1))\n\n\t\t\t\t// TOUCH should work across multiple shards\n\t\t\t\tresult := client.Touch(ctx, keys...)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Val()).To(Equal(int64(len(keys))))\n\t\t\t})\n\n\t\t\tIt(\"should test DEL command with multi-slot routing\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Set up keys across multiple shards\n\t\t\t\tkeys := []string{\n\t\t\t\t\t\"del_test_key_1111\",\n\t\t\t\t\t\"del_test_key_2222\",\n\t\t\t\t\t\"del_test_key_3333\",\n\t\t\t\t}\n\n\t\t\t\t// Set all keys\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tExpect(client.Set(ctx, key, \"value\", 0).Err()).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\t// Verify keys are on different shards\n\t\t\t\tslotMap := make(map[int]bool)\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tslot := hashtag.Slot(key)\n\t\t\t\t\tslotMap[slot] = true\n\t\t\t\t}\n\t\t\t\tExpect(len(slotMap)).To(BeNumerically(\">\", 1))\n\n\t\t\t\t// DEL should work across multiple shards\n\t\t\t\tresult := client.Del(ctx, keys...)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Val()).To(Equal(int64(len(keys))))\n\n\t\t\t\t// Verify all keys were deleted\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tval := client.Get(ctx, key)\n\t\t\t\t\tExpect(val.Err()).To(Equal(redis.Nil))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should test DBSIZE command with agg_sum aggregation across all shards\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Set keys across multiple shards\n\t\t\t\tkeys := []string{\n\t\t\t\t\t\"dbsize_test_1111\",\n\t\t\t\t\t\"dbsize_test_2222\",\n\t\t\t\t\t\"dbsize_test_3333\",\n\t\t\t\t\t\"dbsize_test_4444\",\n\t\t\t\t\t\"dbsize_test_5555\",\n\t\t\t\t}\n\n\t\t\t\t// Clean up first\n\t\t\t\tclient.Del(ctx, keys...)\n\n\t\t\t\t// Set all keys\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tExpect(client.Set(ctx, key, \"value\", 0).Err()).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\t// DBSIZE should aggregate results from all shards\n\t\t\t\tresult := client.DBSize(ctx)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Val()).To(BeNumerically(\">=\", int64(len(keys))))\n\t\t\t})\n\n\t\t\tIt(\"should test PING command with all_shards routing\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// PING should be sent to all shards and return one successful response\n\t\t\t\tresult := client.Ping(ctx)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Val()).To(Equal(\"PONG\"))\n\t\t\t})\n\n\t\t\tIt(\"should test MGET with single shard optimization\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Use hash tags to ensure all keys are on the same shard\n\t\t\t\tkeys := []string{\n\t\t\t\t\t\"{sameslot}key1\",\n\t\t\t\t\t\"{sameslot}key2\",\n\t\t\t\t\t\"{sameslot}key3\",\n\t\t\t\t}\n\n\t\t\t\t// Verify all keys hash to the same slot\n\t\t\t\tslot := hashtag.Slot(keys[0])\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tExpect(hashtag.Slot(key)).To(Equal(slot))\n\t\t\t\t}\n\n\t\t\t\t// Set all keys\n\t\t\t\tfor i, key := range keys {\n\t\t\t\t\tExpect(client.Set(ctx, key, fmt.Sprintf(\"value%d\", i+1), 0).Err()).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\t// MGET should work even with single shard\n\t\t\t\tresult := client.MGet(ctx, keys...)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\n\t\t\t\tvalues := result.Val()\n\t\t\t\tExpect(len(values)).To(Equal(3))\n\t\t\t\tExpect(values[0]).To(Equal(\"value1\"))\n\t\t\t\tExpect(values[1]).To(Equal(\"value2\"))\n\t\t\t\tExpect(values[2]).To(Equal(\"value3\"))\n\t\t\t})\n\n\t\t\tIt(\"should test empty MGET command returns error\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// MGET with no keys should return an error\n\t\t\t\tresult := client.MGet(ctx)\n\t\t\t\tExpect(result.Err()).To(HaveOccurred())\n\t\t\t\tExpect(result.Err().Error()).To(ContainSubstring(\"multi-shard command mget has no key arguments\"))\n\t\t\t})\n\n\t\t\tIt(\"should test MGET integration with MSET across multiple shards\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Create test data\n\t\t\t\ttestData := map[string]string{\n\t\t\t\t\t\"integration_key_1111\": \"alpha\",\n\t\t\t\t\t\"integration_key_2222\": \"beta\",\n\t\t\t\t\t\"integration_key_3333\": \"gamma\",\n\t\t\t\t\t\"integration_key_4444\": \"delta\",\n\t\t\t\t\t\"integration_key_5555\": \"epsilon\",\n\t\t\t\t}\n\n\t\t\t\t// Verify keys are on different shards\n\t\t\t\tslotMap := make(map[int]bool)\n\t\t\t\tfor key := range testData {\n\t\t\t\t\tslot := hashtag.Slot(key)\n\t\t\t\t\tslotMap[slot] = true\n\t\t\t\t}\n\t\t\t\tExpect(len(slotMap)).To(BeNumerically(\">\", 1))\n\n\t\t\t\t// Use individual SET commands instead of MSET\n\t\t\t\tkeys := make([]string, 0, len(testData))\n\t\t\t\tfor key := range testData {\n\t\t\t\t\tkeys = append(keys, key)\n\t\t\t\t}\n\n\t\t\t\tmsetResult := client.MSet(ctx, testData)\n\t\t\t\tExpect(msetResult.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(msetResult.Val()).To(Equal(\"OK\"))\n\n\t\t\t\t// Execute MGET\n\t\t\t\tmgetResult := client.MGet(ctx, keys...)\n\t\t\t\tExpect(mgetResult.Err()).NotTo(HaveOccurred())\n\n\t\t\t\t// Verify all values match\n\t\t\t\tvalues := mgetResult.Val()\n\t\t\t\tExpect(len(values)).To(Equal(len(keys)))\n\t\t\t\tfor i, key := range keys {\n\t\t\t\t\tExpect(values[i]).To(Equal(testData[key]))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should test multi-shard commands cannot be used in pipeline\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Create keys across multiple shards\n\t\t\t\tkeys := []string{\n\t\t\t\t\t\"pipeline_test_1111\",\n\t\t\t\t\t\"pipeline_test_2222\",\n\t\t\t\t\t\"pipeline_test_3333\",\n\t\t\t\t}\n\n\t\t\t\t// Verify keys are on different shards\n\t\t\t\tslotMap := make(map[int]bool)\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tslot := hashtag.Slot(key)\n\t\t\t\t\tslotMap[slot] = true\n\t\t\t\t}\n\t\t\t\tExpect(len(slotMap)).To(BeNumerically(\">\", 1))\n\n\t\t\t\t// Try to use MGET in pipeline - should fail\n\t\t\t\tpipe := client.Pipeline()\n\t\t\t\tpipe.MGet(ctx, keys...)\n\t\t\t\t_, err := pipe.Exec(ctx)\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\tExpect(err.Error()).To(ContainSubstring(\"cannot pipeline command\"))\n\t\t\t})\n\n\t\t\tIt(\"should test DisableRoutingPolicies option disables routing policies\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Test 1: With routing policies enabled (default), MGET should work across slots\n\t\t\t\ttestData := map[string]string{\n\t\t\t\t\t\"disable_routing_key_1111\": \"value1\",\n\t\t\t\t\t\"disable_routing_key_2222\": \"value2\",\n\t\t\t\t\t\"disable_routing_key_3333\": \"value3\",\n\t\t\t\t}\n\n\t\t\t\t// Set keys\n\t\t\t\tfor key, value := range testData {\n\t\t\t\t\tExpect(client.Set(ctx, key, value, 0).Err()).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\t// Verify keys are on different shards\n\t\t\t\tslotMap := make(map[int]bool)\n\t\t\t\tfor key := range testData {\n\t\t\t\t\tslot := hashtag.Slot(key)\n\t\t\t\t\tslotMap[slot] = true\n\t\t\t\t}\n\t\t\t\tExpect(len(slotMap)).To(BeNumerically(\">\", 1))\n\n\t\t\t\tkeys := make([]string, 0, len(testData))\n\t\t\t\tfor key := range testData {\n\t\t\t\t\tkeys = append(keys, key)\n\t\t\t\t}\n\n\t\t\t\t// With routing policies enabled, MGET should work\n\t\t\t\tmgetResult := client.MGet(ctx, keys...)\n\t\t\t\tExpect(mgetResult.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(mgetResult.Val())).To(Equal(len(keys)))\n\n\t\t\t\t// Test 2: With routing policies disabled, MGET should fail with CROSSSLOT error\n\t\t\t\topt := redisClusterOptions()\n\t\t\t\topt.DisableRoutingPolicies = true\n\t\t\t\tclientWithoutPolicies := cluster.newClusterClient(ctx, opt)\n\t\t\t\tdefer clientWithoutPolicies.Close()\n\n\t\t\t\t// Try MGET with routing policies disabled - should fail with CROSSSLOT error\n\t\t\t\tmgetResultDisabled := clientWithoutPolicies.MGet(ctx, keys...)\n\t\t\t\tExpect(mgetResultDisabled.Err()).To(HaveOccurred())\n\t\t\t\tExpect(mgetResultDisabled.Err().Error()).To(ContainSubstring(\"CROSSSLOT\"))\n\t\t\t})\n\n\t\t\tIt(\"should test large MGET with many keys across all shards\", func() {\n\t\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t\t// Create many keys to ensure coverage across all shards\n\t\t\t\tnumKeys := 100\n\t\t\t\tkeys := make([]string, numKeys)\n\t\t\t\tvalues := make(map[string]string)\n\n\t\t\t\tfor i := 0; i < numKeys; i++ {\n\t\t\t\t\tkey := fmt.Sprintf(\"large_mget_key_%d\", i)\n\t\t\t\t\tvalue := fmt.Sprintf(\"value_%d\", i)\n\t\t\t\t\tkeys[i] = key\n\t\t\t\t\tvalues[key] = value\n\t\t\t\t\tExpect(client.Set(ctx, key, value, 0).Err()).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\t// Verify keys are distributed across multiple shards\n\t\t\t\tslotMap := make(map[int]bool)\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tslot := hashtag.Slot(key)\n\t\t\t\t\tslotMap[slot] = true\n\t\t\t\t}\n\t\t\t\tExpect(len(slotMap)).To(BeNumerically(\">\", 1))\n\n\t\t\t\t// Execute MGET\n\t\t\t\tresult := client.MGet(ctx, keys...)\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\n\t\t\t\t// Verify all values are correct\n\t\t\t\tresultValues := result.Val()\n\t\t\t\tExpect(len(resultValues)).To(Equal(numKeys))\n\t\t\t\tfor i, key := range keys {\n\t\t\t\t\tExpect(resultValues[i]).To(Equal(values[key]))\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\n\t\tIt(\"should route keyless commands to arbitrary shards using round robin\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\tvar numMasters int\n\t\t\tvar numMastersMu sync.Mutex\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\tnumMastersMu.Lock()\n\t\t\t\tnumMasters++\n\t\t\t\tnumMastersMu.Unlock()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(numMasters).To(BeNumerically(\">\", 1))\n\n\t\t\terr = client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.ConfigResetStat(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Helper function to get ECHO command counts from all nodes\n\t\t\tgetEchoCounts := func() map[string]int {\n\t\t\t\techoCounts := make(map[string]int)\n\t\t\t\tvar echoCountsMu sync.Mutex\n\t\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\t\tinfo := master.Info(ctx, \"server\")\n\t\t\t\t\tExpect(info.Err()).NotTo(HaveOccurred())\n\n\t\t\t\t\tserverInfo := info.Val()\n\t\t\t\t\tportStart := strings.Index(serverInfo, \"tcp_port:\")\n\t\t\t\t\tportLine := serverInfo[portStart:]\n\t\t\t\t\tportEnd := strings.Index(portLine, \"\\r\\n\")\n\t\t\t\t\tif portEnd == -1 {\n\t\t\t\t\t\tportEnd = len(portLine)\n\t\t\t\t\t}\n\t\t\t\t\tport := strings.TrimPrefix(portLine[:portEnd], \"tcp_port:\")\n\n\t\t\t\t\tcommandStats := master.Info(ctx, \"commandstats\")\n\t\t\t\t\tcount := 0\n\t\t\t\t\tif commandStats.Err() == nil {\n\t\t\t\t\t\tstats := commandStats.Val()\n\t\t\t\t\t\tcmdStatKey := \"cmdstat_echo:\"\n\t\t\t\t\t\tif strings.Contains(stats, cmdStatKey) {\n\t\t\t\t\t\t\tstatStart := strings.Index(stats, cmdStatKey)\n\t\t\t\t\t\t\tstatLine := stats[statStart:]\n\t\t\t\t\t\t\tstatEnd := strings.Index(statLine, \"\\r\\n\")\n\t\t\t\t\t\t\tif statEnd == -1 {\n\t\t\t\t\t\t\t\tstatEnd = len(statLine)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tstatLine = statLine[:statEnd]\n\n\t\t\t\t\t\t\tcallsStart := strings.Index(statLine, \"calls=\")\n\t\t\t\t\t\t\tif callsStart != -1 {\n\t\t\t\t\t\t\t\tcallsStr := statLine[callsStart+6:]\n\t\t\t\t\t\t\t\tcallsEnd := strings.Index(callsStr, \",\")\n\t\t\t\t\t\t\t\tif callsEnd == -1 {\n\t\t\t\t\t\t\t\t\tcallsEnd = strings.Index(callsStr, \"\\r\")\n\t\t\t\t\t\t\t\t\tif callsEnd == -1 {\n\t\t\t\t\t\t\t\t\t\tcallsEnd = len(callsStr)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif callsCount, err := strconv.Atoi(callsStr[:callsEnd]); err == nil {\n\t\t\t\t\t\t\t\t\tcount = callsCount\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\techoCountsMu.Lock()\n\t\t\t\t\techoCounts[port] = count\n\t\t\t\t\techoCountsMu.Unlock()\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\treturn echoCounts\n\t\t\t}\n\n\t\t\t// Single ECHO command should go to exactly one shard\n\t\t\tresult := client.Echo(ctx, \"single_test\")\n\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(result.Val()).To(Equal(\"single_test\"))\n\n\t\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t\t// Verify single command went to exactly one shard\n\t\t\techoCounts := getEchoCounts()\n\t\t\tshardsWithEcho := 0\n\t\t\tfor _, count := range echoCounts {\n\t\t\t\tif count > 0 {\n\t\t\t\t\tshardsWithEcho++\n\t\t\t\t\tExpect(count).To(Equal(1))\n\t\t\t\t}\n\t\t\t}\n\t\t\tExpect(shardsWithEcho).To(Equal(1))\n\n\t\t\t// Reset stats before multi-ECHO test to get clean counts\n\t\t\terr = client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.ConfigResetStat(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Test Multiple ECHO commands should distribute across all shards using round robin\n\t\t\t// With round robin, sending numMasters commands should hit each shard exactly once\n\t\t\tnumCommands := numMasters\n\n\t\t\tfor i := 0; i < numCommands; i++ {\n\t\t\t\tresult := client.Echo(ctx, fmt.Sprintf(\"multi_test_%d\", i))\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Val()).To(Equal(fmt.Sprintf(\"multi_test_%d\", i)))\n\t\t\t}\n\n\t\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t\techoCounts = getEchoCounts()\n\t\t\ttotalEchos := 0\n\t\t\tshardsWithEchos := 0\n\t\t\tfor _, count := range echoCounts {\n\t\t\t\tif count > 0 {\n\t\t\t\t\tshardsWithEchos++\n\t\t\t\t}\n\t\t\t\ttotalEchos += count\n\t\t\t}\n\n\t\t\t// All shards should have received commands with round robin distribution\n\t\t\tExpect(shardsWithEchos).To(Equal(numMasters))\n\n\t\t\t// Total should match what we sent\n\t\t\tExpect(totalEchos).To(Equal(numCommands))\n\t\t})\n\t})\n\n\tvar _ = Describe(\"ClusterClient ParseURL\", func() {\n\t\tcases := []struct {\n\t\t\ttest string\n\t\t\turl  string\n\t\t\to    *redis.ClusterOptions // expected value\n\t\t\terr  error\n\t\t}{\n\t\t\t{\n\t\t\t\ttest: \"ParseRedisURL\",\n\t\t\t\turl:  \"redis://localhost:123\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}},\n\t\t\t}, {\n\t\t\t\ttest: \"ParseRedissURL\",\n\t\t\t\turl:  \"rediss://localhost:123\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, TLSConfig: &tls.Config{ServerName: \"localhost\"}},\n\t\t\t}, {\n\t\t\t\ttest: \"MissingRedisPort\",\n\t\t\t\turl:  \"redis://localhost\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:6379\"}},\n\t\t\t}, {\n\t\t\t\ttest: \"MissingRedissPort\",\n\t\t\t\turl:  \"rediss://localhost\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:6379\"}, TLSConfig: &tls.Config{ServerName: \"localhost\"}},\n\t\t\t}, {\n\t\t\t\ttest: \"MultipleRedisURLs\",\n\t\t\t\turl:  \"redis://localhost:123?addr=localhost:1234&addr=localhost:12345\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\", \"localhost:1234\", \"localhost:12345\"}},\n\t\t\t}, {\n\t\t\t\ttest: \"MultipleRedissURLs\",\n\t\t\t\turl:  \"rediss://localhost:123?addr=localhost:1234&addr=localhost:12345\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\", \"localhost:1234\", \"localhost:12345\"}, TLSConfig: &tls.Config{ServerName: \"localhost\"}},\n\t\t\t}, {\n\t\t\t\ttest: \"OnlyPassword\",\n\t\t\t\turl:  \"redis://:bar@localhost:123\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, Password: \"bar\"},\n\t\t\t}, {\n\t\t\t\ttest: \"OnlyUser\",\n\t\t\t\turl:  \"redis://foo@localhost:123\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, Username: \"foo\"},\n\t\t\t}, {\n\t\t\t\ttest: \"RedisUsernamePassword\",\n\t\t\t\turl:  \"redis://foo:bar@localhost:123\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, Username: \"foo\", Password: \"bar\"},\n\t\t\t}, {\n\t\t\t\ttest: \"RedissUsernamePassword\",\n\t\t\t\turl:  \"rediss://foo:bar@localhost:123?addr=localhost:1234\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\", \"localhost:1234\"}, Username: \"foo\", Password: \"bar\", TLSConfig: &tls.Config{ServerName: \"localhost\"}},\n\t\t\t}, {\n\t\t\t\ttest: \"QueryParameters\",\n\t\t\t\turl:  \"redis://localhost:123?read_timeout=2&pool_fifo=true&addr=localhost:1234\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\", \"localhost:1234\"}, ReadTimeout: 2 * time.Second, PoolFIFO: true},\n\t\t\t}, {\n\t\t\t\ttest: \"DisabledTimeout\",\n\t\t\t\turl:  \"redis://localhost:123?conn_max_idle_time=0\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, ConnMaxIdleTime: -1},\n\t\t\t}, {\n\t\t\t\ttest: \"DisabledTimeoutNeg\",\n\t\t\t\turl:  \"redis://localhost:123?conn_max_idle_time=-1\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, ConnMaxIdleTime: -1},\n\t\t\t}, {\n\t\t\t\ttest: \"UseDefault\",\n\t\t\t\turl:  \"redis://localhost:123?conn_max_idle_time=\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, ConnMaxIdleTime: 0},\n\t\t\t}, {\n\t\t\t\ttest: \"Protocol\",\n\t\t\t\turl:  \"redis://localhost:123?protocol=2\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, Protocol: 2},\n\t\t\t}, {\n\t\t\t\ttest: \"ClientName\",\n\t\t\t\turl:  \"redis://localhost:123?client_name=cluster_hi\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, ClientName: \"cluster_hi\"},\n\t\t\t}, {\n\t\t\t\ttest: \"UseDefaultMissing=\",\n\t\t\t\turl:  \"redis://localhost:123?conn_max_idle_time\",\n\t\t\t\to:    &redis.ClusterOptions{Addrs: []string{\"localhost:123\"}, ConnMaxIdleTime: 0},\n\t\t\t}, {\n\t\t\t\ttest: \"InvalidQueryAddr\",\n\t\t\t\turl:  \"rediss://foo:bar@localhost:123?addr=rediss://foo:barr@localhost:1234\",\n\t\t\t\terr:  errors.New(`redis: unable to parse addr param: rediss://foo:barr@localhost:1234`),\n\t\t\t}, {\n\t\t\t\ttest: \"InvalidInt\",\n\t\t\t\turl:  \"redis://localhost?pool_size=five\",\n\t\t\t\terr:  errors.New(`redis: invalid pool_size number: strconv.Atoi: parsing \"five\": invalid syntax`),\n\t\t\t}, {\n\t\t\t\ttest: \"InvalidBool\",\n\t\t\t\turl:  \"redis://localhost?pool_fifo=yes\",\n\t\t\t\terr:  errors.New(`redis: invalid pool_fifo boolean: expected true/false/1/0 or an empty string, got \"yes\"`),\n\t\t\t}, {\n\t\t\t\ttest: \"UnknownParam\",\n\t\t\t\turl:  \"redis://localhost?abc=123\",\n\t\t\t\terr:  errors.New(\"redis: unexpected option: abc\"),\n\t\t\t}, {\n\t\t\t\ttest: \"InvalidScheme\",\n\t\t\t\turl:  \"https://google.com\",\n\t\t\t\terr:  errors.New(\"redis: invalid URL scheme: https\"),\n\t\t\t},\n\t\t}\n\n\t\tIt(\"should match ParseClusterURL\", func() {\n\t\t\tfor i := range cases {\n\t\t\t\ttc := cases[i]\n\t\t\t\tactual, err := redis.ParseClusterURL(tc.url)\n\t\t\t\tif tc.err != nil {\n\t\t\t\t\tExpect(err).Should(MatchError(tc.err))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\tif err == nil {\n\t\t\t\t\tExpect(tc.o).NotTo(BeNil())\n\n\t\t\t\t\tExpect(tc.o.Addrs).To(Equal(actual.Addrs))\n\t\t\t\t\tExpect(tc.o.TLSConfig).To(Equal(actual.TLSConfig))\n\t\t\t\t\tExpect(tc.o.Username).To(Equal(actual.Username))\n\t\t\t\t\tExpect(tc.o.Password).To(Equal(actual.Password))\n\t\t\t\t\tExpect(tc.o.MaxRetries).To(Equal(actual.MaxRetries))\n\t\t\t\t\tExpect(tc.o.MinRetryBackoff).To(Equal(actual.MinRetryBackoff))\n\t\t\t\t\tExpect(tc.o.MaxRetryBackoff).To(Equal(actual.MaxRetryBackoff))\n\t\t\t\t\tExpect(tc.o.DialTimeout).To(Equal(actual.DialTimeout))\n\t\t\t\t\tExpect(tc.o.ReadTimeout).To(Equal(actual.ReadTimeout))\n\t\t\t\t\tExpect(tc.o.WriteTimeout).To(Equal(actual.WriteTimeout))\n\t\t\t\t\tExpect(tc.o.PoolFIFO).To(Equal(actual.PoolFIFO))\n\t\t\t\t\tExpect(tc.o.PoolSize).To(Equal(actual.PoolSize))\n\t\t\t\t\tExpect(tc.o.MinIdleConns).To(Equal(actual.MinIdleConns))\n\t\t\t\t\tExpect(tc.o.ConnMaxLifetime).To(Equal(actual.ConnMaxLifetime))\n\t\t\t\t\tExpect(tc.o.ConnMaxIdleTime).To(Equal(actual.ConnMaxIdleTime))\n\t\t\t\t\tExpect(tc.o.PoolTimeout).To(Equal(actual.PoolTimeout))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should distribute keyless commands randomly across shards using random shard picker\", func() {\n\t\t\tSkipBeforeRedisVersion(7.9, \"The tips are included from Redis 8\")\n\n\t\t\t// Create a cluster client with random shard picker\n\t\t\topt := redisClusterOptions()\n\t\t\topt.ShardPicker = &routing.RandomPicker{}\n\t\t\trandomClient := cluster.newClusterClient(ctx, opt)\n\t\t\tdefer randomClient.Close()\n\n\t\t\tEventually(func() error {\n\t\t\t\treturn randomClient.Ping(ctx).Err()\n\t\t\t}, 30*time.Second).ShouldNot(HaveOccurred())\n\n\t\t\tvar numMasters int\n\t\t\tvar numMastersMu sync.Mutex\n\t\t\terr := randomClient.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\tnumMastersMu.Lock()\n\t\t\t\tnumMasters++\n\t\t\t\tnumMastersMu.Unlock()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(numMasters).To(BeNumerically(\">\", 1))\n\n\t\t\t// Reset command statistics on all masters\n\t\t\terr = randomClient.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\treturn master.ConfigResetStat(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Helper function to get ECHO command counts from all nodes\n\t\t\tgetEchoCounts := func() map[string]int {\n\t\t\t\techoCounts := make(map[string]int)\n\t\t\t\tvar echoCountsMu sync.Mutex\n\t\t\t\terr := randomClient.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\t\taddr := master.Options().Addr\n\t\t\t\t\tport := addr[strings.LastIndex(addr, \":\")+1:]\n\n\t\t\t\t\tinfo, err := master.Info(ctx, \"commandstats\").Result()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tcount := 0\n\t\t\t\t\tif strings.Contains(info, \"cmdstat_echo:\") {\n\t\t\t\t\t\tlines := strings.Split(info, \"\\n\")\n\t\t\t\t\t\tfor _, line := range lines {\n\t\t\t\t\t\t\tif strings.HasPrefix(line, \"cmdstat_echo:\") {\n\t\t\t\t\t\t\t\tparts := strings.Split(line, \",\")\n\t\t\t\t\t\t\t\tif len(parts) > 0 {\n\t\t\t\t\t\t\t\t\tcallsPart := strings.Split(parts[0], \"=\")\n\t\t\t\t\t\t\t\t\tif len(callsPart) > 1 {\n\t\t\t\t\t\t\t\t\t\tif parsedCount, parseErr := strconv.Atoi(callsPart[1]); parseErr == nil {\n\t\t\t\t\t\t\t\t\t\t\tcount = parsedCount\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\techoCountsMu.Lock()\n\t\t\t\t\techoCounts[port] = count\n\t\t\t\t\techoCountsMu.Unlock()\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\treturn echoCounts\n\t\t\t}\n\n\t\t\t// Execute multiple ECHO commands and measure distribution\n\t\t\tnumCommands := 100\n\t\t\tfor i := 0; i < numCommands; i++ {\n\t\t\t\tresult := randomClient.Echo(ctx, fmt.Sprintf(\"random_test_%d\", i))\n\t\t\t\tExpect(result.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\techoCounts := getEchoCounts()\n\n\t\t\ttotalEchos := 0\n\t\t\tshardsWithEchos := 0\n\n\t\t\tfor _, count := range echoCounts {\n\t\t\t\tif count > 0 {\n\t\t\t\t\tshardsWithEchos++\n\t\t\t\t}\n\t\t\t\ttotalEchos += count\n\t\t\t}\n\n\t\t\tExpect(totalEchos).To(Equal(numCommands))\n\t\t\tExpect(shardsWithEchos).To(BeNumerically(\">=\", 2))\n\t\t})\n\t})\n})\n\nvar _ = Describe(\"ClusterClient FailingTimeoutSeconds\", func() {\n\tvar client *redis.ClusterClient\n\n\tAfterEach(func() {\n\t\tif client != nil {\n\t\t\t_ = client.Close()\n\t\t}\n\t})\n\n\tIt(\"should use default failing timeout of 15 seconds\", func() {\n\t\topt := redisClusterOptions()\n\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t// Default should be 15 seconds\n\t\tExpect(opt.FailingTimeoutSeconds).To(Equal(15))\n\t})\n\n\tIt(\"should use custom failing timeout\", func() {\n\t\topt := redisClusterOptions()\n\t\topt.FailingTimeoutSeconds = 30\n\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t// Should use custom value\n\t\tExpect(opt.FailingTimeoutSeconds).To(Equal(30))\n\t})\n\n\tIt(\"should parse failing_timeout_seconds from URL\", func() {\n\t\turl := \"redis://localhost:16600?failing_timeout_seconds=25\"\n\t\topt, err := redis.ParseClusterURL(url)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(opt.FailingTimeoutSeconds).To(Equal(25))\n\t})\n\n\tIt(\"should handle node failing timeout correctly\", func() {\n\t\topt := redisClusterOptions()\n\t\topt.FailingTimeoutSeconds = 2 // Short timeout for testing\n\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t// Get a node and mark it as failing\n\t\tnodes, err := client.Nodes(ctx, \"A\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(nodes)).To(BeNumerically(\">\", 0))\n\n\t\tnode := nodes[0]\n\n\t\t// Initially not failing\n\t\tExpect(node.Failing()).To(BeFalse())\n\n\t\t// Mark as failing\n\t\tnode.MarkAsFailing()\n\t\tExpect(node.Failing()).To(BeTrue())\n\n\t\t// Should still be failing after 1 second (less than timeout)\n\t\ttime.Sleep(1 * time.Second)\n\t\tExpect(node.Failing()).To(BeTrue())\n\n\t\t// Should not be failing after timeout expires\n\t\ttime.Sleep(2 * time.Second) // Total 3 seconds > 2 second timeout\n\t\tExpect(node.Failing()).To(BeFalse())\n\t})\n\n\tIt(\"should handle zero timeout by using default\", func() {\n\t\topt := redisClusterOptions()\n\t\topt.FailingTimeoutSeconds = 0 // Should use default\n\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\t// After initialization, should be set to default\n\t\tExpect(opt.FailingTimeoutSeconds).To(Equal(15))\n\t})\n})\n"
  },
  {
    "path": "otel.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/otel\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// ConnInfo provides information about a Redis connection for metrics.\ntype ConnInfo interface {\n\tRemoteAddr() net.Addr\n\tPoolName() string\n}\n\ntype Pooler interface {\n\tPoolStats() *pool.Stats\n}\n\ntype PubSubPooler interface {\n\tStats() *pool.PubSubStats\n}\n\n// OTelRecorder is the interface for recording OpenTelemetry metrics.\n\ntype OTelRecorder interface {\n\t// RecordOperationDuration records the total operation duration (including all retries)\n\tRecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn ConnInfo, dbIndex int)\n\n\t// RecordPipelineOperationDuration records the total pipeline/transaction duration.\n\t// operationName should be \"PIPELINE\" for regular pipelines or \"MULTI\" for transactions.\n\tRecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn ConnInfo, dbIndex int)\n\n\t// RecordConnectionCreateTime records the time it took to create a new connection\n\tRecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn ConnInfo)\n\n\t// RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed\n\t// delta: +1 for relaxed, -1 for unrelaxed\n\t// poolName: name of the connection pool (e.g., \"main\", \"pubsub\")\n\t// notificationType: the notification type that triggered the timeout relaxation (e.g., \"MOVING\", \"HANDOFF\")\n\tRecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn ConnInfo, poolName, notificationType string)\n\n\t// RecordConnectionHandoff records when a connection is handed off to another node\n\t// poolName: name of the connection pool (e.g., \"main\", \"pubsub\")\n\tRecordConnectionHandoff(ctx context.Context, cn ConnInfo, poolName string)\n\n\t// RecordError records client errors (ASK, MOVED, handshake failures, etc.)\n\t// errorType: type of error (e.g., \"ASK\", \"MOVED\", \"HANDSHAKE_FAILED\")\n\t// statusCode: Redis response status code if available (e.g., \"MOVED\", \"ASK\")\n\t// isInternal: whether this is an internal error\n\t// retryAttempts: number of retry attempts made\n\tRecordError(ctx context.Context, errorType string, cn ConnInfo, statusCode string, isInternal bool, retryAttempts int)\n\n\t// RecordMaintenanceNotification records when a maintenance notification is received\n\t// notificationType: the type of notification (e.g., \"MOVING\", \"MIGRATING\", etc.)\n\tRecordMaintenanceNotification(ctx context.Context, cn ConnInfo, notificationType string)\n\n\t// RecordConnectionWaitTime records the time spent waiting for a connection from the pool\n\tRecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn ConnInfo)\n\n\t// RecordConnectionClosed records when a connection is closed\n\t// reason: reason for closing (e.g., \"idle\", \"max_lifetime\", \"error\", \"pool_closed\")\n\t// err: the error that caused the close (nil for non-error closures)\n\tRecordConnectionClosed(ctx context.Context, cn ConnInfo, reason string, err error)\n\n\t// RecordPubSubMessage records a Pub/Sub message\n\t// direction: \"sent\" or \"received\"\n\t// channel: channel name (may be hidden for cardinality reduction)\n\t// sharded: true for sharded pub/sub (SPUBLISH/SSUBSCRIBE)\n\tRecordPubSubMessage(ctx context.Context, cn ConnInfo, direction, channel string, sharded bool)\n\n\t// RecordStreamLag records the lag for stream consumer group processing\n\t// lag: time difference between message creation and consumption\n\t// streamName: name of the stream (may be hidden for cardinality reduction)\n\t// consumerGroup: name of the consumer group\n\t// consumerName: name of the consumer\n\tRecordStreamLag(ctx context.Context, lag time.Duration, cn ConnInfo, streamName, consumerGroup, consumerName string)\n}\n\n// This is used for async gauge metrics that need to pull stats from pools periodically.\ntype OTelPoolRegistrar interface {\n\t// RegisterPool is called when a new client is created with its main connection pool.\n\t// poolName: unique identifier for the pool (e.g., \"main_abc123\")\n\tRegisterPool(poolName string, pool Pooler)\n\t// UnregisterPool is called when a client is closed to remove its pool from the registry.\n\tUnregisterPool(pool Pooler)\n\t// RegisterPubSubPool is called when a new client is created with a PubSub pool.\n\t// poolName: unique identifier for the pool (e.g., \"main_abc123_pubsub\")\n\tRegisterPubSubPool(poolName string, pool PubSubPooler)\n\t// UnregisterPubSubPool is called when a PubSub client is closed to remove its pool.\n\tUnregisterPubSubPool(pool PubSubPooler)\n}\n\n// SetOTelRecorder sets the global OpenTelemetry recorder.\nfunc SetOTelRecorder(r OTelRecorder) {\n\tif r == nil {\n\t\totel.SetGlobalRecorder(nil)\n\t\treturn\n\t}\n\totel.SetGlobalRecorder(&otelRecorderAdapter{r})\n}\n\ntype otelRecorderAdapter struct {\n\trecorder OTelRecorder\n}\n\n// toConnInfo converts *pool.Conn to ConnInfo interface properly.\n// This ensures that a nil *pool.Conn becomes a true nil interface,\n// not a non-nil interface containing a nil pointer.\nfunc toConnInfo(cn *pool.Conn) ConnInfo {\n\tif cn == nil {\n\t\treturn nil\n\t}\n\treturn cn\n}\n\nfunc (a *otelRecorderAdapter) RecordOperationDuration(ctx context.Context, duration time.Duration, cmd otel.Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\t// Convert internal Cmder to public Cmder\n\tif publicCmd, ok := cmd.(Cmder); ok {\n\t\ta.recorder.RecordOperationDuration(ctx, duration, publicCmd, attempts, err, toConnInfo(cn), dbIndex)\n\t}\n}\n\nfunc (a *otelRecorderAdapter) RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {\n\ta.recorder.RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, toConnInfo(cn), dbIndex)\n}\n\nfunc (a *otelRecorderAdapter) RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn) {\n\ta.recorder.RecordConnectionCreateTime(ctx, duration, toConnInfo(cn))\n}\n\nfunc (a *otelRecorderAdapter) RecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string) {\n\ta.recorder.RecordConnectionRelaxedTimeout(ctx, delta, toConnInfo(cn), poolName, notificationType)\n}\n\nfunc (a *otelRecorderAdapter) RecordConnectionHandoff(ctx context.Context, cn *pool.Conn, poolName string) {\n\ta.recorder.RecordConnectionHandoff(ctx, toConnInfo(cn), poolName)\n}\n\nfunc (a *otelRecorderAdapter) RecordError(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int) {\n\ta.recorder.RecordError(ctx, errorType, toConnInfo(cn), statusCode, isInternal, retryAttempts)\n}\n\nfunc (a *otelRecorderAdapter) RecordMaintenanceNotification(ctx context.Context, cn *pool.Conn, notificationType string) {\n\ta.recorder.RecordMaintenanceNotification(ctx, toConnInfo(cn), notificationType)\n}\n\nfunc (a *otelRecorderAdapter) RecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn *pool.Conn) {\n\ta.recorder.RecordConnectionWaitTime(ctx, duration, toConnInfo(cn))\n}\n\nfunc (a *otelRecorderAdapter) RecordConnectionClosed(ctx context.Context, cn *pool.Conn, reason string, err error) {\n\ta.recorder.RecordConnectionClosed(ctx, toConnInfo(cn), reason, err)\n}\n\nfunc (a *otelRecorderAdapter) RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool) {\n\ta.recorder.RecordPubSubMessage(ctx, toConnInfo(cn), direction, channel, sharded)\n}\n\nfunc (a *otelRecorderAdapter) RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) {\n\ta.recorder.RecordStreamLag(ctx, lag, toConnInfo(cn), streamName, consumerGroup, consumerName)\n}\n\nfunc (a *otelRecorderAdapter) RegisterPool(poolName string, p pool.Pooler) {\n\tif registrar, ok := a.recorder.(OTelPoolRegistrar); ok {\n\t\tregistrar.RegisterPool(poolName, &poolerAdapter{p})\n\t}\n}\n\nfunc (a *otelRecorderAdapter) UnregisterPool(p pool.Pooler) {\n\tif registrar, ok := a.recorder.(OTelPoolRegistrar); ok {\n\t\tregistrar.UnregisterPool(&poolerAdapter{p})\n\t}\n}\n\nfunc (a *otelRecorderAdapter) RegisterPubSubPool(poolName string, p otel.PubSubPooler) {\n\tif registrar, ok := a.recorder.(OTelPoolRegistrar); ok {\n\t\tregistrar.RegisterPubSubPool(poolName, &pubSubPoolerAdapter{p})\n\t}\n}\n\nfunc (a *otelRecorderAdapter) UnregisterPubSubPool(p otel.PubSubPooler) {\n\tif registrar, ok := a.recorder.(OTelPoolRegistrar); ok {\n\t\tregistrar.UnregisterPubSubPool(&pubSubPoolerAdapter{p})\n\t}\n}\n\ntype poolerAdapter struct {\n\tp pool.Pooler\n}\n\nfunc (a *poolerAdapter) PoolStats() *pool.Stats {\n\treturn a.p.Stats()\n}\n\ntype pubSubPoolerAdapter struct {\n\tp otel.PubSubPooler\n}\n\nfunc (a *pubSubPoolerAdapter) Stats() *pool.PubSubStats {\n\treturn a.p.Stats()\n}\n"
  },
  {
    "path": "pipeline.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\ntype pipelineExecer func(context.Context, []Cmder) error\n\n// Pipeliner is a mechanism to realise Redis Pipeline technique.\n//\n// Pipelining is a technique to extremely speed up processing by packing\n// operations to batches, send them at once to Redis and read a replies in a\n// single step.\n// See https://redis.io/topics/pipelining\n//\n// Pay attention, that Pipeline is not a transaction, so you can get unexpected\n// results in case of big pipelines and small read/write timeouts.\n// Redis client has retransmission logic in case of timeouts, pipeline\n// can be retransmitted and commands can be executed more then once.\n// To avoid this: it is good idea to use reasonable bigger read/write timeouts\n// depends of your batch size and/or use TxPipeline.\ntype Pipeliner interface {\n\tStatefulCmdable\n\n\t// Len obtains the number of commands in the pipeline that have not yet been executed.\n\tLen() int\n\n\t// Do is an API for executing any command.\n\t// If a certain Redis command is not yet supported, you can use Do to execute it.\n\tDo(ctx context.Context, args ...interface{}) *Cmd\n\n\t// Process queues the cmd for later execution.\n\tProcess(ctx context.Context, cmd Cmder) error\n\n\t// BatchProcess adds multiple commands to be executed into the pipeline buffer.\n\tBatchProcess(ctx context.Context, cmd ...Cmder) error\n\n\t// Discard discards all commands in the pipeline buffer that have not yet been executed.\n\tDiscard()\n\n\t// Exec sends all the commands buffered in the pipeline to the redis server.\n\tExec(ctx context.Context) ([]Cmder, error)\n\n\t// Cmds returns the list of queued commands.\n\tCmds() []Cmder\n}\n\nvar _ Pipeliner = (*Pipeline)(nil)\n\n// Pipeline implements pipelining as described in\n// https://redis.io/docs/latest/develop/using-commands/pipelining.\n// Please note: it is not safe for concurrent use by multiple goroutines.\ntype Pipeline struct {\n\tcmdable\n\tstatefulCmdable\n\n\texec pipelineExecer\n\tcmds []Cmder\n}\n\nfunc (c *Pipeline) init() {\n\tc.cmdable = c.Process\n\tc.statefulCmdable = c.Process\n}\n\n// Len returns the number of queued commands.\nfunc (c *Pipeline) Len() int {\n\treturn len(c.cmds)\n}\n\n// Do queues the custom command for later execution.\nfunc (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd {\n\tcmd := NewCmd(ctx, args...)\n\tif len(args) == 0 {\n\t\tcmd.SetErr(errors.New(\"redis: please enter the command to be executed\"))\n\t\treturn cmd\n\t}\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Process queues the cmd for later execution.\nfunc (c *Pipeline) Process(ctx context.Context, cmd Cmder) error {\n\treturn c.BatchProcess(ctx, cmd)\n}\n\n// BatchProcess queues multiple cmds for later execution.\nfunc (c *Pipeline) BatchProcess(ctx context.Context, cmd ...Cmder) error {\n\tc.cmds = append(c.cmds, cmd...)\n\treturn nil\n}\n\n// Discard resets the pipeline and discards queued commands.\nfunc (c *Pipeline) Discard() {\n\tc.cmds = c.cmds[:0]\n}\n\n// Exec executes all previously queued commands using one\n// client-server roundtrip.\n//\n// Exec always returns list of commands and error of the first failed\n// command if any.\nfunc (c *Pipeline) Exec(ctx context.Context) ([]Cmder, error) {\n\tif len(c.cmds) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tcmds := c.cmds\n\tc.cmds = nil\n\n\treturn cmds, c.exec(ctx, cmds)\n}\n\nfunc (c *Pipeline) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\tif err := fn(c); err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.Exec(ctx)\n}\n\nfunc (c *Pipeline) Pipeline() Pipeliner {\n\treturn c\n}\n\nfunc (c *Pipeline) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.Pipelined(ctx, fn)\n}\n\nfunc (c *Pipeline) TxPipeline() Pipeliner {\n\treturn c\n}\n\nfunc (c *Pipeline) Cmds() []Cmder {\n\treturn c.cmds\n}\n"
  },
  {
    "path": "pipeline_test.go",
    "content": "package redis_test\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"pipelining\", func() {\n\tvar client *redis.Client\n\tvar pipe *redis.Pipeline\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"supports block style\", func() {\n\t\tvar get *redis.StringCmd\n\t\tcmds, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\tget = pipe.Get(ctx, \"foo\")\n\t\t\treturn nil\n\t\t})\n\t\tExpect(err).To(Equal(redis.Nil))\n\t\tExpect(cmds).To(HaveLen(1))\n\t\tExpect(cmds[0]).To(Equal(get))\n\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\tExpect(get.Val()).To(Equal(\"\"))\n\t})\n\n\tIt(\"exports queued commands\", func() {\n\t\tp := client.Pipeline()\n\t\tcmds := p.Cmds()\n\t\tExpect(cmds).To(BeEmpty())\n\n\t\tp.Set(ctx, \"foo\", \"bar\", 0)\n\t\tp.Get(ctx, \"foo\")\n\t\tcmds = p.Cmds()\n\t\tExpect(cmds).To(HaveLen(p.Len()))\n\t\tExpect(cmds[0].Name()).To(Equal(\"set\"))\n\t\tExpect(cmds[1].Name()).To(Equal(\"get\"))\n\n\t\tcmds, err := p.Exec(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(cmds).To(HaveLen(2))\n\t\tExpect(cmds[0].Name()).To(Equal(\"set\"))\n\t\tExpect(cmds[0].(*redis.StatusCmd).Val()).To(Equal(\"OK\"))\n\t\tExpect(cmds[1].Name()).To(Equal(\"get\"))\n\t\tExpect(cmds[1].(*redis.StringCmd).Val()).To(Equal(\"bar\"))\n\n\t\tcmds = p.Cmds()\n\t\tExpect(cmds).To(BeEmpty())\n\t})\n\n\tIt(\"pipeline: basic exec\", func() {\n\t\tp := client.Pipeline()\n\t\tp.Get(ctx, \"key\")\n\t\tp.Set(ctx, \"key\", \"value\", 0)\n\t\tp.Get(ctx, \"key\")\n\t\tcmds, err := p.Exec(ctx)\n\t\tExpect(err).To(Equal(redis.Nil))\n\t\tExpect(cmds).To(HaveLen(3))\n\t\tExpect(cmds[0].Err()).To(Equal(redis.Nil))\n\t\tExpect(cmds[1].(*redis.StatusCmd).Val()).To(Equal(\"OK\"))\n\t\tExpect(cmds[1].Err()).NotTo(HaveOccurred())\n\t\tExpect(cmds[2].(*redis.StringCmd).Val()).To(Equal(\"value\"))\n\t\tExpect(cmds[2].Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"pipeline: exec pipeline when get conn failed\", func() {\n\t\tp := client.Pipeline()\n\t\tp.Get(ctx, \"key\")\n\t\tp.Set(ctx, \"key\", \"value\", 0)\n\t\tp.Get(ctx, \"key\")\n\n\t\tclient.Close()\n\n\t\tcmds, err := p.Exec(ctx)\n\t\tExpect(err).To(Equal(redis.ErrClosed))\n\t\tExpect(cmds).To(HaveLen(3))\n\t\tfor _, cmd := range cmds {\n\t\t\tExpect(cmd.Err()).To(Equal(redis.ErrClosed))\n\t\t}\n\n\t\tclient = redis.NewClient(redisOptions())\n\t})\n\n\tassertPipeline := func() {\n\t\tIt(\"returns no errors when there are no commands\", func() {\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tIt(\"discards queued commands\", func() {\n\t\t\tpipe.Get(ctx, \"key\")\n\t\t\tpipe.Discard()\n\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cmds).To(BeNil())\n\t\t})\n\n\t\tIt(\"handles val/err\", func() {\n\t\t\terr := client.Set(ctx, \"key\", \"value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tget := pipe.Get(ctx, \"key\")\n\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cmds).To(HaveLen(1))\n\n\t\t\tval, err := get.Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"value\"))\n\t\t})\n\n\t\tIt(\"supports custom command\", func() {\n\t\t\tpipe.Do(ctx, \"ping\")\n\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t})\n\n\t\tIt(\"handles large pipelines\", Label(\"NonRedisEnterprise\"), func() {\n\t\t\tfor callCount := 1; callCount < 16; callCount++ {\n\t\t\t\tfor i := 1; i <= callCount; i++ {\n\t\t\t\t\tpipe.SetNX(ctx, strconv.Itoa(i)+\"_key\", strconv.Itoa(i)+\"_value\", 0)\n\t\t\t\t}\n\n\t\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmds).To(HaveLen(callCount))\n\t\t\t\tfor _, cmd := range cmds {\n\t\t\t\t\tExpect(cmd).To(BeAssignableToTypeOf(&redis.BoolCmd{}))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should Exec, not Do\", func() {\n\t\t\terr := pipe.Do(ctx).Err()\n\t\t\tExpect(err).To(Equal(errors.New(\"redis: please enter the command to be executed\")))\n\t\t})\n\n\t\tIt(\"should process\", func() {\n\t\t\terr := pipe.Process(ctx, redis.NewCmd(ctx, \"asking\"))\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(pipe.Cmds()).To(HaveLen(1))\n\t\t})\n\n\t\tIt(\"should batchProcess\", func() {\n\t\t\terr := pipe.BatchProcess(ctx, redis.NewCmd(ctx, \"asking\"))\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(pipe.Cmds()).To(HaveLen(1))\n\n\t\t\tpipe.Discard()\n\t\t\tExpect(pipe.Cmds()).To(HaveLen(0))\n\n\t\t\terr = pipe.BatchProcess(ctx, redis.NewCmd(ctx, \"asking\"), redis.NewCmd(ctx, \"set\", \"key\", \"value\"))\n\t\t\tExpect(err).To(BeNil())\n\t\t\tExpect(pipe.Cmds()).To(HaveLen(2))\n\t\t})\n\t}\n\n\tDescribe(\"Pipeline\", func() {\n\t\tBeforeEach(func() {\n\t\t\tpipe = client.Pipeline().(*redis.Pipeline)\n\t\t})\n\n\t\tassertPipeline()\n\t})\n\n\tDescribe(\"TxPipeline\", func() {\n\t\tBeforeEach(func() {\n\t\t\tpipe = client.TxPipeline().(*redis.Pipeline)\n\t\t})\n\n\t\tassertPipeline()\n\t})\n})\n"
  },
  {
    "path": "pool_pubsub_bench_test.go",
    "content": "// Pool and PubSub Benchmark Suite\n//\n// This file contains comprehensive benchmarks for both pool operations and PubSub initialization.\n// It's designed to be run against different branches to compare performance.\n//\n// Usage Examples:\n//   # Run all benchmarks\n//   go test -bench=. -run='^$' -benchtime=1s pool_pubsub_bench_test.go\n//\n//   # Run only pool benchmarks\n//   go test -bench=BenchmarkPool -run='^$' pool_pubsub_bench_test.go\n//\n//   # Run only PubSub benchmarks\n//   go test -bench=BenchmarkPubSub -run='^$' pool_pubsub_bench_test.go\n//\n//   # Compare between branches\n//   git checkout branch1 && go test -bench=. -run='^$' pool_pubsub_bench_test.go > branch1.txt\n//   git checkout branch2 && go test -bench=. -run='^$' pool_pubsub_bench_test.go > branch2.txt\n//   benchcmp branch1.txt branch2.txt\n//\n//   # Run with memory profiling\n//   go test -bench=BenchmarkPoolGetPut -run='^$' -memprofile=mem.prof pool_pubsub_bench_test.go\n//\n//   # Run with CPU profiling\n//   go test -bench=BenchmarkPoolGetPut -run='^$' -cpuprofile=cpu.prof pool_pubsub_bench_test.go\n\npackage redis_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n)\n\n// dummyDialer creates a mock connection for benchmarking\nfunc dummyDialer(ctx context.Context) (net.Conn, error) {\n\treturn &dummyConn{}, nil\n}\n\n// dummyConn implements net.Conn for benchmarking\ntype dummyConn struct{}\n\nfunc (c *dummyConn) Read(b []byte) (n int, err error)  { return len(b), nil }\nfunc (c *dummyConn) Write(b []byte) (n int, err error) { return len(b), nil }\nfunc (c *dummyConn) Close() error                      { return nil }\nfunc (c *dummyConn) LocalAddr() net.Addr               { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 6379} }\nfunc (c *dummyConn) RemoteAddr() net.Addr {\n\treturn &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 6379}\n}\nfunc (c *dummyConn) SetDeadline(t time.Time) error      { return nil }\nfunc (c *dummyConn) SetReadDeadline(t time.Time) error  { return nil }\nfunc (c *dummyConn) SetWriteDeadline(t time.Time) error { return nil }\n\n// =============================================================================\n// POOL BENCHMARKS\n// =============================================================================\n\n// BenchmarkPoolGetPut benchmarks the core pool Get/Put operations\nfunc BenchmarkPoolGetPut(b *testing.B) {\n\tctx := context.Background()\n\n\tpoolSizes := []int{1, 2, 4, 8, 16, 32, 64, 128}\n\n\tfor _, poolSize := range poolSizes {\n\t\tb.Run(fmt.Sprintf(\"PoolSize_%d\", poolSize), func(b *testing.B) {\n\t\t\tconnPool := pool.NewConnPool(&pool.Options{\n\t\t\t\tDialer:             dummyDialer,\n\t\t\t\tPoolSize:           int32(poolSize),\n\t\t\t\tMaxConcurrentDials: poolSize,\n\t\t\t\tPoolTimeout:        time.Second,\n\t\t\t\tDialTimeout:        time.Second,\n\t\t\t\tConnMaxIdleTime:    time.Hour,\n\t\t\t\tMinIdleConns:       int32(0), // Start with no idle connections\n\t\t\t})\n\t\t\tdefer connPool.Close()\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tb.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tconnPool.Put(ctx, cn)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\n// BenchmarkPoolGetPutWithMinIdle benchmarks pool operations with MinIdleConns\nfunc BenchmarkPoolGetPutWithMinIdle(b *testing.B) {\n\tctx := context.Background()\n\n\tconfigs := []struct {\n\t\tpoolSize     int\n\t\tminIdleConns int\n\t}{\n\t\t{8, 2},\n\t\t{16, 4},\n\t\t{32, 8},\n\t\t{64, 16},\n\t}\n\n\tfor _, config := range configs {\n\t\tb.Run(fmt.Sprintf(\"Pool_%d_MinIdle_%d\", config.poolSize, config.minIdleConns), func(b *testing.B) {\n\t\t\tconnPool := pool.NewConnPool(&pool.Options{\n\t\t\t\tDialer:             dummyDialer,\n\t\t\t\tPoolSize:           int32(config.poolSize),\n\t\t\t\tMaxConcurrentDials: config.poolSize,\n\t\t\t\tMinIdleConns:       int32(config.minIdleConns),\n\t\t\t\tPoolTimeout:        time.Second,\n\t\t\t\tDialTimeout:        time.Second,\n\t\t\t\tConnMaxIdleTime:    time.Hour,\n\t\t\t})\n\t\t\tdefer connPool.Close()\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tb.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tconnPool.Put(ctx, cn)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\n// BenchmarkPoolConcurrentGetPut benchmarks pool under high concurrency\nfunc BenchmarkPoolConcurrentGetPut(b *testing.B) {\n\tctx := context.Background()\n\n\tconnPool := pool.NewConnPool(&pool.Options{\n\t\tDialer:             dummyDialer,\n\t\tPoolSize:           int32(32),\n\t\tMaxConcurrentDials: 32,\n\t\tPoolTimeout:        time.Second,\n\t\tDialTimeout:        time.Second,\n\t\tConnMaxIdleTime:    time.Hour,\n\t\tMinIdleConns:       int32(0),\n\t})\n\tdefer connPool.Close()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\t// Test with different levels of concurrency\n\tconcurrencyLevels := []int{1, 2, 4, 8, 16, 32, 64}\n\n\tfor _, concurrency := range concurrencyLevels {\n\t\tb.Run(fmt.Sprintf(\"Concurrency_%d\", concurrency), func(b *testing.B) {\n\t\t\tb.SetParallelism(concurrency)\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tcn, err := connPool.Get(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tb.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tconnPool.Put(ctx, cn)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\n// =============================================================================\n// PUBSUB BENCHMARKS\n// =============================================================================\n\n// benchmarkClient creates a Redis client for benchmarking with mock dialer\nfunc benchmarkClient(poolSize int) *redis.Client {\n\treturn redis.NewClient(&redis.Options{\n\t\tAddr:         \"localhost:6379\", // Mock address\n\t\tDialTimeout:  time.Second,\n\t\tReadTimeout:  time.Second,\n\t\tWriteTimeout: time.Second,\n\t\tPoolSize:     poolSize,\n\t\tMinIdleConns: 0, // Start with no idle connections for consistent benchmarks\n\t})\n}\n\n// BenchmarkPubSubCreation benchmarks PubSub creation and subscription\nfunc BenchmarkPubSubCreation(b *testing.B) {\n\tctx := context.Background()\n\n\tpoolSizes := []int{1, 4, 8, 16, 32}\n\n\tfor _, poolSize := range poolSizes {\n\t\tb.Run(fmt.Sprintf(\"PoolSize_%d\", poolSize), func(b *testing.B) {\n\t\t\tclient := benchmarkClient(poolSize)\n\t\t\tdefer client.Close()\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tpubsub := client.Subscribe(ctx, \"test-channel\")\n\t\t\t\tpubsub.Close()\n\t\t\t}\n\t\t})\n\t}\n}\n\n// BenchmarkPubSubPatternCreation benchmarks PubSub pattern subscription\nfunc BenchmarkPubSubPatternCreation(b *testing.B) {\n\tctx := context.Background()\n\n\tpoolSizes := []int{1, 4, 8, 16, 32}\n\n\tfor _, poolSize := range poolSizes {\n\t\tb.Run(fmt.Sprintf(\"PoolSize_%d\", poolSize), func(b *testing.B) {\n\t\t\tclient := benchmarkClient(poolSize)\n\t\t\tdefer client.Close()\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tpubsub := client.PSubscribe(ctx, \"test-*\")\n\t\t\t\tpubsub.Close()\n\t\t\t}\n\t\t})\n\t}\n}\n\n// BenchmarkPubSubConcurrentCreation benchmarks concurrent PubSub creation\nfunc BenchmarkPubSubConcurrentCreation(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkClient(32)\n\tdefer client.Close()\n\n\tconcurrencyLevels := []int{1, 2, 4, 8, 16}\n\n\tfor _, concurrency := range concurrencyLevels {\n\t\tb.Run(fmt.Sprintf(\"Concurrency_%d\", concurrency), func(b *testing.B) {\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\n\t\t\tvar wg sync.WaitGroup\n\t\t\tsemaphore := make(chan struct{}, concurrency)\n\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\twg.Add(1)\n\t\t\t\tsemaphore <- struct{}{}\n\n\t\t\t\tgo func() {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tdefer func() { <-semaphore }()\n\n\t\t\t\t\tpubsub := client.Subscribe(ctx, \"test-channel\")\n\t\t\t\t\tpubsub.Close()\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\twg.Wait()\n\t\t})\n\t}\n}\n\n// BenchmarkPubSubMultipleChannels benchmarks subscribing to multiple channels\nfunc BenchmarkPubSubMultipleChannels(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkClient(16)\n\tdefer client.Close()\n\n\tchannelCounts := []int{1, 5, 10, 25, 50, 100}\n\n\tfor _, channelCount := range channelCounts {\n\t\tb.Run(fmt.Sprintf(\"Channels_%d\", channelCount), func(b *testing.B) {\n\t\t\t// Prepare channel names\n\t\t\tchannels := make([]string, channelCount)\n\t\t\tfor i := 0; i < channelCount; i++ {\n\t\t\t\tchannels[i] = fmt.Sprintf(\"channel-%d\", i)\n\t\t\t}\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\tpubsub := client.Subscribe(ctx, channels...)\n\t\t\t\tpubsub.Close()\n\t\t\t}\n\t\t})\n\t}\n}\n\n// BenchmarkPubSubReuse benchmarks reusing PubSub connections\nfunc BenchmarkPubSubReuse(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkClient(16)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t// Benchmark just the creation and closing of PubSub connections\n\t\t// This simulates reuse patterns without requiring actual Redis operations\n\t\tpubsub := client.Subscribe(ctx, fmt.Sprintf(\"test-channel-%d\", i))\n\t\tpubsub.Close()\n\t}\n}\n\n// =============================================================================\n// COMBINED BENCHMARKS\n// =============================================================================\n\n// BenchmarkPoolAndPubSubMixed benchmarks mixed pool stats and PubSub operations\nfunc BenchmarkPoolAndPubSubMixed(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkClient(32)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t// Mix of pool stats collection and PubSub creation\n\t\t\tif pb.Next() {\n\t\t\t\t// Pool stats operation\n\t\t\t\tstats := client.PoolStats()\n\t\t\t\t_ = stats.Hits + stats.Misses // Use the stats to prevent optimization\n\t\t\t}\n\n\t\t\tif pb.Next() {\n\t\t\t\t// PubSub operation\n\t\t\t\tpubsub := client.Subscribe(ctx, \"test-channel\")\n\t\t\t\tpubsub.Close()\n\t\t\t}\n\t\t}\n\t})\n}\n\n// BenchmarkPoolStatsCollection benchmarks pool statistics collection\nfunc BenchmarkPoolStatsCollection(b *testing.B) {\n\tclient := benchmarkClient(16)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tstats := client.PoolStats()\n\t\t_ = stats.Hits + stats.Misses + stats.Timeouts // Use the stats to prevent optimization\n\t}\n}\n\n// BenchmarkPoolHighContention tests pool performance under high contention\nfunc BenchmarkPoolHighContention(b *testing.B) {\n\tctx := context.Background()\n\tclient := benchmarkClient(32)\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t// High contention Get/Put operations\n\t\t\tpubsub := client.Subscribe(ctx, \"test-channel\")\n\t\t\tpubsub.Close()\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pool_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"pool\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\topt := redisOptions()\n\t\topt.MinIdleConns = 0\n\t\topt.ConnMaxLifetime = 0\n\t\topt.ConnMaxIdleTime = time.Second\n\t\tclient = redis.NewClient(opt)\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"respects max size\", func() {\n\t\tperform(1000, func(id int) {\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tpool := client.Pool()\n\t\tExpect(pool.Len()).To(BeNumerically(\"<=\", 10))\n\t\tExpect(pool.IdleLen()).To(BeNumerically(\"<=\", 10))\n\t\tExpect(pool.Len()).To(Equal(pool.IdleLen()))\n\t})\n\n\tIt(\"respects max size on multi\", func() {\n\t\tperform(1000, func(id int) {\n\t\t\tvar ping *redis.StatusCmd\n\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\tcmds, err := tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tping = pipe.Ping(ctx)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\treturn err\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(ping.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ping.Val()).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tpool := client.Pool()\n\t\tExpect(pool.Len()).To(BeNumerically(\"<=\", 10))\n\t\tExpect(pool.IdleLen()).To(BeNumerically(\"<=\", 10))\n\t\tExpect(pool.Len()).To(Equal(pool.IdleLen()))\n\t})\n\n\tIt(\"respects max size on pipelines\", func() {\n\t\tperform(1000, func(id int) {\n\t\t\tpipe := client.Pipeline()\n\t\t\tping := pipe.Ping(ctx)\n\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\tExpect(ping.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ping.Val()).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tpool := client.Pool()\n\t\tExpect(pool.Len()).To(BeNumerically(\"<=\", 10))\n\t\tExpect(pool.IdleLen()).To(BeNumerically(\"<=\", 10))\n\t\tExpect(pool.Len()).To(Equal(pool.IdleLen()))\n\t})\n\n\tIt(\"removes broken connections\", func() {\n\t\tcn, err := client.Pool().Get(context.Background())\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tcn.SetNetConn(&badConn{})\n\t\tclient.Pool().Put(ctx, cn)\n\n\t\tval, err := client.Ping(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"PONG\"))\n\n\t\tval, err = client.Ping(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"PONG\"))\n\n\t\tpool := client.Pool()\n\t\tExpect(pool.Len()).To(Equal(1))\n\t\tExpect(pool.IdleLen()).To(Equal(1))\n\n\t\tstats := pool.Stats()\n\t\tExpect(stats.Hits).To(Equal(uint32(1)))\n\t\tExpect(stats.Misses).To(Equal(uint32(2)))\n\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t})\n\n\tIt(\"reuses connections\", func() {\n\t\t// explain: https://github.com/redis/go-redis/pull/1675\n\t\topt := redisOptions()\n\t\topt.MinIdleConns = 0\n\t\topt.ConnMaxLifetime = 0\n\t\topt.ConnMaxIdleTime = 10 * time.Second\n\t\tclient = redis.NewClient(opt)\n\n\t\tfor i := 0; i < 100; i++ {\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t}\n\n\t\tpool := client.Pool()\n\t\tExpect(pool.Len()).To(Equal(1))\n\t\tExpect(pool.IdleLen()).To(Equal(1))\n\n\t\tstats := pool.Stats()\n\t\tExpect(stats.Hits).To(Equal(uint32(99)))\n\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t\tExpect(stats.Timeouts).To(Equal(uint32(0)))\n\t})\n})\n"
  },
  {
    "path": "probabilistic.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\ntype ProbabilisticCmdable interface {\n\tBFAdd(ctx context.Context, key string, element interface{}) *BoolCmd\n\tBFCard(ctx context.Context, key string) *IntCmd\n\tBFExists(ctx context.Context, key string, element interface{}) *BoolCmd\n\tBFInfo(ctx context.Context, key string) *BFInfoCmd\n\tBFInfoArg(ctx context.Context, key, option string) *BFInfoCmd\n\tBFInfoCapacity(ctx context.Context, key string) *BFInfoCmd\n\tBFInfoSize(ctx context.Context, key string) *BFInfoCmd\n\tBFInfoFilters(ctx context.Context, key string) *BFInfoCmd\n\tBFInfoItems(ctx context.Context, key string) *BFInfoCmd\n\tBFInfoExpansion(ctx context.Context, key string) *BFInfoCmd\n\tBFInsert(ctx context.Context, key string, options *BFInsertOptions, elements ...interface{}) *BoolSliceCmd\n\tBFMAdd(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd\n\tBFMExists(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd\n\tBFReserve(ctx context.Context, key string, errorRate float64, capacity int64) *StatusCmd\n\tBFReserveExpansion(ctx context.Context, key string, errorRate float64, capacity, expansion int64) *StatusCmd\n\tBFReserveNonScaling(ctx context.Context, key string, errorRate float64, capacity int64) *StatusCmd\n\tBFReserveWithArgs(ctx context.Context, key string, options *BFReserveOptions) *StatusCmd\n\tBFScanDump(ctx context.Context, key string, iterator int64) *ScanDumpCmd\n\tBFLoadChunk(ctx context.Context, key string, iterator int64, data interface{}) *StatusCmd\n\n\tCFAdd(ctx context.Context, key string, element interface{}) *BoolCmd\n\tCFAddNX(ctx context.Context, key string, element interface{}) *BoolCmd\n\tCFCount(ctx context.Context, key string, element interface{}) *IntCmd\n\tCFDel(ctx context.Context, key string, element interface{}) *BoolCmd\n\tCFExists(ctx context.Context, key string, element interface{}) *BoolCmd\n\tCFInfo(ctx context.Context, key string) *CFInfoCmd\n\tCFInsert(ctx context.Context, key string, options *CFInsertOptions, elements ...interface{}) *BoolSliceCmd\n\tCFInsertNX(ctx context.Context, key string, options *CFInsertOptions, elements ...interface{}) *IntSliceCmd\n\tCFMExists(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd\n\tCFReserve(ctx context.Context, key string, capacity int64) *StatusCmd\n\tCFReserveWithArgs(ctx context.Context, key string, options *CFReserveOptions) *StatusCmd\n\tCFReserveExpansion(ctx context.Context, key string, capacity int64, expansion int64) *StatusCmd\n\tCFReserveBucketSize(ctx context.Context, key string, capacity int64, bucketsize int64) *StatusCmd\n\tCFReserveMaxIterations(ctx context.Context, key string, capacity int64, maxiterations int64) *StatusCmd\n\tCFScanDump(ctx context.Context, key string, iterator int64) *ScanDumpCmd\n\tCFLoadChunk(ctx context.Context, key string, iterator int64, data interface{}) *StatusCmd\n\n\tCMSIncrBy(ctx context.Context, key string, elements ...interface{}) *IntSliceCmd\n\tCMSInfo(ctx context.Context, key string) *CMSInfoCmd\n\tCMSInitByDim(ctx context.Context, key string, width, height int64) *StatusCmd\n\tCMSInitByProb(ctx context.Context, key string, errorRate, probability float64) *StatusCmd\n\tCMSMerge(ctx context.Context, destKey string, sourceKeys ...string) *StatusCmd\n\tCMSMergeWithWeight(ctx context.Context, destKey string, sourceKeys map[string]int64) *StatusCmd\n\tCMSQuery(ctx context.Context, key string, elements ...interface{}) *IntSliceCmd\n\n\tTopKAdd(ctx context.Context, key string, elements ...interface{}) *StringSliceCmd\n\tTopKCount(ctx context.Context, key string, elements ...interface{}) *IntSliceCmd\n\tTopKIncrBy(ctx context.Context, key string, elements ...interface{}) *StringSliceCmd\n\tTopKInfo(ctx context.Context, key string) *TopKInfoCmd\n\tTopKList(ctx context.Context, key string) *StringSliceCmd\n\tTopKListWithCount(ctx context.Context, key string) *MapStringIntCmd\n\tTopKQuery(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd\n\tTopKReserve(ctx context.Context, key string, k int64) *StatusCmd\n\tTopKReserveWithOptions(ctx context.Context, key string, k int64, width, depth int64, decay float64) *StatusCmd\n\n\tTDigestAdd(ctx context.Context, key string, elements ...float64) *StatusCmd\n\tTDigestByRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd\n\tTDigestByRevRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd\n\tTDigestCDF(ctx context.Context, key string, elements ...float64) *FloatSliceCmd\n\tTDigestCreate(ctx context.Context, key string) *StatusCmd\n\tTDigestCreateWithCompression(ctx context.Context, key string, compression int64) *StatusCmd\n\tTDigestInfo(ctx context.Context, key string) *TDigestInfoCmd\n\tTDigestMax(ctx context.Context, key string) *FloatCmd\n\tTDigestMin(ctx context.Context, key string) *FloatCmd\n\tTDigestMerge(ctx context.Context, destKey string, options *TDigestMergeOptions, sourceKeys ...string) *StatusCmd\n\tTDigestQuantile(ctx context.Context, key string, elements ...float64) *FloatSliceCmd\n\tTDigestRank(ctx context.Context, key string, values ...float64) *IntSliceCmd\n\tTDigestReset(ctx context.Context, key string) *StatusCmd\n\tTDigestRevRank(ctx context.Context, key string, values ...float64) *IntSliceCmd\n\tTDigestTrimmedMean(ctx context.Context, key string, lowCutQuantile, highCutQuantile float64) *FloatCmd\n}\n\ntype BFInsertOptions struct {\n\tCapacity   int64\n\tError      float64\n\tExpansion  int64\n\tNonScaling bool\n\tNoCreate   bool\n}\n\ntype BFReserveOptions struct {\n\tCapacity   int64\n\tError      float64\n\tExpansion  int64\n\tNonScaling bool\n}\n\ntype CFReserveOptions struct {\n\tCapacity      int64\n\tBucketSize    int64\n\tMaxIterations int64\n\tExpansion     int64\n}\n\ntype CFInsertOptions struct {\n\tCapacity int64\n\tNoCreate bool\n}\n\n// -------------------------------------------\n// Bloom filter commands\n//-------------------------------------------\n\n// BFReserve creates an empty Bloom filter with a single sub-filter\n// for the initial specified capacity and with an upper bound error_rate.\n// For more information - https://redis.io/commands/bf.reserve/\nfunc (c cmdable) BFReserve(ctx context.Context, key string, errorRate float64, capacity int64) *StatusCmd {\n\targs := []interface{}{\"BF.RESERVE\", key, errorRate, capacity}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFReserveExpansion creates an empty Bloom filter with a single sub-filter\n// for the initial specified capacity and with an upper bound error_rate.\n// This function also allows for specifying an expansion rate for the filter.\n// For more information - https://redis.io/commands/bf.reserve/\nfunc (c cmdable) BFReserveExpansion(ctx context.Context, key string, errorRate float64, capacity, expansion int64) *StatusCmd {\n\targs := []interface{}{\"BF.RESERVE\", key, errorRate, capacity, \"EXPANSION\", expansion}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFReserveNonScaling creates an empty Bloom filter with a single sub-filter\n// for the initial specified capacity and with an upper bound error_rate.\n// This function also allows for specifying that the filter should not scale.\n// For more information - https://redis.io/commands/bf.reserve/\nfunc (c cmdable) BFReserveNonScaling(ctx context.Context, key string, errorRate float64, capacity int64) *StatusCmd {\n\targs := []interface{}{\"BF.RESERVE\", key, errorRate, capacity, \"NONSCALING\"}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFReserveWithArgs creates an empty Bloom filter with a single sub-filter\n// for the initial specified capacity and with an upper bound error_rate.\n// This function also allows for specifying additional options such as expansion rate and non-scaling behavior.\n// For more information - https://redis.io/commands/bf.reserve/\nfunc (c cmdable) BFReserveWithArgs(ctx context.Context, key string, options *BFReserveOptions) *StatusCmd {\n\targs := []interface{}{\"BF.RESERVE\", key}\n\tif options != nil {\n\t\targs = append(args, options.Error, options.Capacity)\n\t\tif options.Expansion != 0 {\n\t\t\targs = append(args, \"EXPANSION\", options.Expansion)\n\t\t}\n\t\tif options.NonScaling {\n\t\t\targs = append(args, \"NONSCALING\")\n\t\t}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFAdd adds an item to a Bloom filter.\n// For more information - https://redis.io/commands/bf.add/\nfunc (c cmdable) BFAdd(ctx context.Context, key string, element interface{}) *BoolCmd {\n\targs := []interface{}{\"BF.ADD\", key, element}\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFCard returns the cardinality of a Bloom filter -\n// number of items that were added to a Bloom filter and detected as unique\n// (items that caused at least one bit to be set in at least one sub-filter).\n// For more information - https://redis.io/commands/bf.card/\nfunc (c cmdable) BFCard(ctx context.Context, key string) *IntCmd {\n\targs := []interface{}{\"BF.CARD\", key}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFExists determines whether a given item was added to a Bloom filter.\n// For more information - https://redis.io/commands/bf.exists/\nfunc (c cmdable) BFExists(ctx context.Context, key string, element interface{}) *BoolCmd {\n\targs := []interface{}{\"BF.EXISTS\", key, element}\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFLoadChunk restores a Bloom filter previously saved using BF.SCANDUMP.\n// For more information - https://redis.io/commands/bf.loadchunk/\nfunc (c cmdable) BFLoadChunk(ctx context.Context, key string, iterator int64, data interface{}) *StatusCmd {\n\targs := []interface{}{\"BF.LOADCHUNK\", key, iterator, data}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Begins an incremental save of the Bloom filter.\n// This command is useful for large Bloom filters that cannot fit into the DUMP and RESTORE model.\n// For more information - https://redis.io/commands/bf.scandump/\nfunc (c cmdable) BFScanDump(ctx context.Context, key string, iterator int64) *ScanDumpCmd {\n\targs := []interface{}{\"BF.SCANDUMP\", key, iterator}\n\tcmd := newScanDumpCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype ScanDump struct {\n\tIter int64\n\tData string\n}\n\ntype ScanDumpCmd struct {\n\tbaseCmd\n\n\tval ScanDump\n}\n\nfunc newScanDumpCmd(ctx context.Context, args ...interface{}) *ScanDumpCmd {\n\treturn &ScanDumpCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeScanDump,\n\t\t},\n\t}\n}\n\nfunc (cmd *ScanDumpCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *ScanDumpCmd) SetVal(val ScanDump) {\n\tcmd.val = val\n}\n\nfunc (cmd *ScanDumpCmd) Result() (ScanDump, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *ScanDumpCmd) Val() ScanDump {\n\treturn cmd.val\n}\n\nfunc (cmd *ScanDumpCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = ScanDump{}\n\tfor i := 0; i < n; i++ {\n\t\titer, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val.Data = data\n\t\tcmd.val.Iter = iter\n\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *ScanDumpCmd) Clone() Cmder {\n\treturn &ScanDumpCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // ScanDump is a simple struct, can be copied directly\n\t}\n}\n\n// Returns information about a Bloom filter.\n// For more information - https://redis.io/commands/bf.info/\nfunc (c cmdable) BFInfo(ctx context.Context, key string) *BFInfoCmd {\n\targs := []interface{}{\"BF.INFO\", key}\n\tcmd := NewBFInfoCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype BFInfo struct {\n\tCapacity      int64\n\tSize          int64\n\tFilters       int64\n\tItemsInserted int64\n\tExpansionRate int64\n}\n\ntype BFInfoCmd struct {\n\tbaseCmd\n\n\tval BFInfo\n}\n\nfunc NewBFInfoCmd(ctx context.Context, args ...interface{}) *BFInfoCmd {\n\treturn &BFInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeBFInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *BFInfoCmd) SetVal(val BFInfo) {\n\tcmd.val = val\n}\n\nfunc (cmd *BFInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *BFInfoCmd) Val() BFInfo {\n\treturn cmd.val\n}\n\nfunc (cmd *BFInfoCmd) Result() (BFInfo, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {\n\tresult := BFInfo{}\n\n\t// Create a mapping from key names to pointers of struct fields\n\trespMapping := map[string]*int64{\n\t\t\"Capacity\":                 &result.Capacity,\n\t\t\"CAPACITY\":                 &result.Capacity,\n\t\t\"Size\":                     &result.Size,\n\t\t\"SIZE\":                     &result.Size,\n\t\t\"Number of filters\":        &result.Filters,\n\t\t\"FILTERS\":                  &result.Filters,\n\t\t\"Number of items inserted\": &result.ItemsInserted,\n\t\t\"ITEMS\":                    &result.ItemsInserted,\n\t\t\"Expansion rate\":           &result.ExpansionRate,\n\t\t\"EXPANSION\":                &result.ExpansionRate,\n\t}\n\n\t// Helper function to read and assign a value based on the key\n\treadAndAssignValue := func(key string) error {\n\t\tfieldPtr, exists := respMapping[key]\n\t\tif !exists {\n\t\t\treturn fmt.Errorf(\"redis: BLOOM.INFO unexpected key %s\", key)\n\t\t}\n\n\t\t// Read the integer and assign to the field via pointer dereferencing\n\t\tval, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*fieldPtr = val\n\t\treturn nil\n\t}\n\n\treadType, err := rd.PeekReplyType()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(cmd.args) > 2 && readType == proto.RespArray {\n\t\tn, err := rd.ReadArrayLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif key, ok := cmd.args[2].(string); ok && n == 1 {\n\t\t\tif err := readAndAssignValue(key); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"redis: BLOOM.INFO invalid argument key type\")\n\t\t}\n\t} else {\n\t\tn, err := rd.ReadMapLen()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i := 0; i < n; i++ {\n\t\t\tkey, err := rd.ReadString()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := readAndAssignValue(key); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tcmd.val = result\n\treturn nil\n}\n\nfunc (cmd *BFInfoCmd) Clone() Cmder {\n\treturn &BFInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // BFInfo is a simple struct, can be copied directly\n\t}\n}\n\n// BFInfoCapacity returns information about the capacity of a Bloom filter.\n// For more information - https://redis.io/commands/bf.info/\nfunc (c cmdable) BFInfoCapacity(ctx context.Context, key string) *BFInfoCmd {\n\treturn c.BFInfoArg(ctx, key, \"CAPACITY\")\n}\n\n// BFInfoSize returns information about the size of a Bloom filter.\n// For more information - https://redis.io/commands/bf.info/\nfunc (c cmdable) BFInfoSize(ctx context.Context, key string) *BFInfoCmd {\n\treturn c.BFInfoArg(ctx, key, \"SIZE\")\n}\n\n// BFInfoFilters returns information about the filters of a Bloom filter.\n// For more information - https://redis.io/commands/bf.info/\nfunc (c cmdable) BFInfoFilters(ctx context.Context, key string) *BFInfoCmd {\n\treturn c.BFInfoArg(ctx, key, \"FILTERS\")\n}\n\n// BFInfoItems returns information about the items of a Bloom filter.\n// For more information - https://redis.io/commands/bf.info/\nfunc (c cmdable) BFInfoItems(ctx context.Context, key string) *BFInfoCmd {\n\treturn c.BFInfoArg(ctx, key, \"ITEMS\")\n}\n\n// BFInfoExpansion returns information about the expansion rate of a Bloom filter.\n// For more information - https://redis.io/commands/bf.info/\nfunc (c cmdable) BFInfoExpansion(ctx context.Context, key string) *BFInfoCmd {\n\treturn c.BFInfoArg(ctx, key, \"EXPANSION\")\n}\n\n// BFInfoArg returns information about a specific option of a Bloom filter.\n// For more information - https://redis.io/commands/bf.info/\nfunc (c cmdable) BFInfoArg(ctx context.Context, key, option string) *BFInfoCmd {\n\targs := []interface{}{\"BF.INFO\", key, option}\n\tcmd := NewBFInfoCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFInsert inserts elements into a Bloom filter.\n// This function also allows for specifying additional options such as:\n// capacity, error rate, expansion rate, and non-scaling behavior.\n// For more information - https://redis.io/commands/bf.insert/\nfunc (c cmdable) BFInsert(ctx context.Context, key string, options *BFInsertOptions, elements ...interface{}) *BoolSliceCmd {\n\targs := []interface{}{\"BF.INSERT\", key}\n\tif options != nil {\n\t\tif options.Capacity != 0 {\n\t\t\targs = append(args, \"CAPACITY\", options.Capacity)\n\t\t}\n\t\tif options.Error != 0 {\n\t\t\targs = append(args, \"ERROR\", options.Error)\n\t\t}\n\t\tif options.Expansion != 0 {\n\t\t\targs = append(args, \"EXPANSION\", options.Expansion)\n\t\t}\n\t\tif options.NoCreate {\n\t\t\targs = append(args, \"NOCREATE\")\n\t\t}\n\t\tif options.NonScaling {\n\t\t\targs = append(args, \"NONSCALING\")\n\t\t}\n\t}\n\targs = append(args, \"ITEMS\")\n\targs = append(args, elements...)\n\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFMAdd adds multiple elements to a Bloom filter.\n// Returns an array of booleans indicating whether each element was added to the filter or not.\n// For more information - https://redis.io/commands/bf.madd/\nfunc (c cmdable) BFMAdd(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd {\n\targs := []interface{}{\"BF.MADD\", key}\n\targs = append(args, elements...)\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BFMExists check if multiple elements exist in a Bloom filter.\n// Returns an array of booleans indicating whether each element exists in the filter or not.\n// For more information - https://redis.io/commands/bf.mexists/\nfunc (c cmdable) BFMExists(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd {\n\targs := []interface{}{\"BF.MEXISTS\", key}\n\targs = append(args, elements...)\n\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// -------------------------------------------\n// Cuckoo filter commands\n//-------------------------------------------\n\n// CFReserve creates an empty Cuckoo filter with the specified capacity.\n// For more information - https://redis.io/commands/cf.reserve/\nfunc (c cmdable) CFReserve(ctx context.Context, key string, capacity int64) *StatusCmd {\n\targs := []interface{}{\"CF.RESERVE\", key, capacity}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFReserveExpansion creates an empty Cuckoo filter with the specified capacity and expansion rate.\n// For more information - https://redis.io/commands/cf.reserve/\nfunc (c cmdable) CFReserveExpansion(ctx context.Context, key string, capacity int64, expansion int64) *StatusCmd {\n\targs := []interface{}{\"CF.RESERVE\", key, capacity, \"EXPANSION\", expansion}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFReserveBucketSize creates an empty Cuckoo filter with the specified capacity and bucket size.\n// For more information - https://redis.io/commands/cf.reserve/\nfunc (c cmdable) CFReserveBucketSize(ctx context.Context, key string, capacity int64, bucketsize int64) *StatusCmd {\n\targs := []interface{}{\"CF.RESERVE\", key, capacity, \"BUCKETSIZE\", bucketsize}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFReserveMaxIterations creates an empty Cuckoo filter with the specified capacity and maximum number of iterations.\n// For more information - https://redis.io/commands/cf.reserve/\nfunc (c cmdable) CFReserveMaxIterations(ctx context.Context, key string, capacity int64, maxiterations int64) *StatusCmd {\n\targs := []interface{}{\"CF.RESERVE\", key, capacity, \"MAXITERATIONS\", maxiterations}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFReserveWithArgs creates an empty Cuckoo filter with the specified options.\n// This function allows for specifying additional options such as bucket size and maximum number of iterations.\n// For more information - https://redis.io/commands/cf.reserve/\nfunc (c cmdable) CFReserveWithArgs(ctx context.Context, key string, options *CFReserveOptions) *StatusCmd {\n\targs := []interface{}{\"CF.RESERVE\", key, options.Capacity}\n\tif options.BucketSize != 0 {\n\t\targs = append(args, \"BUCKETSIZE\", options.BucketSize)\n\t}\n\tif options.MaxIterations != 0 {\n\t\targs = append(args, \"MAXITERATIONS\", options.MaxIterations)\n\t}\n\tif options.Expansion != 0 {\n\t\targs = append(args, \"EXPANSION\", options.Expansion)\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFAdd adds an element to a Cuckoo filter.\n// Returns true if the element was added to the filter or false if it already exists in the filter.\n// For more information - https://redis.io/commands/cf.add/\nfunc (c cmdable) CFAdd(ctx context.Context, key string, element interface{}) *BoolCmd {\n\targs := []interface{}{\"CF.ADD\", key, element}\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFAddNX adds an element to a Cuckoo filter only if it does not already exist in the filter.\n// Returns true if the element was added to the filter or false if it already exists in the filter.\n// For more information - https://redis.io/commands/cf.addnx/\nfunc (c cmdable) CFAddNX(ctx context.Context, key string, element interface{}) *BoolCmd {\n\targs := []interface{}{\"CF.ADDNX\", key, element}\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFCount returns an estimate of the number of times an element may be in a Cuckoo Filter.\n// For more information - https://redis.io/commands/cf.count/\nfunc (c cmdable) CFCount(ctx context.Context, key string, element interface{}) *IntCmd {\n\targs := []interface{}{\"CF.COUNT\", key, element}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFDel deletes an item once from the cuckoo filter.\n// For more information - https://redis.io/commands/cf.del/\nfunc (c cmdable) CFDel(ctx context.Context, key string, element interface{}) *BoolCmd {\n\targs := []interface{}{\"CF.DEL\", key, element}\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFExists determines whether an item may exist in the Cuckoo Filter or not.\n// For more information - https://redis.io/commands/cf.exists/\nfunc (c cmdable) CFExists(ctx context.Context, key string, element interface{}) *BoolCmd {\n\targs := []interface{}{\"CF.EXISTS\", key, element}\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFLoadChunk restores a filter previously saved using SCANDUMP.\n// For more information - https://redis.io/commands/cf.loadchunk/\nfunc (c cmdable) CFLoadChunk(ctx context.Context, key string, iterator int64, data interface{}) *StatusCmd {\n\targs := []interface{}{\"CF.LOADCHUNK\", key, iterator, data}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFScanDump begins an incremental save of the cuckoo filter.\n// For more information - https://redis.io/commands/cf.scandump/\nfunc (c cmdable) CFScanDump(ctx context.Context, key string, iterator int64) *ScanDumpCmd {\n\targs := []interface{}{\"CF.SCANDUMP\", key, iterator}\n\tcmd := newScanDumpCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype CFInfo struct {\n\tSize             int64\n\tNumBuckets       int64\n\tNumFilters       int64\n\tNumItemsInserted int64\n\tNumItemsDeleted  int64\n\tBucketSize       int64\n\tExpansionRate    int64\n\tMaxIteration     int64\n}\n\ntype CFInfoCmd struct {\n\tbaseCmd\n\n\tval CFInfo\n}\n\nfunc NewCFInfoCmd(ctx context.Context, args ...interface{}) *CFInfoCmd {\n\treturn &CFInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeCFInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *CFInfoCmd) SetVal(val CFInfo) {\n\tcmd.val = val\n}\n\nfunc (cmd *CFInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *CFInfoCmd) Val() CFInfo {\n\treturn cmd.val\n}\n\nfunc (cmd *CFInfoCmd) Result() (CFInfo, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *CFInfoCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar key string\n\tvar result CFInfo\n\tfor f := 0; f < n; f++ {\n\t\tkey, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"Size\":\n\t\t\tresult.Size, err = rd.ReadInt()\n\t\tcase \"Number of buckets\":\n\t\t\tresult.NumBuckets, err = rd.ReadInt()\n\t\tcase \"Number of filters\":\n\t\t\tresult.NumFilters, err = rd.ReadInt()\n\t\tcase \"Number of items inserted\":\n\t\t\tresult.NumItemsInserted, err = rd.ReadInt()\n\t\tcase \"Number of items deleted\":\n\t\t\tresult.NumItemsDeleted, err = rd.ReadInt()\n\t\tcase \"Bucket size\":\n\t\t\tresult.BucketSize, err = rd.ReadInt()\n\t\tcase \"Expansion rate\":\n\t\t\tresult.ExpansionRate, err = rd.ReadInt()\n\t\tcase \"Max iterations\":\n\t\t\tresult.MaxIteration, err = rd.ReadInt()\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: CF.INFO unexpected key %s\", key)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcmd.val = result\n\treturn nil\n}\n\nfunc (cmd *CFInfoCmd) Clone() Cmder {\n\treturn &CFInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // CFInfo is a simple struct, can be copied directly\n\t}\n}\n\n// CFInfo returns information about a Cuckoo filter.\n// For more information - https://redis.io/commands/cf.info/\nfunc (c cmdable) CFInfo(ctx context.Context, key string) *CFInfoCmd {\n\targs := []interface{}{\"CF.INFO\", key}\n\tcmd := NewCFInfoCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFInsert inserts elements into a Cuckoo filter.\n// This function also allows for specifying additional options such as capacity, error rate, expansion rate, and non-scaling behavior.\n// Returns an array of booleans indicating whether each element was added to the filter or not.\n// For more information - https://redis.io/commands/cf.insert/\nfunc (c cmdable) CFInsert(ctx context.Context, key string, options *CFInsertOptions, elements ...interface{}) *BoolSliceCmd {\n\targs := []interface{}{\"CF.INSERT\", key}\n\targs = c.getCfInsertWithArgs(args, options, elements...)\n\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CFInsertNX inserts elements into a Cuckoo filter only if they do not already exist in the filter.\n// This function also allows for specifying additional options such as:\n// capacity, error rate, expansion rate, and non-scaling behavior.\n// Returns an array of integers indicating whether each element was added to the filter or not.\n// For more information - https://redis.io/commands/cf.insertnx/\nfunc (c cmdable) CFInsertNX(ctx context.Context, key string, options *CFInsertOptions, elements ...interface{}) *IntSliceCmd {\n\targs := []interface{}{\"CF.INSERTNX\", key}\n\targs = c.getCfInsertWithArgs(args, options, elements...)\n\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) getCfInsertWithArgs(args []interface{}, options *CFInsertOptions, elements ...interface{}) []interface{} {\n\tif options != nil {\n\t\tif options.Capacity != 0 {\n\t\t\targs = append(args, \"CAPACITY\", options.Capacity)\n\t\t}\n\t\tif options.NoCreate {\n\t\t\targs = append(args, \"NOCREATE\")\n\t\t}\n\t}\n\targs = append(args, \"ITEMS\")\n\targs = append(args, elements...)\n\n\treturn args\n}\n\n// CFMExists check if multiple elements exist in a Cuckoo filter.\n// Returns an array of booleans indicating whether each element exists in the filter or not.\n// For more information - https://redis.io/commands/cf.mexists/\nfunc (c cmdable) CFMExists(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd {\n\targs := []interface{}{\"CF.MEXISTS\", key}\n\targs = append(args, elements...)\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// -------------------------------------------\n// CMS commands\n//-------------------------------------------\n\n// CMSIncrBy increments the count of one or more items in a Count-Min Sketch filter.\n// Returns an array of integers representing the updated count of each item.\n// For more information - https://redis.io/commands/cms.incrby/\nfunc (c cmdable) CMSIncrBy(ctx context.Context, key string, elements ...interface{}) *IntSliceCmd {\n\targs := make([]interface{}, 2, 2+len(elements))\n\targs[0] = \"CMS.INCRBY\"\n\targs[1] = key\n\targs = appendArgs(args, elements)\n\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype CMSInfo struct {\n\tWidth int64\n\tDepth int64\n\tCount int64\n}\n\ntype CMSInfoCmd struct {\n\tbaseCmd\n\n\tval CMSInfo\n}\n\nfunc NewCMSInfoCmd(ctx context.Context, args ...interface{}) *CMSInfoCmd {\n\treturn &CMSInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeCMSInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *CMSInfoCmd) SetVal(val CMSInfo) {\n\tcmd.val = val\n}\n\nfunc (cmd *CMSInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *CMSInfoCmd) Val() CMSInfo {\n\treturn cmd.val\n}\n\nfunc (cmd *CMSInfoCmd) Result() (CMSInfo, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *CMSInfoCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar key string\n\tvar result CMSInfo\n\tfor f := 0; f < n; f++ {\n\t\tkey, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"width\":\n\t\t\tresult.Width, err = rd.ReadInt()\n\t\tcase \"depth\":\n\t\t\tresult.Depth, err = rd.ReadInt()\n\t\tcase \"count\":\n\t\t\tresult.Count, err = rd.ReadInt()\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: CMS.INFO unexpected key %s\", key)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcmd.val = result\n\treturn nil\n}\n\nfunc (cmd *CMSInfoCmd) Clone() Cmder {\n\treturn &CMSInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // CMSInfo is a simple struct, can be copied directly\n\t}\n}\n\n// CMSInfo returns information about a Count-Min Sketch filter.\n// For more information - https://redis.io/commands/cms.info/\nfunc (c cmdable) CMSInfo(ctx context.Context, key string) *CMSInfoCmd {\n\targs := []interface{}{\"CMS.INFO\", key}\n\tcmd := NewCMSInfoCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CMSInitByDim creates an empty Count-Min Sketch filter with the specified dimensions.\n// For more information - https://redis.io/commands/cms.initbydim/\nfunc (c cmdable) CMSInitByDim(ctx context.Context, key string, width, depth int64) *StatusCmd {\n\targs := []interface{}{\"CMS.INITBYDIM\", key, width, depth}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CMSInitByProb creates an empty Count-Min Sketch filter with the specified error rate and probability.\n// For more information - https://redis.io/commands/cms.initbyprob/\nfunc (c cmdable) CMSInitByProb(ctx context.Context, key string, errorRate, probability float64) *StatusCmd {\n\targs := []interface{}{\"CMS.INITBYPROB\", key, errorRate, probability}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CMSMerge merges multiple Count-Min Sketch filters into a single filter.\n// The destination filter must not exist and will be created with the dimensions of the first source filter.\n// The number of items in each source filter must be equal.\n// Returns OK on success or an error if the filters could not be merged.\n// For more information - https://redis.io/commands/cms.merge/\nfunc (c cmdable) CMSMerge(ctx context.Context, destKey string, sourceKeys ...string) *StatusCmd {\n\targs := []interface{}{\"CMS.MERGE\", destKey, len(sourceKeys)}\n\tfor _, s := range sourceKeys {\n\t\targs = append(args, s)\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CMSMergeWithWeight merges multiple Count-Min Sketch filters into a single filter with weights for each source filter.\n// The destination filter must not exist and will be created with the dimensions of the first source filter.\n// The number of items in each source filter must be equal.\n// Returns OK on success or an error if the filters could not be merged.\n// For more information - https://redis.io/commands/cms.merge/\nfunc (c cmdable) CMSMergeWithWeight(ctx context.Context, destKey string, sourceKeys map[string]int64) *StatusCmd {\n\targs := make([]interface{}, 0, 4+(len(sourceKeys)*2+1))\n\targs = append(args, \"CMS.MERGE\", destKey, len(sourceKeys))\n\n\tif len(sourceKeys) > 0 {\n\t\tsk := make([]interface{}, len(sourceKeys))\n\t\tsw := make([]interface{}, len(sourceKeys))\n\n\t\ti := 0\n\t\tfor k, w := range sourceKeys {\n\t\t\tsk[i] = k\n\t\t\tsw[i] = w\n\t\t\ti++\n\t\t}\n\n\t\targs = append(args, sk...)\n\t\targs = append(args, \"WEIGHTS\")\n\t\targs = append(args, sw...)\n\t}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// CMSQuery returns count for item(s).\n// For more information - https://redis.io/commands/cms.query/\nfunc (c cmdable) CMSQuery(ctx context.Context, key string, elements ...interface{}) *IntSliceCmd {\n\targs := []interface{}{\"CMS.QUERY\", key}\n\targs = append(args, elements...)\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// -------------------------------------------\n// TopK commands\n//--------------------------------------------\n\n// TopKAdd adds one or more elements to a Top-K filter.\n// Returns an array of strings representing the items that were removed from the filter, if any.\n// For more information - https://redis.io/commands/topk.add/\nfunc (c cmdable) TopKAdd(ctx context.Context, key string, elements ...interface{}) *StringSliceCmd {\n\targs := make([]interface{}, 2, 2+len(elements))\n\targs[0] = \"TOPK.ADD\"\n\targs[1] = key\n\targs = appendArgs(args, elements)\n\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TopKReserve creates an empty Top-K filter with the specified number of top items to keep.\n// For more information - https://redis.io/commands/topk.reserve/\nfunc (c cmdable) TopKReserve(ctx context.Context, key string, k int64) *StatusCmd {\n\targs := []interface{}{\"TOPK.RESERVE\", key, k}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TopKReserveWithOptions creates an empty Top-K filter with the specified number of top items to keep and additional options.\n// This function allows for specifying additional options such as width, depth and decay.\n// For more information - https://redis.io/commands/topk.reserve/\nfunc (c cmdable) TopKReserveWithOptions(ctx context.Context, key string, k int64, width, depth int64, decay float64) *StatusCmd {\n\targs := []interface{}{\"TOPK.RESERVE\", key, k, width, depth, decay}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype TopKInfo struct {\n\tK     int64\n\tWidth int64\n\tDepth int64\n\tDecay float64\n}\n\ntype TopKInfoCmd struct {\n\tbaseCmd\n\n\tval TopKInfo\n}\n\nfunc NewTopKInfoCmd(ctx context.Context, args ...interface{}) *TopKInfoCmd {\n\treturn &TopKInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeTopKInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *TopKInfoCmd) SetVal(val TopKInfo) {\n\tcmd.val = val\n}\n\nfunc (cmd *TopKInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *TopKInfoCmd) Val() TopKInfo {\n\treturn cmd.val\n}\n\nfunc (cmd *TopKInfoCmd) Result() (TopKInfo, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *TopKInfoCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar key string\n\tvar result TopKInfo\n\tfor f := 0; f < n; f++ {\n\t\tkey, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"k\":\n\t\t\tresult.K, err = rd.ReadInt()\n\t\tcase \"width\":\n\t\t\tresult.Width, err = rd.ReadInt()\n\t\tcase \"depth\":\n\t\t\tresult.Depth, err = rd.ReadInt()\n\t\tcase \"decay\":\n\t\t\tresult.Decay, err = rd.ReadFloat()\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: topk.info unexpected key %s\", key)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcmd.val = result\n\treturn nil\n}\n\nfunc (cmd *TopKInfoCmd) Clone() Cmder {\n\treturn &TopKInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // TopKInfo is a simple struct, can be copied directly\n\t}\n}\n\n// TopKInfo returns information about a Top-K filter.\n// For more information - https://redis.io/commands/topk.info/\nfunc (c cmdable) TopKInfo(ctx context.Context, key string) *TopKInfoCmd {\n\targs := []interface{}{\"TOPK.INFO\", key}\n\n\tcmd := NewTopKInfoCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TopKQuery check if multiple elements exist in a Top-K filter.\n// Returns an array of booleans indicating whether each element exists in the filter or not.\n// For more information - https://redis.io/commands/topk.query/\nfunc (c cmdable) TopKQuery(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd {\n\targs := make([]interface{}, 2, 2+len(elements))\n\targs[0] = \"TOPK.QUERY\"\n\targs[1] = key\n\targs = appendArgs(args, elements)\n\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TopKCount returns an estimate of the number of times an item may be in a Top-K filter.\n// For more information - https://redis.io/commands/topk.count/\nfunc (c cmdable) TopKCount(ctx context.Context, key string, elements ...interface{}) *IntSliceCmd {\n\targs := make([]interface{}, 2, 2+len(elements))\n\targs[0] = \"TOPK.COUNT\"\n\targs[1] = key\n\targs = appendArgs(args, elements)\n\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TopKIncrBy increases the count of one or more items in a Top-K filter.\n// For more information - https://redis.io/commands/topk.incrby/\nfunc (c cmdable) TopKIncrBy(ctx context.Context, key string, elements ...interface{}) *StringSliceCmd {\n\targs := make([]interface{}, 2, 2+len(elements))\n\targs[0] = \"TOPK.INCRBY\"\n\targs[1] = key\n\targs = appendArgs(args, elements)\n\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TopKList returns all items in Top-K list.\n// For more information - https://redis.io/commands/topk.list/\nfunc (c cmdable) TopKList(ctx context.Context, key string) *StringSliceCmd {\n\targs := []interface{}{\"TOPK.LIST\", key}\n\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TopKListWithCount returns all items in Top-K list with their respective count.\n// For more information - https://redis.io/commands/topk.list/\nfunc (c cmdable) TopKListWithCount(ctx context.Context, key string) *MapStringIntCmd {\n\targs := []interface{}{\"TOPK.LIST\", key, \"WITHCOUNT\"}\n\n\tcmd := NewMapStringIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// -------------------------------------------\n// t-digest commands\n// --------------------------------------------\n\n// TDigestAdd adds one or more elements to a t-Digest data structure.\n// Returns OK on success or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.add/\nfunc (c cmdable) TDigestAdd(ctx context.Context, key string, elements ...float64) *StatusCmd {\n\targs := make([]interface{}, 2+len(elements))\n\targs[0] = \"TDIGEST.ADD\"\n\targs[1] = key\n\n\tfor i, v := range elements {\n\t\targs[2+i] = v\n\t}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestByRank returns an array of values from a t-Digest data structure based on their rank.\n// The rank of an element is its position in the sorted list of all elements in the t-Digest.\n// Returns an array of floats representing the values at the specified ranks or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.byrank/\nfunc (c cmdable) TDigestByRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd {\n\targs := make([]interface{}, 2+len(rank))\n\targs[0] = \"TDIGEST.BYRANK\"\n\targs[1] = key\n\n\tfor i, r := range rank {\n\t\targs[2+i] = r\n\t}\n\n\tcmd := NewFloatSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestByRevRank returns an array of values from a t-Digest data structure based on their reverse rank.\n// The reverse rank of an element is its position in the sorted list of all elements in the t-Digest when sorted in descending order.\n// Returns an array of floats representing the values at the specified ranks or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.byrevrank/\nfunc (c cmdable) TDigestByRevRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd {\n\targs := make([]interface{}, 2+len(rank))\n\targs[0] = \"TDIGEST.BYREVRANK\"\n\targs[1] = key\n\n\tfor i, r := range rank {\n\t\targs[2+i] = r\n\t}\n\n\tcmd := NewFloatSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestCDF returns an array of cumulative distribution function (CDF) values for one or more elements in a t-Digest data structure.\n// The CDF value for an element is the fraction of all elements in the t-Digest that are less than or equal to it.\n// Returns an array of floats representing the CDF values for each element or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.cdf/\nfunc (c cmdable) TDigestCDF(ctx context.Context, key string, elements ...float64) *FloatSliceCmd {\n\targs := make([]interface{}, 2+len(elements))\n\targs[0] = \"TDIGEST.CDF\"\n\targs[1] = key\n\n\tfor i, v := range elements {\n\t\targs[2+i] = v\n\t}\n\n\tcmd := NewFloatSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestCreate creates an empty t-Digest data structure with default parameters.\n// Returns OK on success or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.create/\nfunc (c cmdable) TDigestCreate(ctx context.Context, key string) *StatusCmd {\n\targs := []interface{}{\"TDIGEST.CREATE\", key}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestCreateWithCompression creates an empty t-Digest data structure with a specified compression parameter.\n// The compression parameter controls the accuracy and memory usage of the t-Digest.\n// Returns OK on success or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.create/\nfunc (c cmdable) TDigestCreateWithCompression(ctx context.Context, key string, compression int64) *StatusCmd {\n\targs := []interface{}{\"TDIGEST.CREATE\", key, \"COMPRESSION\", compression}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype TDigestInfo struct {\n\tCompression       int64\n\tCapacity          int64\n\tMergedNodes       int64\n\tUnmergedNodes     int64\n\tMergedWeight      int64\n\tUnmergedWeight    int64\n\tObservations      int64\n\tTotalCompressions int64\n\tMemoryUsage       int64\n}\n\ntype TDigestInfoCmd struct {\n\tbaseCmd\n\n\tval TDigestInfo\n}\n\nfunc NewTDigestInfoCmd(ctx context.Context, args ...interface{}) *TDigestInfoCmd {\n\treturn &TDigestInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeTDigestInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *TDigestInfoCmd) SetVal(val TDigestInfo) {\n\tcmd.val = val\n}\n\nfunc (cmd *TDigestInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *TDigestInfoCmd) Val() TDigestInfo {\n\treturn cmd.val\n}\n\nfunc (cmd *TDigestInfoCmd) Result() (TDigestInfo, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *TDigestInfoCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar key string\n\tvar result TDigestInfo\n\tfor f := 0; f < n; f++ {\n\t\tkey, err = rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"Compression\":\n\t\t\tresult.Compression, err = rd.ReadInt()\n\t\tcase \"Capacity\":\n\t\t\tresult.Capacity, err = rd.ReadInt()\n\t\tcase \"Merged nodes\":\n\t\t\tresult.MergedNodes, err = rd.ReadInt()\n\t\tcase \"Unmerged nodes\":\n\t\t\tresult.UnmergedNodes, err = rd.ReadInt()\n\t\tcase \"Merged weight\":\n\t\t\tresult.MergedWeight, err = rd.ReadInt()\n\t\tcase \"Unmerged weight\":\n\t\t\tresult.UnmergedWeight, err = rd.ReadInt()\n\t\tcase \"Observations\":\n\t\t\tresult.Observations, err = rd.ReadInt()\n\t\tcase \"Total compressions\":\n\t\t\tresult.TotalCompressions, err = rd.ReadInt()\n\t\tcase \"Memory usage\":\n\t\t\tresult.MemoryUsage, err = rd.ReadInt()\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"redis: tdigest.info unexpected key %s\", key)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcmd.val = result\n\treturn nil\n}\n\nfunc (cmd *TDigestInfoCmd) Clone() Cmder {\n\treturn &TDigestInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // TDigestInfo is a simple struct, can be copied directly\n\t}\n}\n\n// TDigestInfo returns information about a t-Digest data structure.\n// For more information - https://redis.io/commands/tdigest.info/\nfunc (c cmdable) TDigestInfo(ctx context.Context, key string) *TDigestInfoCmd {\n\targs := []interface{}{\"TDIGEST.INFO\", key}\n\n\tcmd := NewTDigestInfoCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestMax returns the maximum value from a t-Digest data structure.\n// For more information - https://redis.io/commands/tdigest.max/\nfunc (c cmdable) TDigestMax(ctx context.Context, key string) *FloatCmd {\n\targs := []interface{}{\"TDIGEST.MAX\", key}\n\n\tcmd := NewFloatCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype TDigestMergeOptions struct {\n\tCompression int64\n\tOverride    bool\n}\n\n// TDigestMerge merges multiple t-Digest data structures into a single t-Digest.\n// This function also allows for specifying additional options such as compression and override behavior.\n// Returns OK on success or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.merge/\nfunc (c cmdable) TDigestMerge(ctx context.Context, destKey string, options *TDigestMergeOptions, sourceKeys ...string) *StatusCmd {\n\targs := []interface{}{\"TDIGEST.MERGE\", destKey, len(sourceKeys)}\n\n\tfor _, sourceKey := range sourceKeys {\n\t\targs = append(args, sourceKey)\n\t}\n\n\tif options != nil {\n\t\tif options.Compression != 0 {\n\t\t\targs = append(args, \"COMPRESSION\", options.Compression)\n\t\t}\n\t\tif options.Override {\n\t\t\targs = append(args, \"OVERRIDE\")\n\t\t}\n\t}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestMin returns the minimum value from a t-Digest data structure.\n// For more information - https://redis.io/commands/tdigest.min/\nfunc (c cmdable) TDigestMin(ctx context.Context, key string) *FloatCmd {\n\targs := []interface{}{\"TDIGEST.MIN\", key}\n\n\tcmd := NewFloatCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestQuantile returns an array of quantile values for one or more elements in a t-Digest data structure.\n// The quantile value for an element is the fraction of all elements in the t-Digest that are less than or equal to it.\n// Returns an array of floats representing the quantile values for each element or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.quantile/\nfunc (c cmdable) TDigestQuantile(ctx context.Context, key string, elements ...float64) *FloatSliceCmd {\n\targs := make([]interface{}, 2+len(elements))\n\targs[0] = \"TDIGEST.QUANTILE\"\n\targs[1] = key\n\n\tfor i, v := range elements {\n\t\targs[2+i] = v\n\t}\n\n\tcmd := NewFloatSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestRank returns an array of rank values for one or more elements in a t-Digest data structure.\n// The rank of an element is its position in the sorted list of all elements in the t-Digest.\n// Returns an array of integers representing the rank values for each element or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.rank/\nfunc (c cmdable) TDigestRank(ctx context.Context, key string, values ...float64) *IntSliceCmd {\n\targs := make([]interface{}, 2+len(values))\n\targs[0] = \"TDIGEST.RANK\"\n\targs[1] = key\n\n\tfor i, v := range values {\n\t\targs[i+2] = v\n\t}\n\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestReset resets a t-Digest data structure to its initial state.\n// Returns OK on success or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.reset/\nfunc (c cmdable) TDigestReset(ctx context.Context, key string) *StatusCmd {\n\targs := []interface{}{\"TDIGEST.RESET\", key}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestRevRank returns an array of reverse rank values for one or more elements in a t-Digest data structure.\n// The reverse rank of an element is its position in the sorted list of all elements in the t-Digest when sorted in descending order.\n// Returns an array of integers representing the reverse rank values for each element or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.revrank/\nfunc (c cmdable) TDigestRevRank(ctx context.Context, key string, values ...float64) *IntSliceCmd {\n\targs := make([]interface{}, 2+len(values))\n\targs[0] = \"TDIGEST.REVRANK\"\n\targs[1] = key\n\n\tfor i, v := range values {\n\t\targs[2+i] = v\n\t}\n\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TDigestTrimmedMean returns the trimmed mean value from a t-Digest data structure.\n// The trimmed mean is calculated by removing a specified fraction of the highest and lowest values from the t-Digest and then calculating the mean of the remaining values.\n// Returns a float representing the trimmed mean value or an error if the operation could not be completed.\n// For more information - https://redis.io/commands/tdigest.trimmed_mean/\nfunc (c cmdable) TDigestTrimmedMean(ctx context.Context, key string, lowCutQuantile, highCutQuantile float64) *FloatCmd {\n\targs := []interface{}{\"TDIGEST.TRIMMED_MEAN\", key, lowCutQuantile, highCutQuantile}\n\n\tcmd := NewFloatCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "probabilistic_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"Probabilistic commands\", Label(\"probabilistic\"), func() {\n\tctx := context.TODO()\n\n\tsetupRedisClient := func(protocolVersion int) *redis.Client {\n\t\treturn redis.NewClient(&redis.Options{\n\t\t\tAddr:     \"localhost:6379\",\n\t\t\tDB:       0,\n\t\t\tProtocol: protocolVersion,\n\t\t})\n\t}\n\n\tprotocols := []int{2, 3}\n\tfor _, protocol := range protocols {\n\t\tprotocol := protocol // capture loop variable for each context\n\n\t\tContext(fmt.Sprintf(\"with protocol version %d\", protocol), func() {\n\t\t\tvar client *redis.Client\n\n\t\t\tBeforeEach(func() {\n\t\t\t\tclient = setupRedisClient(protocol)\n\t\t\t\tExpect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())\n\t\t\t})\n\n\t\t\tAfterEach(func() {\n\t\t\t\tif client != nil {\n\t\t\t\t\tclient.FlushDB(ctx)\n\t\t\t\t\tclient.Close()\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tDescribe(\"bloom\", Label(\"bloom\"), func() {\n\t\t\t\tIt(\"should BFAdd\", Label(\"bloom\", \"bfadd\"), func() {\n\t\t\t\t\tresultAdd, err := client.BFAdd(ctx, \"testbf1\", 1).Result()\n\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultAdd).To(BeTrue())\n\n\t\t\t\t\tresultInfo, err := client.BFInfo(ctx, \"testbf1\").Result()\n\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{}))\n\t\t\t\t\tExpect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(1)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFCard\", Label(\"bloom\", \"bfcard\"), func() {\n\t\t\t\t\t// This is a probabilistic data structure, and it's not always guaranteed that we will get back\n\t\t\t\t\t// the exact number of inserted items, during hash collisions\n\t\t\t\t\t// But with such a low number of items (only 3),\n\t\t\t\t\t// the probability of a collision is very low, so we can expect to get back the exact number of items\n\t\t\t\t\t_, err := client.BFAdd(ctx, \"testbf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\t_, err = client.BFAdd(ctx, \"testbf1\", \"item2\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\t_, err = client.BFAdd(ctx, \"testbf1\", 3).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.BFCard(ctx, \"testbf1\").Result()\n\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeEquivalentTo(int64(3)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFExists\", Label(\"bloom\", \"bfexists\"), func() {\n\t\t\t\t\texists, err := client.BFExists(ctx, \"testbf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(exists).To(BeFalse())\n\n\t\t\t\t\t_, err = client.BFAdd(ctx, \"testbf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\texists, err = client.BFExists(ctx, \"testbf1\", \"item1\").Result()\n\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(exists).To(BeTrue())\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFInfo and BFReserve\", Label(\"bloom\", \"bfinfo\", \"bfreserve\"), func() {\n\t\t\t\t\terr := client.BFReserve(ctx, \"testbf1\", 0.001, 2000).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.BFInfo(ctx, \"testbf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeAssignableToTypeOf(redis.BFInfo{}))\n\t\t\t\t\tExpect(result.Capacity).To(BeEquivalentTo(int64(2000)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFInfoCapacity, BFInfoSize, BFInfoFilters, BFInfoItems, BFInfoExpansion, \", Label(\"bloom\", \"bfinfocapacity\", \"bfinfosize\", \"bfinfofilters\", \"bfinfoitems\", \"bfinfoexpansion\"), func() {\n\t\t\t\t\terr := client.BFReserve(ctx, \"testbf1\", 0.001, 2000).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.BFInfoCapacity(ctx, \"testbf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result.Capacity).To(BeEquivalentTo(int64(2000)))\n\n\t\t\t\t\tresult, err = client.BFInfoItems(ctx, \"testbf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result.ItemsInserted).To(BeEquivalentTo(int64(0)))\n\n\t\t\t\t\tresult, err = client.BFInfoSize(ctx, \"testbf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result.Size).To(BeEquivalentTo(int64(4056)))\n\n\t\t\t\t\terr = client.BFReserveExpansion(ctx, \"testbf2\", 0.001, 2000, 3).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err = client.BFInfoFilters(ctx, \"testbf2\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result.Filters).To(BeEquivalentTo(int64(1)))\n\n\t\t\t\t\tresult, err = client.BFInfoExpansion(ctx, \"testbf2\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result.ExpansionRate).To(BeEquivalentTo(int64(3)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFInsert\", Label(\"bloom\", \"bfinsert\"), func() {\n\t\t\t\t\toptions := &redis.BFInsertOptions{\n\t\t\t\t\t\tCapacity:   2000,\n\t\t\t\t\t\tError:      0.001,\n\t\t\t\t\t\tExpansion:  3,\n\t\t\t\t\t\tNonScaling: false,\n\t\t\t\t\t\tNoCreate:   true,\n\t\t\t\t\t}\n\n\t\t\t\t\t_, err := client.BFInsert(ctx, \"testbf1\", options, \"item1\").Result()\n\t\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\t\tExpect(err).To(MatchError(\"ERR not found\"))\n\n\t\t\t\t\toptions = &redis.BFInsertOptions{\n\t\t\t\t\t\tCapacity:   2000,\n\t\t\t\t\t\tError:      0.001,\n\t\t\t\t\t\tExpansion:  3,\n\t\t\t\t\t\tNonScaling: false,\n\t\t\t\t\t\tNoCreate:   false,\n\t\t\t\t\t}\n\n\t\t\t\t\tresultInsert, err := client.BFInsert(ctx, \"testbf1\", options, \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultInsert)).To(BeEquivalentTo(1))\n\n\t\t\t\t\texists, err := client.BFExists(ctx, \"testbf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(exists).To(BeTrue())\n\n\t\t\t\t\tresult, err := client.BFInfo(ctx, \"testbf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeAssignableToTypeOf(redis.BFInfo{}))\n\t\t\t\t\tExpect(result.Capacity).To(BeEquivalentTo(int64(2000)))\n\t\t\t\t\tExpect(result.ExpansionRate).To(BeEquivalentTo(int64(3)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFMAdd\", Label(\"bloom\", \"bfmadd\"), func() {\n\t\t\t\t\tresultAdd, err := client.BFMAdd(ctx, \"testbf1\", \"item1\", \"item2\", \"item3\").Result()\n\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultAdd)).To(Equal(3))\n\n\t\t\t\t\tresultInfo, err := client.BFInfo(ctx, \"testbf1\").Result()\n\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{}))\n\t\t\t\t\tExpect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(3)))\n\t\t\t\t\tresultAdd2, err := client.BFMAdd(ctx, \"testbf1\", \"item1\", \"item2\", \"item4\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultAdd2[0]).To(BeFalse())\n\t\t\t\t\tExpect(resultAdd2[1]).To(BeFalse())\n\t\t\t\t\tExpect(resultAdd2[2]).To(BeTrue())\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFMExists\", Label(\"bloom\", \"bfmexists\"), func() {\n\t\t\t\t\texist, err := client.BFMExists(ctx, \"testbf1\", \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(exist)).To(Equal(3))\n\t\t\t\t\tExpect(exist[0]).To(BeFalse())\n\t\t\t\t\tExpect(exist[1]).To(BeFalse())\n\t\t\t\t\tExpect(exist[2]).To(BeFalse())\n\n\t\t\t\t\t_, err = client.BFMAdd(ctx, \"testbf1\", \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\texist, err = client.BFMExists(ctx, \"testbf1\", \"item1\", \"item2\", \"item3\", \"item4\").Result()\n\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(exist)).To(Equal(4))\n\t\t\t\t\tExpect(exist[0]).To(BeTrue())\n\t\t\t\t\tExpect(exist[1]).To(BeTrue())\n\t\t\t\t\tExpect(exist[2]).To(BeTrue())\n\t\t\t\t\tExpect(exist[3]).To(BeFalse())\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFReserveExpansion\", Label(\"bloom\", \"bfreserveexpansion\"), func() {\n\t\t\t\t\terr := client.BFReserveExpansion(ctx, \"testbf1\", 0.001, 2000, 3).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.BFInfo(ctx, \"testbf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeAssignableToTypeOf(redis.BFInfo{}))\n\t\t\t\t\tExpect(result.Capacity).To(BeEquivalentTo(int64(2000)))\n\t\t\t\t\tExpect(result.ExpansionRate).To(BeEquivalentTo(int64(3)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFReserveNonScaling\", Label(\"bloom\", \"bfreservenonscaling\"), func() {\n\t\t\t\t\terr := client.BFReserveNonScaling(ctx, \"testbfns1\", 0.001, 1000).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\t_, err = client.BFInfo(ctx, \"testbfns1\").Result()\n\t\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFScanDump and BFLoadChunk\", Label(\"bloom\", \"bfscandump\", \"bfloadchunk\"), func() {\n\t\t\t\t\terr := client.BFReserve(ctx, \"testbfsd1\", 0.001, 3000).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\t\t\tclient.BFAdd(ctx, \"testbfsd1\", i)\n\t\t\t\t\t}\n\t\t\t\t\tinfBefore := client.BFInfoSize(ctx, \"testbfsd1\")\n\t\t\t\t\tfd := []redis.ScanDump{}\n\t\t\t\t\tsd, err := client.BFScanDump(ctx, \"testbfsd1\", 0).Result()\n\t\t\t\t\tfor {\n\t\t\t\t\t\tif sd.Iter == 0 {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\t\tfd = append(fd, sd)\n\t\t\t\t\t\tsd, err = client.BFScanDump(ctx, \"testbfsd1\", sd.Iter).Result()\n\t\t\t\t\t}\n\t\t\t\t\tclient.Del(ctx, \"testbfsd1\")\n\t\t\t\t\tfor _, e := range fd {\n\t\t\t\t\t\tclient.BFLoadChunk(ctx, \"testbfsd1\", e.Iter, e.Data)\n\t\t\t\t\t}\n\t\t\t\t\tinfAfter := client.BFInfoSize(ctx, \"testbfsd1\")\n\t\t\t\t\tExpect(infBefore).To(BeEquivalentTo(infAfter))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should BFReserveWithArgs\", Label(\"bloom\", \"bfreserveargs\"), func() {\n\t\t\t\t\toptions := &redis.BFReserveOptions{\n\t\t\t\t\t\tCapacity:   2000,\n\t\t\t\t\t\tError:      0.001,\n\t\t\t\t\t\tExpansion:  3,\n\t\t\t\t\t\tNonScaling: false,\n\t\t\t\t\t}\n\t\t\t\t\terr := client.BFReserveWithArgs(ctx, \"testbf\", options).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.BFInfo(ctx, \"testbf\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeAssignableToTypeOf(redis.BFInfo{}))\n\t\t\t\t\tExpect(result.Capacity).To(BeEquivalentTo(int64(2000)))\n\t\t\t\t\tExpect(result.ExpansionRate).To(BeEquivalentTo(int64(3)))\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tDescribe(\"cuckoo\", Label(\"cuckoo\"), func() {\n\t\t\t\tIt(\"should CFAdd\", Label(\"cuckoo\", \"cfadd\"), func() {\n\t\t\t\t\tadd, err := client.CFAdd(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(add).To(BeTrue())\n\n\t\t\t\t\texists, err := client.CFExists(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(exists).To(BeTrue())\n\n\t\t\t\t\tinfo, err := client.CFInfo(ctx, \"testcf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(info).To(BeAssignableToTypeOf(redis.CFInfo{}))\n\t\t\t\t\tExpect(info.NumItemsInserted).To(BeEquivalentTo(int64(1)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFAddNX\", Label(\"cuckoo\", \"cfaddnx\"), func() {\n\t\t\t\t\tadd, err := client.CFAddNX(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(add).To(BeTrue())\n\n\t\t\t\t\texists, err := client.CFExists(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(exists).To(BeTrue())\n\n\t\t\t\t\tresult, err := client.CFAddNX(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeFalse())\n\n\t\t\t\t\tinfo, err := client.CFInfo(ctx, \"testcf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(info).To(BeAssignableToTypeOf(redis.CFInfo{}))\n\t\t\t\t\tExpect(info.NumItemsInserted).To(BeEquivalentTo(int64(1)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFCount\", Label(\"cuckoo\", \"cfcount\"), func() {\n\t\t\t\t\tclient.CFAdd(ctx, \"testcf1\", \"item1\")\n\t\t\t\t\tcnt, err := client.CFCount(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(cnt).To(BeEquivalentTo(int64(1)))\n\n\t\t\t\t\terr = client.CFAdd(ctx, \"testcf1\", \"item1\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tcnt, err = client.CFCount(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(cnt).To(BeEquivalentTo(int64(2)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFDel and CFExists\", Label(\"cuckoo\", \"cfdel\", \"cfexists\"), func() {\n\t\t\t\t\terr := client.CFAdd(ctx, \"testcf1\", \"item1\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\texists, err := client.CFExists(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(exists).To(BeTrue())\n\n\t\t\t\t\tdel, err := client.CFDel(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(del).To(BeTrue())\n\n\t\t\t\t\texists, err = client.CFExists(ctx, \"testcf1\", \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(exists).To(BeFalse())\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFInfo and CFReserve\", Label(\"cuckoo\", \"cfinfo\", \"cfreserve\"), func() {\n\t\t\t\t\terr := client.CFReserve(ctx, \"testcf1\", 1000).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.CFReserveExpansion(ctx, \"testcfe1\", 1000, 1).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.CFReserveBucketSize(ctx, \"testcfbs1\", 1000, 4).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.CFReserveMaxIterations(ctx, \"testcfmi1\", 1000, 10).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.CFInfo(ctx, \"testcf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeAssignableToTypeOf(redis.CFInfo{}))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFScanDump and CFLoadChunk\", Label(\"bloom\", \"cfscandump\", \"cfloadchunk\"), func() {\n\t\t\t\t\terr := client.CFReserve(ctx, \"testcfsd1\", 1000).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tfor i := 0; i < 1000; i++ {\n\t\t\t\t\t\tItem := fmt.Sprintf(\"item%d\", i)\n\t\t\t\t\t\tclient.CFAdd(ctx, \"testcfsd1\", Item)\n\t\t\t\t\t}\n\t\t\t\t\tinfBefore := client.CFInfo(ctx, \"testcfsd1\")\n\t\t\t\t\tfd := []redis.ScanDump{}\n\t\t\t\t\tsd, err := client.CFScanDump(ctx, \"testcfsd1\", 0).Result()\n\t\t\t\t\tfor {\n\t\t\t\t\t\tif sd.Iter == 0 {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\t\tfd = append(fd, sd)\n\t\t\t\t\t\tsd, err = client.CFScanDump(ctx, \"testcfsd1\", sd.Iter).Result()\n\t\t\t\t\t}\n\t\t\t\t\tclient.Del(ctx, \"testcfsd1\")\n\t\t\t\t\tfor _, e := range fd {\n\t\t\t\t\t\tclient.CFLoadChunk(ctx, \"testcfsd1\", e.Iter, e.Data)\n\t\t\t\t\t}\n\t\t\t\t\tinfAfter := client.CFInfo(ctx, \"testcfsd1\")\n\t\t\t\t\tExpect(infBefore).To(BeEquivalentTo(infAfter))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFInfo and CFReserveWithArgs\", Label(\"cuckoo\", \"cfinfo\", \"cfreserveargs\"), func() {\n\t\t\t\t\targs := &redis.CFReserveOptions{\n\t\t\t\t\t\tCapacity:      2048,\n\t\t\t\t\t\tBucketSize:    3,\n\t\t\t\t\t\tMaxIterations: 15,\n\t\t\t\t\t\tExpansion:     2,\n\t\t\t\t\t}\n\n\t\t\t\t\terr := client.CFReserveWithArgs(ctx, \"testcf1\", args).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.CFInfo(ctx, \"testcf1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeAssignableToTypeOf(redis.CFInfo{}))\n\t\t\t\t\tExpect(result.BucketSize).To(BeEquivalentTo(int64(3)))\n\t\t\t\t\tExpect(result.MaxIteration).To(BeEquivalentTo(int64(15)))\n\t\t\t\t\tExpect(result.ExpansionRate).To(BeEquivalentTo(int64(2)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFInsert\", Label(\"cuckoo\", \"cfinsert\"), func() {\n\t\t\t\t\targs := &redis.CFInsertOptions{\n\t\t\t\t\t\tCapacity: 3000,\n\t\t\t\t\t\tNoCreate: true,\n\t\t\t\t\t}\n\n\t\t\t\t\t_, err := client.CFInsert(ctx, \"testcf1\", args, \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).To(HaveOccurred())\n\n\t\t\t\t\targs = &redis.CFInsertOptions{\n\t\t\t\t\t\tCapacity: 3000,\n\t\t\t\t\t\tNoCreate: false,\n\t\t\t\t\t}\n\n\t\t\t\t\tresult, err := client.CFInsert(ctx, \"testcf1\", args, \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(result)).To(BeEquivalentTo(3))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFInsertNX\", Label(\"cuckoo\", \"cfinsertnx\"), func() {\n\t\t\t\t\targs := &redis.CFInsertOptions{\n\t\t\t\t\t\tCapacity: 3000,\n\t\t\t\t\t\tNoCreate: true,\n\t\t\t\t\t}\n\n\t\t\t\t\t_, err := client.CFInsertNX(ctx, \"testcf1\", args, \"item1\", \"item2\", \"item2\").Result()\n\t\t\t\t\tExpect(err).To(HaveOccurred())\n\n\t\t\t\t\targs = &redis.CFInsertOptions{\n\t\t\t\t\t\tCapacity: 3000,\n\t\t\t\t\t\tNoCreate: false,\n\t\t\t\t\t}\n\n\t\t\t\t\tresult, err := client.CFInsertNX(ctx, \"testcf2\", args, \"item1\", \"item2\", \"item2\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(result)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(result[0]).To(BeEquivalentTo(int64(1)))\n\t\t\t\t\tExpect(result[1]).To(BeEquivalentTo(int64(1)))\n\t\t\t\t\tExpect(result[2]).To(BeEquivalentTo(int64(0)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CFMexists\", Label(\"cuckoo\", \"cfmexists\"), func() {\n\t\t\t\t\terr := client.CFInsert(ctx, \"testcf1\", nil, \"item1\", \"item2\", \"item3\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.CFMExists(ctx, \"testcf1\", \"item1\", \"item2\", \"item3\", \"item4\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(result)).To(BeEquivalentTo(4))\n\t\t\t\t\tExpect(result[0]).To(BeTrue())\n\t\t\t\t\tExpect(result[1]).To(BeTrue())\n\t\t\t\t\tExpect(result[2]).To(BeTrue())\n\t\t\t\t\tExpect(result[3]).To(BeFalse())\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tDescribe(\"CMS\", Label(\"cms\"), func() {\n\t\t\t\tIt(\"should CMSIncrBy\", Label(\"cms\", \"cmsincrby\"), func() {\n\t\t\t\t\terr := client.CMSInitByDim(ctx, \"testcms1\", 5, 10).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.CMSIncrBy(ctx, \"testcms1\", \"item1\", 1, \"item2\", 2, \"item3\", 3).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(result)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(result[0]).To(BeEquivalentTo(int64(1)))\n\t\t\t\t\tExpect(result[1]).To(BeEquivalentTo(int64(2)))\n\t\t\t\t\tExpect(result[2]).To(BeEquivalentTo(int64(3)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CMSInitByDim and CMSInfo\", Label(\"cms\", \"cmsinitbydim\", \"cmsinfo\"), func() {\n\t\t\t\t\terr := client.CMSInitByDim(ctx, \"testcms1\", 5, 10).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tinfo, err := client.CMSInfo(ctx, \"testcms1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tExpect(info).To(BeAssignableToTypeOf(redis.CMSInfo{}))\n\t\t\t\t\tExpect(info.Width).To(BeEquivalentTo(int64(5)))\n\t\t\t\t\tExpect(info.Depth).To(BeEquivalentTo(int64(10)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CMSInitByProb\", Label(\"cms\", \"cmsinitbyprob\"), func() {\n\t\t\t\t\terr := client.CMSInitByProb(ctx, \"testcms1\", 0.002, 0.01).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tinfo, err := client.CMSInfo(ctx, \"testcms1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(info).To(BeAssignableToTypeOf(redis.CMSInfo{}))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should CMSMerge, CMSMergeWithWeight and CMSQuery\", Label(\"cms\", \"cmsmerge\", \"cmsquery\", \"NonRedisEnterprise\"), func() {\n\t\t\t\t\terr := client.CMSMerge(ctx, \"destCms1\", \"testcms2\", \"testcms3\").Err()\n\t\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\t\tExpect(err).To(MatchError(\"CMS: key does not exist\"))\n\n\t\t\t\t\terr = client.CMSInitByDim(ctx, \"destCms1\", 5, 10).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.CMSInitByDim(ctx, \"destCms2\", 5, 10).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.CMSInitByDim(ctx, \"cms1\", 2, 20).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.CMSInitByDim(ctx, \"cms2\", 3, 20).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\terr = client.CMSMerge(ctx, \"destCms1\", \"cms1\", \"cms2\").Err()\n\t\t\t\t\tExpect(err).To(MatchError(\"CMS: width/depth is not equal\"))\n\n\t\t\t\t\tclient.Del(ctx, \"cms1\", \"cms2\")\n\n\t\t\t\t\terr = client.CMSInitByDim(ctx, \"cms1\", 5, 10).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.CMSInitByDim(ctx, \"cms2\", 5, 10).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tclient.CMSIncrBy(ctx, \"cms1\", \"item1\", 1, \"item2\", 2)\n\t\t\t\t\tclient.CMSIncrBy(ctx, \"cms2\", \"item2\", 2, \"item3\", 3)\n\n\t\t\t\t\terr = client.CMSMerge(ctx, \"destCms1\", \"cms1\", \"cms2\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err := client.CMSQuery(ctx, \"destCms1\", \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(result)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(result[0]).To(BeEquivalentTo(int64(1)))\n\t\t\t\t\tExpect(result[1]).To(BeEquivalentTo(int64(4)))\n\t\t\t\t\tExpect(result[2]).To(BeEquivalentTo(int64(3)))\n\n\t\t\t\t\tsourceSketches := map[string]int64{\n\t\t\t\t\t\t\"cms1\": 1,\n\t\t\t\t\t\t\"cms2\": 2,\n\t\t\t\t\t}\n\t\t\t\t\terr = client.CMSMergeWithWeight(ctx, \"destCms2\", sourceSketches).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresult, err = client.CMSQuery(ctx, \"destCms2\", \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(result)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(result[0]).To(BeEquivalentTo(int64(1)))\n\t\t\t\t\tExpect(result[1]).To(BeEquivalentTo(int64(6)))\n\t\t\t\t\tExpect(result[2]).To(BeEquivalentTo(int64(6)))\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tDescribe(\"TopK\", Label(\"topk\"), func() {\n\t\t\t\tIt(\"should TopKReserve, TopKInfo, TopKAdd, TopKQuery, TopKCount, TopKIncrBy, TopKList, TopKListWithCount\", Label(\"topk\", \"topkreserve\", \"topkinfo\", \"topkadd\", \"topkquery\", \"topkcount\", \"topkincrby\", \"topklist\", \"topklistwithcount\"), func() {\n\t\t\t\t\terr := client.TopKReserve(ctx, \"topk1\", 3).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresultInfo, err := client.TopKInfo(ctx, \"topk1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultInfo.K).To(BeEquivalentTo(int64(3)))\n\n\t\t\t\t\tresultAdd, err := client.TopKAdd(ctx, \"topk1\", \"item1\", \"item2\", 3, \"item1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultAdd)).To(BeEquivalentTo(int64(4)))\n\n\t\t\t\t\tresultQuery, err := client.TopKQuery(ctx, \"topk1\", \"item1\", \"item2\", 4, 3).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultQuery)).To(BeEquivalentTo(4))\n\t\t\t\t\tExpect(resultQuery[0]).To(BeTrue())\n\t\t\t\t\tExpect(resultQuery[1]).To(BeTrue())\n\t\t\t\t\tExpect(resultQuery[2]).To(BeFalse())\n\t\t\t\t\tExpect(resultQuery[3]).To(BeTrue())\n\n\t\t\t\t\tresultCount, err := client.TopKCount(ctx, \"topk1\", \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultCount)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(resultCount[0]).To(BeEquivalentTo(int64(2)))\n\t\t\t\t\tExpect(resultCount[1]).To(BeEquivalentTo(int64(1)))\n\t\t\t\t\tExpect(resultCount[2]).To(BeEquivalentTo(int64(0)))\n\n\t\t\t\t\tresultIncr, err := client.TopKIncrBy(ctx, \"topk1\", \"item1\", 5, \"item2\", 10).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultIncr)).To(BeEquivalentTo(2))\n\n\t\t\t\t\tresultCount, err = client.TopKCount(ctx, \"topk1\", \"item1\", \"item2\", \"item3\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultCount)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(resultCount[0]).To(BeEquivalentTo(int64(7)))\n\t\t\t\t\tExpect(resultCount[1]).To(BeEquivalentTo(int64(11)))\n\t\t\t\t\tExpect(resultCount[2]).To(BeEquivalentTo(int64(0)))\n\n\t\t\t\t\tresultList, err := client.TopKList(ctx, \"topk1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultList)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(resultList).To(ContainElements(\"item2\", \"item1\", \"3\"))\n\n\t\t\t\t\tresultListWithCount, err := client.TopKListWithCount(ctx, \"topk1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(resultListWithCount)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(resultListWithCount[\"3\"]).To(BeEquivalentTo(int64(1)))\n\t\t\t\t\tExpect(resultListWithCount[\"item1\"]).To(BeEquivalentTo(int64(7)))\n\t\t\t\t\tExpect(resultListWithCount[\"item2\"]).To(BeEquivalentTo(int64(11)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should TopKReserveWithOptions\", Label(\"topk\", \"topkreservewithoptions\"), func() {\n\t\t\t\t\terr := client.TopKReserveWithOptions(ctx, \"topk1\", 3, 1500, 8, 0.5).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tresultInfo, err := client.TopKInfo(ctx, \"topk1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultInfo.K).To(BeEquivalentTo(int64(3)))\n\t\t\t\t\tExpect(resultInfo.Width).To(BeEquivalentTo(int64(1500)))\n\t\t\t\t\tExpect(resultInfo.Depth).To(BeEquivalentTo(int64(8)))\n\t\t\t\t\tExpect(resultInfo.Decay).To(BeEquivalentTo(0.5))\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tDescribe(\"t-digest\", Label(\"tdigest\"), func() {\n\t\t\t\tIt(\"should TDigestAdd, TDigestCreate, TDigestInfo, TDigestByRank, TDigestByRevRank, TDigestCDF, TDigestMax, TDigestMin, TDigestQuantile, TDigestRank, TDigestRevRank, TDigestTrimmedMean, TDigestReset, \", Label(\"tdigest\", \"tdigestadd\", \"tdigestcreate\", \"tdigestinfo\", \"tdigestbyrank\", \"tdigestbyrevrank\", \"tdigestcdf\", \"tdigestmax\", \"tdigestmin\", \"tdigestquantile\", \"tdigestrank\", \"tdigestrevrank\", \"tdigesttrimmedmean\", \"tdigestreset\"), func() {\n\t\t\t\t\terr := client.TDigestCreate(ctx, \"tdigest1\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tinfo, err := client.TDigestInfo(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(info.Observations).To(BeEquivalentTo(int64(0)))\n\n\t\t\t\t\t// Test with empty sketch\n\t\t\t\t\tbyRank, err := client.TDigestByRank(ctx, \"tdigest1\", 0, 1, 2, 3).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(byRank)).To(BeEquivalentTo(4))\n\n\t\t\t\t\tbyRevRank, err := client.TDigestByRevRank(ctx, \"tdigest1\", 0, 1, 2).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(byRevRank)).To(BeEquivalentTo(3))\n\n\t\t\t\t\tcdf, err := client.TDigestCDF(ctx, \"tdigest1\", 15, 35, 70).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(cdf)).To(BeEquivalentTo(3))\n\n\t\t\t\t\tmax, err := client.TDigestMax(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(math.IsNaN(max)).To(BeTrue())\n\n\t\t\t\t\tmin, err := client.TDigestMin(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(math.IsNaN(min)).To(BeTrue())\n\n\t\t\t\t\tquantile, err := client.TDigestQuantile(ctx, \"tdigest1\", 0.1, 0.2).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(quantile)).To(BeEquivalentTo(2))\n\n\t\t\t\t\trank, err := client.TDigestRank(ctx, \"tdigest1\", 10, 20).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(rank)).To(BeEquivalentTo(2))\n\n\t\t\t\t\trevRank, err := client.TDigestRevRank(ctx, \"tdigest1\", 10, 20).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(revRank)).To(BeEquivalentTo(2))\n\n\t\t\t\t\ttrimmedMean, err := client.TDigestTrimmedMean(ctx, \"tdigest1\", 0.1, 0.6).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(math.IsNaN(trimmedMean)).To(BeTrue())\n\n\t\t\t\t\t// Add elements\n\t\t\t\t\terr = client.TDigestAdd(ctx, \"tdigest1\", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tinfo, err = client.TDigestInfo(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(info.Observations).To(BeEquivalentTo(int64(10)))\n\n\t\t\t\t\tbyRank, err = client.TDigestByRank(ctx, \"tdigest1\", 0, 1, 2).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(byRank)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(byRank[0]).To(BeEquivalentTo(float64(10)))\n\t\t\t\t\tExpect(byRank[1]).To(BeEquivalentTo(float64(20)))\n\t\t\t\t\tExpect(byRank[2]).To(BeEquivalentTo(float64(30)))\n\n\t\t\t\t\tbyRevRank, err = client.TDigestByRevRank(ctx, \"tdigest1\", 0, 1, 2).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(byRevRank)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(byRevRank[0]).To(BeEquivalentTo(float64(100)))\n\t\t\t\t\tExpect(byRevRank[1]).To(BeEquivalentTo(float64(90)))\n\t\t\t\t\tExpect(byRevRank[2]).To(BeEquivalentTo(float64(80)))\n\n\t\t\t\t\tcdf, err = client.TDigestCDF(ctx, \"tdigest1\", 15, 35, 70).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(cdf)).To(BeEquivalentTo(3))\n\t\t\t\t\tExpect(cdf[0]).To(BeEquivalentTo(0.1))\n\t\t\t\t\tExpect(cdf[1]).To(BeEquivalentTo(0.3))\n\t\t\t\t\tExpect(cdf[2]).To(BeEquivalentTo(0.65))\n\n\t\t\t\t\tmax, err = client.TDigestMax(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(max).To(BeEquivalentTo(float64(100)))\n\n\t\t\t\t\tmin, err = client.TDigestMin(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(min).To(BeEquivalentTo(float64(10)))\n\n\t\t\t\t\tquantile, err = client.TDigestQuantile(ctx, \"tdigest1\", 0.1, 0.2).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(quantile)).To(BeEquivalentTo(2))\n\t\t\t\t\tExpect(quantile[0]).To(BeEquivalentTo(float64(20)))\n\t\t\t\t\tExpect(quantile[1]).To(BeEquivalentTo(float64(30)))\n\n\t\t\t\t\trank, err = client.TDigestRank(ctx, \"tdigest1\", 10, 20).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(rank)).To(BeEquivalentTo(2))\n\t\t\t\t\tExpect(rank[0]).To(BeEquivalentTo(int64(0)))\n\t\t\t\t\tExpect(rank[1]).To(BeEquivalentTo(int64(1)))\n\n\t\t\t\t\trevRank, err = client.TDigestRevRank(ctx, \"tdigest1\", 10, 20).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(len(revRank)).To(BeEquivalentTo(2))\n\t\t\t\t\tExpect(revRank[0]).To(BeEquivalentTo(int64(9)))\n\t\t\t\t\tExpect(revRank[1]).To(BeEquivalentTo(int64(8)))\n\n\t\t\t\t\ttrimmedMean, err = client.TDigestTrimmedMean(ctx, \"tdigest1\", 0.1, 0.6).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(trimmedMean).To(BeEquivalentTo(float64(40)))\n\n\t\t\t\t\treset, err := client.TDigestReset(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(reset).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should TDigestCreateWithCompression\", Label(\"tdigest\", \"tcreatewithcompression\"), func() {\n\t\t\t\t\terr := client.TDigestCreateWithCompression(ctx, \"tdigest1\", 2000).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tinfo, err := client.TDigestInfo(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(info.Compression).To(BeEquivalentTo(int64(2000)))\n\t\t\t\t})\n\n\t\t\t\tIt(\"should TDigestMerge\", Label(\"tdigest\", \"tmerge\", \"NonRedisEnterprise\"), func() {\n\t\t\t\t\terr := client.TDigestCreate(ctx, \"tdigest1\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.TDigestAdd(ctx, \"tdigest1\", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\terr = client.TDigestCreate(ctx, \"tdigest2\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.TDigestAdd(ctx, \"tdigest2\", 15, 25, 35, 45, 55, 65, 75, 85, 95, 105).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\terr = client.TDigestCreate(ctx, \"tdigest3\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\terr = client.TDigestAdd(ctx, \"tdigest3\", 50, 60, 70, 80, 90, 100, 110, 120, 130, 140).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\toptions := &redis.TDigestMergeOptions{\n\t\t\t\t\t\tCompression: 1000,\n\t\t\t\t\t\tOverride:    false,\n\t\t\t\t\t}\n\t\t\t\t\terr = client.TDigestMerge(ctx, \"tdigest1\", options, \"tdigest2\", \"tdigest3\").Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tinfo, err := client.TDigestInfo(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(info.Observations).To(BeEquivalentTo(int64(30)))\n\t\t\t\t\tExpect(info.Compression).To(BeEquivalentTo(int64(1000)))\n\n\t\t\t\t\tmax, err := client.TDigestMax(ctx, \"tdigest1\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(max).To(BeEquivalentTo(float64(140)))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t}\n})\n"
  },
  {
    "path": "pubsub.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/otel\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// PubSub implements Pub/Sub commands as described in\n// https://redis.io/docs/latest/develop/pubsub. Message receiving is NOT safe\n// for concurrent use by multiple goroutines.\n//\n// PubSub automatically reconnects to Redis Server and resubscribes\n// to the channels in case of network errors.\ntype PubSub struct {\n\topt *Options\n\n\tnewConn   func(ctx context.Context, addr string, channels []string) (*pool.Conn, error)\n\tcloseConn func(*pool.Conn) error\n\n\tmu        sync.Mutex\n\tcn        *pool.Conn\n\tchannels  map[string]struct{}\n\tpatterns  map[string]struct{}\n\tschannels map[string]struct{}\n\n\tclosed bool\n\texit   chan struct{}\n\n\tcmd *Cmd\n\n\tchOnce sync.Once\n\tmsgCh  *channel\n\tallCh  *channel\n\n\t// Push notification processor for handling generic push notifications\n\tpushProcessor push.NotificationProcessor\n\n\t// Cleanup callback for maintenanceNotifications upgrade tracking\n\tonClose func()\n}\n\nfunc (c *PubSub) init() {\n\tc.exit = make(chan struct{})\n}\n\nfunc (c *PubSub) String() string {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tchannels := mapKeys(c.channels)\n\tchannels = append(channels, mapKeys(c.patterns)...)\n\tchannels = append(channels, mapKeys(c.schannels)...)\n\treturn fmt.Sprintf(\"PubSub(%s)\", strings.Join(channels, \", \"))\n}\n\nfunc (c *PubSub) connWithLock(ctx context.Context) (*pool.Conn, error) {\n\tc.mu.Lock()\n\tcn, err := c.conn(ctx, nil)\n\tc.mu.Unlock()\n\treturn cn, err\n}\n\nfunc (c *PubSub) conn(ctx context.Context, newChannels []string) (*pool.Conn, error) {\n\tif c.closed {\n\t\treturn nil, pool.ErrClosed\n\t}\n\tif c.cn != nil {\n\t\treturn c.cn, nil\n\t}\n\n\tif c.opt.Addr == \"\" {\n\t\t// TODO(maintenanceNotifications):\n\t\t// this is probably cluster client\n\t\t// c.newConn will ignore the addr argument\n\t\t// will be changed when we have maintenanceNotifications upgrades for cluster clients\n\t\tc.opt.Addr = internal.RedisNull\n\t}\n\n\tchannels := mapKeys(c.channels)\n\tchannels = append(channels, newChannels...)\n\n\tcn, err := c.newConn(ctx, c.opt.Addr, channels)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := c.resubscribe(ctx, cn); err != nil {\n\t\t_ = c.closeConn(cn)\n\t\treturn nil, err\n\t}\n\n\tc.cn = cn\n\treturn cn, nil\n}\n\nfunc (c *PubSub) writeCmd(ctx context.Context, cn *pool.Conn, cmd Cmder) error {\n\treturn cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {\n\t\treturn writeCmd(wr, cmd)\n\t})\n}\n\nfunc (c *PubSub) resubscribe(ctx context.Context, cn *pool.Conn) error {\n\tvar firstErr error\n\n\tif len(c.channels) > 0 {\n\t\tfirstErr = c._subscribe(ctx, cn, \"subscribe\", mapKeys(c.channels))\n\t}\n\n\tif len(c.patterns) > 0 {\n\t\terr := c._subscribe(ctx, cn, \"psubscribe\", mapKeys(c.patterns))\n\t\tif err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\n\tif len(c.schannels) > 0 {\n\t\terr := c._subscribe(ctx, cn, \"ssubscribe\", mapKeys(c.schannels))\n\t\tif err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\n\treturn firstErr\n}\n\nfunc mapKeys(m map[string]struct{}) []string {\n\ts := make([]string, len(m))\n\ti := 0\n\tfor k := range m {\n\t\ts[i] = k\n\t\ti++\n\t}\n\treturn s\n}\n\nfunc (c *PubSub) _subscribe(\n\tctx context.Context, cn *pool.Conn, redisCmd string, channels []string,\n) error {\n\targs := make([]interface{}, 0, 1+len(channels))\n\targs = append(args, redisCmd)\n\tfor _, channel := range channels {\n\t\targs = append(args, channel)\n\t}\n\tcmd := NewSliceCmd(ctx, args...)\n\treturn c.writeCmd(ctx, cn, cmd)\n}\n\nfunc (c *PubSub) releaseConnWithLock(\n\tctx context.Context,\n\tcn *pool.Conn,\n\terr error,\n\tallowTimeout bool,\n) {\n\tc.mu.Lock()\n\tc.releaseConn(ctx, cn, err, allowTimeout)\n\tc.mu.Unlock()\n}\n\nfunc (c *PubSub) releaseConn(ctx context.Context, cn *pool.Conn, err error, allowTimeout bool) {\n\tif c.cn != cn {\n\t\treturn\n\t}\n\n\tif !cn.IsUsable() || cn.ShouldHandoff() {\n\t\tc.reconnect(ctx, fmt.Errorf(\"pubsub: connection is not usable\"))\n\t}\n\n\tif isBadConn(err, allowTimeout, c.opt.Addr) {\n\t\tc.reconnect(ctx, err)\n\t}\n}\n\nfunc (c *PubSub) reconnect(ctx context.Context, reason error) {\n\tif c.cn != nil && c.cn.ShouldHandoff() {\n\t\tnewEndpoint := c.cn.GetHandoffEndpoint()\n\t\t// If new endpoint is NULL, use the original address\n\t\tif newEndpoint == internal.RedisNull {\n\t\t\tnewEndpoint = c.opt.Addr\n\t\t}\n\n\t\tif newEndpoint != \"\" {\n\t\t\t// Update the address in the options\n\t\t\toldAddr := c.cn.RemoteAddr().String()\n\t\t\tc.opt.Addr = newEndpoint\n\t\t\tinternal.Logger.Printf(ctx, \"pubsub: reconnecting to new endpoint %s (was %s)\", newEndpoint, oldAddr)\n\t\t}\n\t}\n\t_ = c.closeTheCn(reason)\n\t_, _ = c.conn(ctx, nil)\n}\n\nfunc (c *PubSub) closeTheCn(reason error) error {\n\tif c.cn == nil {\n\t\treturn nil\n\t}\n\terr := c.closeConn(c.cn)\n\tc.cn = nil\n\treturn err\n}\n\nfunc (c *PubSub) Close() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.closed {\n\t\treturn pool.ErrClosed\n\t}\n\tc.closed = true\n\tclose(c.exit)\n\n\t// Call cleanup callback if set\n\tif c.onClose != nil {\n\t\tc.onClose()\n\t}\n\n\treturn c.closeTheCn(pool.ErrClosed)\n}\n\n// Subscribe the client to the specified channels. It returns\n// empty subscription if there are no channels.\nfunc (c *PubSub) Subscribe(ctx context.Context, channels ...string) error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\terr := c.subscribe(ctx, \"subscribe\", channels...)\n\tif c.channels == nil {\n\t\tc.channels = make(map[string]struct{})\n\t}\n\tfor _, s := range channels {\n\t\tc.channels[s] = struct{}{}\n\t}\n\treturn err\n}\n\n// PSubscribe the client to the given patterns. It returns\n// empty subscription if there are no patterns.\nfunc (c *PubSub) PSubscribe(ctx context.Context, patterns ...string) error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\terr := c.subscribe(ctx, \"psubscribe\", patterns...)\n\tif c.patterns == nil {\n\t\tc.patterns = make(map[string]struct{})\n\t}\n\tfor _, s := range patterns {\n\t\tc.patterns[s] = struct{}{}\n\t}\n\treturn err\n}\n\n// SSubscribe Subscribes the client to the specified shard channels.\nfunc (c *PubSub) SSubscribe(ctx context.Context, channels ...string) error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\terr := c.subscribe(ctx, \"ssubscribe\", channels...)\n\tif c.schannels == nil {\n\t\tc.schannels = make(map[string]struct{})\n\t}\n\tfor _, s := range channels {\n\t\tc.schannels[s] = struct{}{}\n\t}\n\treturn err\n}\n\n// Unsubscribe the client from the given channels, or from all of\n// them if none is given.\nfunc (c *PubSub) Unsubscribe(ctx context.Context, channels ...string) error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif len(channels) > 0 {\n\t\tfor _, channel := range channels {\n\t\t\tdelete(c.channels, channel)\n\t\t}\n\t} else {\n\t\t// Unsubscribe from all channels.\n\t\tfor channel := range c.channels {\n\t\t\tdelete(c.channels, channel)\n\t\t}\n\t}\n\n\terr := c.subscribe(ctx, \"unsubscribe\", channels...)\n\treturn err\n}\n\n// PUnsubscribe the client from the given patterns, or from all of\n// them if none is given.\nfunc (c *PubSub) PUnsubscribe(ctx context.Context, patterns ...string) error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif len(patterns) > 0 {\n\t\tfor _, pattern := range patterns {\n\t\t\tdelete(c.patterns, pattern)\n\t\t}\n\t} else {\n\t\t// Unsubscribe from all patterns.\n\t\tfor pattern := range c.patterns {\n\t\t\tdelete(c.patterns, pattern)\n\t\t}\n\t}\n\n\terr := c.subscribe(ctx, \"punsubscribe\", patterns...)\n\treturn err\n}\n\n// SUnsubscribe unsubscribes the client from the given shard channels,\n// or from all of them if none is given.\nfunc (c *PubSub) SUnsubscribe(ctx context.Context, channels ...string) error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif len(channels) > 0 {\n\t\tfor _, channel := range channels {\n\t\t\tdelete(c.schannels, channel)\n\t\t}\n\t} else {\n\t\t// Unsubscribe from all channels.\n\t\tfor channel := range c.schannels {\n\t\t\tdelete(c.schannels, channel)\n\t\t}\n\t}\n\n\terr := c.subscribe(ctx, \"sunsubscribe\", channels...)\n\treturn err\n}\n\nfunc (c *PubSub) subscribe(ctx context.Context, redisCmd string, channels ...string) error {\n\tcn, err := c.conn(ctx, channels)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c._subscribe(ctx, cn, redisCmd, channels)\n\tc.releaseConn(ctx, cn, err, false)\n\treturn err\n}\n\nfunc (c *PubSub) Ping(ctx context.Context, payload ...string) error {\n\targs := []interface{}{\"ping\"}\n\tif len(payload) == 1 {\n\t\targs = append(args, payload[0])\n\t}\n\tcmd := NewCmd(ctx, args...)\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tcn, err := c.conn(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.writeCmd(ctx, cn, cmd)\n\tc.releaseConn(ctx, cn, err, false)\n\treturn err\n}\n\n// ClientSetName assigns  a namee to the PubSub connection using  CLIENT SETNAME,\n// The name is visible in CLIENT LIST output and is useful for debugging\n// and identifying connections in a redis instance.\nfunc (c *PubSub) ClientSetName(ctx context.Context, name string) error {\n\tcmd := NewStatusCmd(ctx, \"client\", \"setname\", name)\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tcn, err := c.conn(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.writeCmd(ctx, cn, cmd)\n\tc.releaseConn(ctx, cn, err, false)\n\treturn err\n}\n\n// Subscription received after a successful subscription to channel.\ntype Subscription struct {\n\t// Can be \"subscribe\", \"unsubscribe\", \"psubscribe\" or \"punsubscribe\".\n\tKind string\n\t// Channel name we have subscribed to.\n\tChannel string\n\t// Number of channels we are currently subscribed to.\n\tCount int\n}\n\nfunc (m *Subscription) String() string {\n\treturn fmt.Sprintf(\"%s: %s\", m.Kind, m.Channel)\n}\n\n// Message received as result of a PUBLISH command issued by another client.\ntype Message struct {\n\tChannel      string\n\tPattern      string\n\tPayload      string\n\tPayloadSlice []string\n}\n\nfunc (m *Message) String() string {\n\treturn fmt.Sprintf(\"Message<%s: %s>\", m.Channel, m.Payload)\n}\n\n// Pong received as result of a PING command issued by another client.\ntype Pong struct {\n\tPayload string\n}\n\nfunc (p *Pong) String() string {\n\tif p.Payload != \"\" {\n\t\treturn fmt.Sprintf(\"Pong<%s>\", p.Payload)\n\t}\n\treturn \"Pong\"\n}\n\nfunc (c *PubSub) newMessage(ctx context.Context, cn *pool.Conn, reply interface{}) (interface{}, error) {\n\tswitch reply := reply.(type) {\n\tcase string:\n\t\treturn &Pong{\n\t\t\tPayload: reply,\n\t\t}, nil\n\tcase []interface{}:\n\t\tswitch kind := reply[0].(string); kind {\n\t\tcase \"subscribe\", \"unsubscribe\", \"psubscribe\", \"punsubscribe\", \"ssubscribe\", \"sunsubscribe\":\n\t\t\t// Can be nil in case of \"unsubscribe\".\n\t\t\tchannel, _ := reply[1].(string)\n\t\t\treturn &Subscription{\n\t\t\t\tKind:    kind,\n\t\t\t\tChannel: channel,\n\t\t\t\tCount:   int(reply[2].(int64)),\n\t\t\t}, nil\n\t\tcase \"message\", \"smessage\":\n\t\t\tchannel := reply[1].(string)\n\t\t\tsharded := kind == \"smessage\"\n\t\t\tswitch payload := reply[2].(type) {\n\t\t\tcase string:\n\t\t\t\tmsg := &Message{\n\t\t\t\t\tChannel: channel,\n\t\t\t\t\tPayload: payload,\n\t\t\t\t}\n\t\t\t\t// Record PubSub message received\n\t\t\t\totel.RecordPubSubMessage(ctx, cn, \"received\", channel, sharded)\n\t\t\t\treturn msg, nil\n\t\t\tcase []interface{}:\n\t\t\t\tss := make([]string, len(payload))\n\t\t\t\tfor i, s := range payload {\n\t\t\t\t\tss[i] = s.(string)\n\t\t\t\t}\n\t\t\t\tmsg := &Message{\n\t\t\t\t\tChannel:      channel,\n\t\t\t\t\tPayloadSlice: ss,\n\t\t\t\t}\n\t\t\t\t// Record PubSub message received\n\t\t\t\totel.RecordPubSubMessage(ctx, cn, \"received\", channel, sharded)\n\t\t\t\treturn msg, nil\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"redis: unsupported pubsub message payload: %T\", payload)\n\t\t\t}\n\t\tcase \"pmessage\":\n\t\t\tchannel := reply[2].(string)\n\t\t\tmsg := &Message{\n\t\t\t\tPattern: reply[1].(string),\n\t\t\t\tChannel: channel,\n\t\t\t\tPayload: reply[3].(string),\n\t\t\t}\n\t\t\t// Record PubSub message received (pattern message, not sharded)\n\t\t\totel.RecordPubSubMessage(ctx, cn, \"received\", channel, false)\n\t\t\treturn msg, nil\n\t\tcase \"pong\":\n\t\t\treturn &Pong{\n\t\t\t\tPayload: reply[1].(string),\n\t\t\t}, nil\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"redis: unsupported pubsub message: %q\", kind)\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: unsupported pubsub message: %#v\", reply)\n\t}\n}\n\n// ReceiveTimeout acts like Receive but returns an error if message\n// is not received in time. This is low-level API and in most cases\n// Channel should be used instead.\nfunc (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (interface{}, error) {\n\tif c.cmd == nil {\n\t\tc.cmd = NewCmd(ctx)\n\t}\n\n\t// Don't hold the lock to allow subscriptions and pings.\n\tcn, err := c.connWithLock(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = cn.WithReader(ctx, timeout, func(rd *proto.Reader) error {\n\t\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\t\tif err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\t\t// Log the error but don't fail the command execution\n\t\t\t// Push notification processing errors shouldn't break normal Redis operations\n\t\t\tinternal.Logger.Printf(ctx, \"push: conn[%d] error processing pending notifications before reading reply: %v\", cn.GetID(), err)\n\t\t}\n\t\treturn c.cmd.readReply(rd)\n\t})\n\tc.releaseConnWithLock(ctx, cn, err, timeout > 0)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c.newMessage(ctx, cn, c.cmd.Val())\n}\n\n// Receive returns a message as a Subscription, Message, Pong or error.\n// See PubSub example for details. This is low-level API and in most cases\n// Channel should be used instead.\n// Receive returns a message as a Subscription, Message, Pong, or an error.\n// See PubSub example for details. This is a low-level API and in most cases\n// Channel should be used instead.\n// This method blocks until a message is received or an error occurs.\n// It may return early with an error if the context is canceled, the connection fails,\n// or other internal errors occur.\nfunc (c *PubSub) Receive(ctx context.Context) (interface{}, error) {\n\treturn c.ReceiveTimeout(ctx, 0)\n}\n\n// ReceiveMessage returns a Message or error ignoring Subscription and Pong\n// messages. This is low-level API and in most cases Channel should be used\n// instead.\nfunc (c *PubSub) ReceiveMessage(ctx context.Context) (*Message, error) {\n\tfor {\n\t\tmsg, err := c.Receive(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch msg := msg.(type) {\n\t\tcase *Subscription:\n\t\t\t// Ignore.\n\t\tcase *Pong:\n\t\t\t// Ignore.\n\t\tcase *Message:\n\t\t\treturn msg, nil\n\t\tdefault:\n\t\t\terr := fmt.Errorf(\"redis: unknown message: %T\", msg)\n\t\t\treturn nil, err\n\t\t}\n\t}\n}\n\nfunc (c *PubSub) getContext() context.Context {\n\tif c.cmd != nil {\n\t\treturn c.cmd.ctx\n\t}\n\treturn context.Background()\n}\n\n//------------------------------------------------------------------------------\n\n// Channel returns a Go channel for concurrently receiving messages.\n// The channel is closed together with the PubSub. If the Go channel\n// is blocked full for 1 minute the message is dropped.\n// Receive* APIs can not be used after channel is created.\n//\n// go-redis periodically sends ping messages to test connection health\n// and re-subscribes if ping can not received for 1 minute.\nfunc (c *PubSub) Channel(opts ...ChannelOption) <-chan *Message {\n\tc.chOnce.Do(func() {\n\t\tc.msgCh = newChannel(c, opts...)\n\t\tc.msgCh.initMsgChan()\n\t})\n\tif c.msgCh == nil {\n\t\terr := fmt.Errorf(\"redis: Channel can't be called after ChannelWithSubscriptions\")\n\t\tpanic(err)\n\t}\n\treturn c.msgCh.msgCh\n}\n\n// ChannelSize is like Channel, but creates a Go channel\n// with specified buffer size.\n//\n// Deprecated: use Channel(WithChannelSize(size)), remove in v9.\nfunc (c *PubSub) ChannelSize(size int) <-chan *Message {\n\treturn c.Channel(WithChannelSize(size))\n}\n\n// ChannelWithSubscriptions is like Channel, but message type can be either\n// *Subscription or *Message. Subscription messages can be used to detect\n// reconnections.\n//\n// ChannelWithSubscriptions can not be used together with Channel or ChannelSize.\nfunc (c *PubSub) ChannelWithSubscriptions(opts ...ChannelOption) <-chan interface{} {\n\tc.chOnce.Do(func() {\n\t\tc.allCh = newChannel(c, opts...)\n\t\tc.allCh.initAllChan()\n\t})\n\tif c.allCh == nil {\n\t\terr := fmt.Errorf(\"redis: ChannelWithSubscriptions can't be called after Channel\")\n\t\tpanic(err)\n\t}\n\treturn c.allCh.allCh\n}\n\nfunc (c *PubSub) processPendingPushNotificationWithReader(ctx context.Context, cn *pool.Conn, rd *proto.Reader) error {\n\t// Only process push notifications for RESP3 connections with a processor\n\tif c.opt.Protocol != 3 || c.pushProcessor == nil {\n\t\treturn nil\n\t}\n\n\t// Create handler context with client, connection pool, and connection information\n\thandlerCtx := c.pushNotificationHandlerContext(cn)\n\treturn c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd)\n}\n\nfunc (c *PubSub) pushNotificationHandlerContext(cn *pool.Conn) push.NotificationHandlerContext {\n\t// PubSub doesn't have a client or connection pool, so we pass nil for those\n\t// PubSub connections are blocking\n\treturn push.NotificationHandlerContext{\n\t\tPubSub:     c,\n\t\tConn:       cn,\n\t\tIsBlocking: true,\n\t}\n}\n\ntype ChannelOption func(c *channel)\n\n// WithChannelSize specifies the Go chan size that is used to buffer incoming messages.\n//\n// The default is 100 messages.\nfunc WithChannelSize(size int) ChannelOption {\n\treturn func(c *channel) {\n\t\tc.chanSize = size\n\t}\n}\n\n// WithChannelHealthCheckInterval specifies the health check interval.\n// PubSub will ping Redis Server if it does not receive any messages within the interval.\n// To disable health check, use zero interval.\n//\n// The default is 3 seconds.\nfunc WithChannelHealthCheckInterval(d time.Duration) ChannelOption {\n\treturn func(c *channel) {\n\t\tc.checkInterval = d\n\t}\n}\n\n// WithChannelSendTimeout specifies the channel send timeout after which\n// the message is dropped.\n//\n// The default is 60 seconds.\nfunc WithChannelSendTimeout(d time.Duration) ChannelOption {\n\treturn func(c *channel) {\n\t\tc.chanSendTimeout = d\n\t}\n}\n\ntype channel struct {\n\tpubSub *PubSub\n\n\tmsgCh chan *Message\n\tallCh chan interface{}\n\tping  chan struct{}\n\n\tchanSize        int\n\tchanSendTimeout time.Duration\n\tcheckInterval   time.Duration\n}\n\nfunc newChannel(pubSub *PubSub, opts ...ChannelOption) *channel {\n\tc := &channel{\n\t\tpubSub: pubSub,\n\n\t\tchanSize:        100,\n\t\tchanSendTimeout: time.Minute,\n\t\tcheckInterval:   3 * time.Second,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\tif c.checkInterval > 0 {\n\t\tc.initHealthCheck()\n\t}\n\treturn c\n}\n\nfunc (c *channel) initHealthCheck() {\n\tctx := context.TODO()\n\tc.ping = make(chan struct{}, 1)\n\n\tgo func() {\n\t\ttimer := time.NewTimer(time.Minute)\n\t\ttimer.Stop()\n\n\t\tfor {\n\t\t\ttimer.Reset(c.checkInterval)\n\t\t\tselect {\n\t\t\tcase <-c.ping:\n\t\t\t\tif !timer.Stop() {\n\t\t\t\t\t<-timer.C\n\t\t\t\t}\n\t\t\tcase <-timer.C:\n\t\t\t\tif pingErr := c.pubSub.Ping(ctx); pingErr != nil {\n\t\t\t\t\tc.pubSub.mu.Lock()\n\t\t\t\t\tc.pubSub.reconnect(ctx, pingErr)\n\t\t\t\t\tc.pubSub.mu.Unlock()\n\t\t\t\t}\n\t\t\tcase <-c.pubSub.exit:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// initMsgChan must be in sync with initAllChan.\nfunc (c *channel) initMsgChan() {\n\tctx := context.TODO()\n\tc.msgCh = make(chan *Message, c.chanSize)\n\n\tgo func() {\n\t\ttimer := time.NewTimer(time.Minute)\n\t\ttimer.Stop()\n\n\t\tvar errCount int\n\t\tfor {\n\t\t\tmsg, err := c.pubSub.Receive(ctx)\n\t\t\tif err != nil {\n\t\t\t\tif err == pool.ErrClosed {\n\t\t\t\t\tclose(c.msgCh)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif errCount > 0 {\n\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t}\n\t\t\t\terrCount++\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\terrCount = 0\n\n\t\t\t// Any message is as good as a ping.\n\t\t\tselect {\n\t\t\tcase c.ping <- struct{}{}:\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tswitch msg := msg.(type) {\n\t\t\tcase *Subscription:\n\t\t\t\t// Ignore.\n\t\t\tcase *Pong:\n\t\t\t\t// Ignore.\n\t\t\tcase *Message:\n\t\t\t\ttimer.Reset(c.chanSendTimeout)\n\t\t\t\tselect {\n\t\t\t\tcase c.msgCh <- msg:\n\t\t\t\t\tif !timer.Stop() {\n\t\t\t\t\t\t<-timer.C\n\t\t\t\t\t}\n\t\t\t\tcase <-timer.C:\n\t\t\t\t\tinternal.Logger.Printf(\n\t\t\t\t\t\tctx, \"redis: %v channel is full for %s (message is dropped)\",\n\t\t\t\t\t\tc, c.chanSendTimeout)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tinternal.Logger.Printf(ctx, \"redis: unknown message type: %T\", msg)\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// initAllChan must be in sync with initMsgChan.\nfunc (c *channel) initAllChan() {\n\tctx := context.TODO()\n\tc.allCh = make(chan interface{}, c.chanSize)\n\n\tgo func() {\n\t\ttimer := time.NewTimer(time.Minute)\n\t\ttimer.Stop()\n\n\t\tvar errCount int\n\t\tfor {\n\t\t\tmsg, err := c.pubSub.Receive(ctx)\n\t\t\tif err != nil {\n\t\t\t\tif err == pool.ErrClosed {\n\t\t\t\t\tclose(c.allCh)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif errCount > 0 {\n\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t}\n\t\t\t\terrCount++\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\terrCount = 0\n\n\t\t\t// Any message is as good as a ping.\n\t\t\tselect {\n\t\t\tcase c.ping <- struct{}{}:\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tswitch msg := msg.(type) {\n\t\t\tcase *Pong:\n\t\t\t\t// Ignore.\n\t\t\tcase *Subscription, *Message:\n\t\t\t\ttimer.Reset(c.chanSendTimeout)\n\t\t\t\tselect {\n\t\t\t\tcase c.allCh <- msg:\n\t\t\t\t\tif !timer.Stop() {\n\t\t\t\t\t\t<-timer.C\n\t\t\t\t\t}\n\t\t\t\tcase <-timer.C:\n\t\t\t\t\tinternal.Logger.Printf(\n\t\t\t\t\t\tctx, \"redis: %v channel is full for %s (message is dropped)\",\n\t\t\t\t\t\tc, c.chanSendTimeout)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tinternal.Logger.Printf(ctx, \"redis: unknown message type: %T\", msg)\n\t\t\t}\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "pubsub_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\n\t\"github.com/redis/go-redis/v9/internal/otel\"\n)\n\ntype PubSubCmdable interface {\n\tPublish(ctx context.Context, channel string, message interface{}) *IntCmd\n\tSPublish(ctx context.Context, channel string, message interface{}) *IntCmd\n\tPubSubChannels(ctx context.Context, pattern string) *StringSliceCmd\n\tPubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd\n\tPubSubNumPat(ctx context.Context) *IntCmd\n\tPubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd\n\tPubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd\n}\n\n// Publish posts the message to the channel.\nfunc (c cmdable) Publish(ctx context.Context, channel string, message interface{}) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"publish\", channel, message)\n\t_ = c(ctx, cmd)\n\t// Record PubSub message sent (if command succeeded)\n\tif cmd.Err() == nil {\n\t\totel.RecordPubSubMessage(ctx, nil, \"sent\", channel, false)\n\t}\n\treturn cmd\n}\n\nfunc (c cmdable) SPublish(ctx context.Context, channel string, message interface{}) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"spublish\", channel, message)\n\t_ = c(ctx, cmd)\n\t// Record PubSub message sent (if command succeeded)\n\tif cmd.Err() == nil {\n\t\totel.RecordPubSubMessage(ctx, nil, \"sent\", channel, true)\n\t}\n\treturn cmd\n}\n\nfunc (c cmdable) PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd {\n\targs := []interface{}{\"pubsub\", \"channels\"}\n\tif pattern != \"*\" {\n\t\targs = append(args, pattern)\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd {\n\targs := make([]interface{}, 2+len(channels))\n\targs[0] = \"pubsub\"\n\targs[1] = \"numsub\"\n\tfor i, channel := range channels {\n\t\targs[2+i] = channel\n\t}\n\tcmd := NewMapStringIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd {\n\targs := []interface{}{\"pubsub\", \"shardchannels\"}\n\tif pattern != \"*\" {\n\t\targs = append(args, pattern)\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd {\n\targs := make([]interface{}, 2+len(channels))\n\targs[0] = \"pubsub\"\n\targs[1] = \"shardnumsub\"\n\tfor i, channel := range channels {\n\t\targs[2+i] = channel\n\t}\n\tcmd := NewMapStringIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) PubSubNumPat(ctx context.Context) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"pubsub\", \"numpat\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "pubsub_test.go",
    "content": "package redis_test\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"PubSub\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\topt := redisOptions()\n\t\topt.MinIdleConns = 0\n\t\topt.ConnMaxLifetime = 0\n\t\tclient = redis.NewClient(opt)\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"implements Stringer\", func() {\n\t\tpubsub := client.PSubscribe(ctx, \"mychannel*\")\n\t\tdefer pubsub.Close()\n\n\t\tExpect(pubsub.String()).To(Equal(\"PubSub(mychannel*)\"))\n\t})\n\n\tIt(\"should support pattern matching\", func() {\n\t\tpubsub := client.PSubscribe(ctx, \"mychannel*\")\n\t\tdefer pubsub.Close()\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"psubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel*\"))\n\t\t\tExpect(subscr.Count).To(Equal(1))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err.(net.Error).Timeout()).To(Equal(true))\n\t\t\tExpect(msgi).To(BeNil())\n\t\t}\n\n\t\tn, err := client.Publish(ctx, \"mychannel1\", \"hello\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(1)))\n\n\t\tExpect(pubsub.PUnsubscribe(ctx, \"mychannel*\")).NotTo(HaveOccurred())\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Message)\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel1\"))\n\t\t\tExpect(subscr.Pattern).To(Equal(\"mychannel*\"))\n\t\t\tExpect(subscr.Payload).To(Equal(\"hello\"))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"punsubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel*\"))\n\t\t\tExpect(subscr.Count).To(Equal(0))\n\t\t}\n\n\t\tstats := client.PoolStats()\n\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t})\n\n\tIt(\"should pub/sub channels\", func() {\n\t\tchannels, err := client.PubSubChannels(ctx, \"mychannel*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(channels).To(BeEmpty())\n\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\", \"mychannel2\")\n\t\tdefer pubsub.Close()\n\n\t\t// sleep a bit to make sure redis knows about the subscriptions\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tchannels, err = client.PubSubChannels(ctx, \"mychannel*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(channels).To(ConsistOf([]string{\"mychannel\", \"mychannel2\"}))\n\n\t\tchannels, err = client.PubSubChannels(ctx, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(channels).To(BeEmpty())\n\n\t\tchannels, err = client.PubSubChannels(ctx, \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(channels)).To(BeNumerically(\">=\", 2))\n\t})\n\n\tIt(\"should sharded pub/sub channels\", func() {\n\t\tchannels, err := client.PubSubShardChannels(ctx, \"mychannel*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(channels).To(BeEmpty())\n\n\t\tpubsub := client.SSubscribe(ctx, \"mychannel\", \"mychannel2\")\n\t\tdefer pubsub.Close()\n\n\t\t// Let Redis process the ssubscribe command.\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tchannels, err = client.PubSubShardChannels(ctx, \"mychannel*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(channels).To(ConsistOf([]string{\"mychannel\", \"mychannel2\"}))\n\n\t\tchannels, err = client.PubSubShardChannels(ctx, \"\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(channels).To(BeEmpty())\n\n\t\tchannels, err = client.PubSubShardChannels(ctx, \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(channels)).To(BeNumerically(\">=\", 2))\n\n\t\tnums, err := client.PubSubShardNumSub(ctx, \"mychannel\", \"mychannel2\", \"mychannel3\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(nums).To(Equal(map[string]int64{\n\t\t\t\"mychannel\":  1,\n\t\t\t\"mychannel2\": 1,\n\t\t\t\"mychannel3\": 0,\n\t\t}))\n\t})\n\n\tIt(\"should return the numbers of subscribers\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\", \"mychannel2\")\n\t\tdefer pubsub.Close()\n\n\t\t// sleep a bit to make sure redis knows about the subscriptions\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tchannels, err := client.PubSubNumSub(ctx, \"mychannel\", \"mychannel2\", \"mychannel3\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(channels).To(Equal(map[string]int64{\n\t\t\t\"mychannel\":  1,\n\t\t\t\"mychannel2\": 1,\n\t\t\t\"mychannel3\": 0,\n\t\t}))\n\t})\n\n\tIt(\"should return the numbers of subscribers by pattern\", func() {\n\t\tnum, err := client.PubSubNumPat(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(num).To(Equal(int64(0)))\n\n\t\tpubsub := client.PSubscribe(ctx, \"*\")\n\t\tdefer pubsub.Close()\n\n\t\t// sleep a bit to make sure redis knows about the subscriptions\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tnum, err = client.PubSubNumPat(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(num).To(Equal(int64(1)))\n\t})\n\n\tIt(\"should pub/sub\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\", \"mychannel2\")\n\t\tdefer pubsub.Close()\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"subscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel\"))\n\t\t\tExpect(subscr.Count).To(Equal(1))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"subscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel2\"))\n\t\t\tExpect(subscr.Count).To(Equal(2))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err.(net.Error).Timeout()).To(Equal(true))\n\t\t\tExpect(msgi).NotTo(HaveOccurred())\n\t\t}\n\n\t\tn, err := client.Publish(ctx, \"mychannel\", \"hello\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(1)))\n\n\t\tn, err = client.Publish(ctx, \"mychannel2\", \"hello2\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(1)))\n\n\t\tExpect(pubsub.Unsubscribe(ctx, \"mychannel\", \"mychannel2\")).NotTo(HaveOccurred())\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tmsg := msgi.(*redis.Message)\n\t\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\t\tExpect(msg.Payload).To(Equal(\"hello\"))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tmsg := msgi.(*redis.Message)\n\t\t\tExpect(msg.Channel).To(Equal(\"mychannel2\"))\n\t\t\tExpect(msg.Payload).To(Equal(\"hello2\"))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"unsubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel\"))\n\t\t\tExpect(subscr.Count).To(Equal(1))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"unsubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel2\"))\n\t\t\tExpect(subscr.Count).To(Equal(0))\n\t\t}\n\n\t\tstats := client.PoolStats()\n\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t})\n\n\tIt(\"should sharded pub/sub\", func() {\n\t\tpubsub := client.SSubscribe(ctx, \"mychannel\", \"mychannel2\")\n\t\tdefer pubsub.Close()\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"ssubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel\"))\n\t\t\tExpect(subscr.Count).To(Equal(1))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"ssubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel2\"))\n\t\t\tExpect(subscr.Count).To(Equal(2))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err.(net.Error).Timeout()).To(Equal(true))\n\t\t\tExpect(msgi).NotTo(HaveOccurred())\n\t\t}\n\n\t\tn, err := client.SPublish(ctx, \"mychannel\", \"hello\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(1)))\n\n\t\tn, err = client.SPublish(ctx, \"mychannel2\", \"hello2\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(1)))\n\n\t\tExpect(pubsub.SUnsubscribe(ctx, \"mychannel\", \"mychannel2\")).NotTo(HaveOccurred())\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tmsg := msgi.(*redis.Message)\n\t\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\t\tExpect(msg.Payload).To(Equal(\"hello\"))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tmsg := msgi.(*redis.Message)\n\t\t\tExpect(msg.Channel).To(Equal(\"mychannel2\"))\n\t\t\tExpect(msg.Payload).To(Equal(\"hello2\"))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"sunsubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel\"))\n\t\t\tExpect(subscr.Count).To(Equal(1))\n\t\t}\n\n\t\t{\n\t\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tsubscr := msgi.(*redis.Subscription)\n\t\t\tExpect(subscr.Kind).To(Equal(\"sunsubscribe\"))\n\t\t\tExpect(subscr.Channel).To(Equal(\"mychannel2\"))\n\t\t\tExpect(subscr.Count).To(Equal(0))\n\t\t}\n\n\t\tstats := client.PoolStats()\n\t\tExpect(stats.Misses).To(Equal(uint32(1)))\n\t})\n\n\tIt(\"should ping/pong\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\t_, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\terr = pubsub.Ping(ctx, \"\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tpong := msgi.(*redis.Pong)\n\t\tExpect(pong.Payload).To(Equal(\"\"))\n\t})\n\n\tIt(\"should ping/pong with payload\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\t_, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\terr = pubsub.Ping(ctx, \"hello\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tmsgi, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tpong := msgi.(*redis.Pong)\n\t\tExpect(pong.Payload).To(Equal(\"hello\"))\n\t})\n\n\tIt(\"should multi-ReceiveMessage\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\tsubscr, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(subscr).To(Equal(&redis.Subscription{\n\t\t\tKind:    \"subscribe\",\n\t\t\tChannel: \"mychannel\",\n\t\t\tCount:   1,\n\t\t}))\n\n\t\terr = client.Publish(ctx, \"mychannel\", \"hello\").Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\terr = client.Publish(ctx, \"mychannel\", \"world\").Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tmsg, err := pubsub.ReceiveMessage(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\tExpect(msg.Payload).To(Equal(\"hello\"))\n\n\t\tmsg, err = pubsub.ReceiveMessage(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\tExpect(msg.Payload).To(Equal(\"world\"))\n\t})\n\n\tIt(\"returns an error when subscribe fails\", func() {\n\t\tpubsub := client.Subscribe(ctx)\n\t\tdefer pubsub.Close()\n\n\t\tpubsub.SetNetConn(&badConn{\n\t\t\treadErr:  io.EOF,\n\t\t\twriteErr: io.EOF,\n\t\t})\n\n\t\terr := pubsub.Subscribe(ctx, \"mychannel\")\n\t\tExpect(err).To(MatchError(\"EOF\"))\n\n\t\terr = pubsub.Subscribe(ctx, \"mychannel\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\texpectReceiveMessageOnError := func(pubsub *redis.PubSub) {\n\t\tpubsub.SetNetConn(&badConn{\n\t\t\treadErr:  io.EOF,\n\t\t\twriteErr: io.EOF,\n\t\t})\n\n\t\tstep := make(chan struct{}, 3)\n\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\n\t\t\tEventually(step).Should(Receive())\n\t\t\terr := client.Publish(ctx, \"mychannel\", \"hello\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tstep <- struct{}{}\n\t\t}()\n\n\t\t_, err := pubsub.ReceiveMessage(ctx)\n\t\tExpect(err).To(Equal(io.EOF))\n\t\tstep <- struct{}{}\n\n\t\tmsg, err := pubsub.ReceiveMessage(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\tExpect(msg.Payload).To(Equal(\"hello\"))\n\n\t\tEventually(step).Should(Receive())\n\t}\n\n\tIt(\"Subscribe should reconnect on ReceiveMessage error\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\tsubscr, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(subscr).To(Equal(&redis.Subscription{\n\t\t\tKind:    \"subscribe\",\n\t\t\tChannel: \"mychannel\",\n\t\t\tCount:   1,\n\t\t}))\n\n\t\texpectReceiveMessageOnError(pubsub)\n\t})\n\n\tIt(\"PSubscribe should reconnect on ReceiveMessage error\", func() {\n\t\tpubsub := client.PSubscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\tsubscr, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(subscr).To(Equal(&redis.Subscription{\n\t\t\tKind:    \"psubscribe\",\n\t\t\tChannel: \"mychannel\",\n\t\t\tCount:   1,\n\t\t}))\n\n\t\texpectReceiveMessageOnError(pubsub)\n\t})\n\n\tIt(\"should return on Close\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\n\t\t\twg.Done()\n\t\t\tdefer wg.Done()\n\n\t\t\t_, err := pubsub.ReceiveMessage(ctx)\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(SatisfyAny(\n\t\t\t\tEqual(\"redis: client is closed\"),\n\t\t\t\tContainSubstring(\"use of closed network connection\"),\n\t\t\t))\n\t\t}()\n\n\t\twg.Wait()\n\t\twg.Add(1)\n\n\t\tExpect(pubsub.Close()).NotTo(HaveOccurred())\n\n\t\twg.Wait()\n\t})\n\n\tIt(\"should ReceiveMessage without a subscription\", func() {\n\t\ttimeout := 100 * time.Millisecond\n\n\t\tpubsub := client.Subscribe(ctx)\n\t\tdefer pubsub.Close()\n\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\t\t\tdefer wg.Done()\n\n\t\t\ttime.Sleep(timeout)\n\n\t\t\terr := pubsub.Subscribe(ctx, \"mychannel\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\ttime.Sleep(timeout)\n\n\t\t\terr = client.Publish(ctx, \"mychannel\", \"hello\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}()\n\n\t\tmsg, err := pubsub.ReceiveMessage(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\tExpect(msg.Payload).To(Equal(\"hello\"))\n\n\t\twg.Wait()\n\t})\n\n\tIt(\"handles big message payload\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\tch := pubsub.Channel()\n\n\t\tbigVal := bigVal()\n\t\terr := client.Publish(ctx, \"mychannel\", bigVal).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tvar msg *redis.Message\n\t\tEventually(ch).Should(Receive(&msg))\n\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\tExpect(msg.Payload).To(Equal(string(bigVal)))\n\t})\n\n\tIt(\"supports concurrent Ping and Receive\", func() {\n\t\tconst N = 100\n\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer GinkgoRecover()\n\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\t_, err := pubsub.ReceiveTimeout(ctx, 5*time.Second)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t\tclose(done)\n\t\t}()\n\n\t\tfor i := 0; i < N; i++ {\n\t\t\terr := pubsub.Ping(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\n\t\tselect {\n\t\tcase <-done:\n\t\tcase <-time.After(30 * time.Second):\n\t\t\tFail(\"timeout\")\n\t\t}\n\t})\n\n\tIt(\"should ClientSetName on PubSub connection\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"test-channel\")\n\t\tdefer pubsub.Close()\n\n\t\t// Wait for subscription to be established\n\t\t_, err := pubsub.Receive(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\terr = pubsub.ClientSetName(ctx, \"my-subscriber\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Verify the name is set via CLIENT LIST\n\t\tclientList, err := client.ClientList(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(clientList).To(ContainSubstring(\"name=my-subscriber\"))\n\t})\n\n\tIt(\"should ChannelMessage\", func() {\n\t\tpubsub := client.Subscribe(ctx, \"mychannel\")\n\t\tdefer pubsub.Close()\n\n\t\tch := pubsub.Channel(\n\t\t\tredis.WithChannelSize(10),\n\t\t\tredis.WithChannelHealthCheckInterval(time.Second),\n\t\t)\n\n\t\ttext := \"test channel message\"\n\t\terr := client.Publish(ctx, \"mychannel\", text).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tvar msg *redis.Message\n\t\tEventually(ch).Should(Receive(&msg))\n\t\tExpect(msg.Channel).To(Equal(\"mychannel\"))\n\t\tExpect(msg.Payload).To(Equal(text))\n\t})\n})\n"
  },
  {
    "path": "push/errors.go",
    "content": "package push\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\n// Push notification error definitions\n// This file contains all error types and messages used by the push notification system\n\n// Error reason constants\nconst (\n\t// HandlerReasons\n\tReasonHandlerNil       = \"handler cannot be nil\"\n\tReasonHandlerExists    = \"cannot overwrite existing handler\"\n\tReasonHandlerProtected = \"handler is protected\"\n\n\t// ProcessorReasons\n\tReasonPushNotificationsDisabled = \"push notifications are disabled\"\n)\n\n// ProcessorType represents the type of processor involved in the error\n// defined as a custom type for better readability and easier maintenance\ntype ProcessorType string\n\nconst (\n\t// ProcessorTypes\n\tProcessorTypeProcessor     = ProcessorType(\"processor\")\n\tProcessorTypeVoidProcessor = ProcessorType(\"void_processor\")\n\tProcessorTypeCustom        = ProcessorType(\"custom\")\n)\n\n// ProcessorOperation represents the operation being performed by the processor\n// defined as a custom type for better readability and easier maintenance\ntype ProcessorOperation string\n\nconst (\n\t// ProcessorOperations\n\tProcessorOperationProcess    = ProcessorOperation(\"process\")\n\tProcessorOperationRegister   = ProcessorOperation(\"register\")\n\tProcessorOperationUnregister = ProcessorOperation(\"unregister\")\n\tProcessorOperationUnknown    = ProcessorOperation(\"unknown\")\n)\n\n// Common error variables for reuse\nvar (\n\t// ErrHandlerNil is returned when attempting to register a nil handler\n\tErrHandlerNil = errors.New(ReasonHandlerNil)\n)\n\n// Registry errors\n\n// ErrHandlerExists creates an error for when attempting to overwrite an existing handler\nfunc ErrHandlerExists(pushNotificationName string) error {\n\treturn NewHandlerError(ProcessorOperationRegister, pushNotificationName, ReasonHandlerExists, nil)\n}\n\n// ErrProtectedHandler creates an error for when attempting to unregister a protected handler\nfunc ErrProtectedHandler(pushNotificationName string) error {\n\treturn NewHandlerError(ProcessorOperationUnregister, pushNotificationName, ReasonHandlerProtected, nil)\n}\n\n// VoidProcessor errors\n\n// ErrVoidProcessorRegister creates an error for when attempting to register a handler on void processor\nfunc ErrVoidProcessorRegister(pushNotificationName string) error {\n\treturn NewProcessorError(ProcessorTypeVoidProcessor, ProcessorOperationRegister, pushNotificationName, ReasonPushNotificationsDisabled, nil)\n}\n\n// ErrVoidProcessorUnregister creates an error for when attempting to unregister a handler on void processor\nfunc ErrVoidProcessorUnregister(pushNotificationName string) error {\n\treturn NewProcessorError(ProcessorTypeVoidProcessor, ProcessorOperationUnregister, pushNotificationName, ReasonPushNotificationsDisabled, nil)\n}\n\n// Error type definitions for advanced error handling\n\n// HandlerError represents errors related to handler operations\ntype HandlerError struct {\n\tOperation            ProcessorOperation\n\tPushNotificationName string\n\tReason               string\n\tErr                  error\n}\n\nfunc (e *HandlerError) Error() string {\n\tif e.Err != nil {\n\t\treturn fmt.Sprintf(\"handler %s failed for '%s': %s (%v)\", e.Operation, e.PushNotificationName, e.Reason, e.Err)\n\t}\n\treturn fmt.Sprintf(\"handler %s failed for '%s': %s\", e.Operation, e.PushNotificationName, e.Reason)\n}\n\nfunc (e *HandlerError) Unwrap() error {\n\treturn e.Err\n}\n\n// NewHandlerError creates a new HandlerError\nfunc NewHandlerError(operation ProcessorOperation, pushNotificationName, reason string, err error) *HandlerError {\n\treturn &HandlerError{\n\t\tOperation:            operation,\n\t\tPushNotificationName: pushNotificationName,\n\t\tReason:               reason,\n\t\tErr:                  err,\n\t}\n}\n\n// ProcessorError represents errors related to processor operations\ntype ProcessorError struct {\n\tProcessorType        ProcessorType      // \"processor\", \"void_processor\"\n\tOperation            ProcessorOperation // \"process\", \"register\", \"unregister\"\n\tPushNotificationName string             // Name of the push notification involved\n\tReason               string\n\tErr                  error\n}\n\nfunc (e *ProcessorError) Error() string {\n\tnotifInfo := \"\"\n\tif e.PushNotificationName != \"\" {\n\t\tnotifInfo = fmt.Sprintf(\" for '%s'\", e.PushNotificationName)\n\t}\n\tif e.Err != nil {\n\t\treturn fmt.Sprintf(\"%s %s failed%s: %s (%v)\", e.ProcessorType, e.Operation, notifInfo, e.Reason, e.Err)\n\t}\n\treturn fmt.Sprintf(\"%s %s failed%s: %s\", e.ProcessorType, e.Operation, notifInfo, e.Reason)\n}\n\nfunc (e *ProcessorError) Unwrap() error {\n\treturn e.Err\n}\n\n// NewProcessorError creates a new ProcessorError\nfunc NewProcessorError(processorType ProcessorType, operation ProcessorOperation, pushNotificationName, reason string, err error) *ProcessorError {\n\treturn &ProcessorError{\n\t\tProcessorType:        processorType,\n\t\tOperation:            operation,\n\t\tPushNotificationName: pushNotificationName,\n\t\tReason:               reason,\n\t\tErr:                  err,\n\t}\n}\n\n// Helper functions for common error scenarios\n\n// IsHandlerNilError checks if an error is due to a nil handler\nfunc IsHandlerNilError(err error) bool {\n\treturn errors.Is(err, ErrHandlerNil)\n}\n\n// IsHandlerExistsError checks if an error is due to attempting to overwrite an existing handler.\n// This function works correctly even when the error is wrapped.\nfunc IsHandlerExistsError(err error) bool {\n\tvar handlerErr *HandlerError\n\tif errors.As(err, &handlerErr) {\n\t\treturn handlerErr.Operation == ProcessorOperationRegister && handlerErr.Reason == ReasonHandlerExists\n\t}\n\treturn false\n}\n\n// IsProtectedHandlerError checks if an error is due to attempting to unregister a protected handler.\n// This function works correctly even when the error is wrapped.\nfunc IsProtectedHandlerError(err error) bool {\n\tvar handlerErr *HandlerError\n\tif errors.As(err, &handlerErr) {\n\t\treturn handlerErr.Operation == ProcessorOperationUnregister && handlerErr.Reason == ReasonHandlerProtected\n\t}\n\treturn false\n}\n\n// IsVoidProcessorError checks if an error is due to void processor operations.\n// This function works correctly even when the error is wrapped.\nfunc IsVoidProcessorError(err error) bool {\n\tvar procErr *ProcessorError\n\tif errors.As(err, &procErr) {\n\t\treturn procErr.ProcessorType == ProcessorTypeVoidProcessor && procErr.Reason == ReasonPushNotificationsDisabled\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "push/handler.go",
    "content": "package push\n\nimport (\n\t\"context\"\n)\n\n// NotificationHandler defines the interface for push notification handlers.\ntype NotificationHandler interface {\n\t// HandlePushNotification processes a push notification with context information.\n\t// The handlerCtx provides information about the client, connection pool, and connection\n\t// on which the notification was received, allowing handlers to make informed decisions.\n\t// Returns an error if the notification could not be handled.\n\tHandlePushNotification(ctx context.Context, handlerCtx NotificationHandlerContext, notification []interface{}) error\n}\n"
  },
  {
    "path": "push/handler_context.go",
    "content": "package push\n\n// No imports needed for this file\n\n// NotificationHandlerContext provides context information about where a push notification was received.\n// This struct allows handlers to make informed decisions based on the source of the notification\n// with strongly typed access to different client types using concrete types.\ntype NotificationHandlerContext struct {\n\t// Client is the Redis client instance that received the notification.\n\t// It is interface to both allow for future expansion and to avoid\n\t// circular dependencies. The developer is responsible for type assertion.\n\t// It can be one of the following types:\n\t// - *redis.baseClient\n\t// - *redis.Client\n\t// - *redis.ClusterClient\n\t// - *redis.Conn\n\tClient interface{}\n\n\t// ConnPool is the connection pool from which the connection was obtained.\n\t// It is interface to both allow for future expansion and to avoid\n\t// circular dependencies. The developer is responsible for type assertion.\n\t// It can be one of the following types:\n\t// - *pool.ConnPool\n\t// - *pool.SingleConnPool\n\t// - *pool.StickyConnPool\n\tConnPool interface{}\n\n\t// PubSub is the PubSub instance that received the notification.\n\t// It is interface to both allow for future expansion and to avoid\n\t// circular dependencies. The developer is responsible for type assertion.\n\t// It can be one of the following types:\n\t// - *redis.PubSub\n\tPubSub interface{}\n\n\t// Conn is the specific connection on which the notification was received.\n\t// It is interface to both allow for future expansion and to avoid\n\t// circular dependencies. The developer is responsible for type assertion.\n\t// It can be one of the following types:\n\t// - *pool.Conn\n\tConn interface{}\n\n\t// IsBlocking indicates if the notification was received on a blocking connection.\n\tIsBlocking bool\n}\n"
  },
  {
    "path": "push/processor.go",
    "content": "package push\n\nimport (\n\t\"context\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\n// NotificationProcessor defines the interface for push notification processors.\ntype NotificationProcessor interface {\n\t// GetHandler returns the handler for a specific push notification name.\n\tGetHandler(pushNotificationName string) NotificationHandler\n\t// ProcessPendingNotifications checks for and processes any pending push notifications.\n\t// To be used when it is known that there are notifications on the socket.\n\t// It will try to read from the socket and if it is empty - it may block.\n\tProcessPendingNotifications(ctx context.Context, handlerCtx NotificationHandlerContext, rd *proto.Reader) error\n\t// RegisterHandler registers a handler for a specific push notification name.\n\tRegisterHandler(pushNotificationName string, handler NotificationHandler, protected bool) error\n\t// UnregisterHandler removes a handler for a specific push notification name.\n\tUnregisterHandler(pushNotificationName string) error\n}\n\n// Processor handles push notifications with a registry of handlers\ntype Processor struct {\n\tregistry *Registry\n}\n\n// NewProcessor creates a new push notification processor\nfunc NewProcessor() *Processor {\n\treturn &Processor{\n\t\tregistry: NewRegistry(),\n\t}\n}\n\n// GetHandler returns the handler for a specific push notification name\nfunc (p *Processor) GetHandler(pushNotificationName string) NotificationHandler {\n\treturn p.registry.GetHandler(pushNotificationName)\n}\n\n// RegisterHandler registers a handler for a specific push notification name\nfunc (p *Processor) RegisterHandler(pushNotificationName string, handler NotificationHandler, protected bool) error {\n\treturn p.registry.RegisterHandler(pushNotificationName, handler, protected)\n}\n\n// UnregisterHandler removes a handler for a specific push notification name\nfunc (p *Processor) UnregisterHandler(pushNotificationName string) error {\n\treturn p.registry.UnregisterHandler(pushNotificationName)\n}\n\n// ProcessPendingNotifications checks for and processes any pending push notifications\n// This method should be called by the client in WithReader before reading the reply\n// It will try to read from the socket and if it is empty - it may block.\nfunc (p *Processor) ProcessPendingNotifications(ctx context.Context, handlerCtx NotificationHandlerContext, rd *proto.Reader) error {\n\tif rd == nil {\n\t\treturn nil\n\t}\n\n\tfor {\n\t\t// Check if there's data available to read\n\t\treplyType, err := rd.PeekReplyType()\n\t\tif err != nil {\n\t\t\t// No more data available or error reading\n\t\t\t// if timeout, it will be handled by the caller\n\t\t\tbreak\n\t\t}\n\n\t\t// Only process push notifications (arrays starting with >)\n\t\tif replyType != proto.RespPush {\n\t\t\tbreak\n\t\t}\n\n\t\t// see if we should skip this notification\n\t\tnotificationName, err := rd.PeekPushNotificationName()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif willHandleNotificationInClient(notificationName) {\n\t\t\tbreak\n\t\t}\n\n\t\t// Read the push notification\n\t\treply, err := rd.ReadReply()\n\t\tif err != nil {\n\t\t\tinternal.Logger.Printf(ctx, \"push: error reading push notification: %v\", err)\n\t\t\tbreak\n\t\t}\n\n\t\t// Convert to slice of interfaces\n\t\tnotification, ok := reply.([]interface{})\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\n\t\t// Handle the notification directly\n\t\tif len(notification) > 0 {\n\t\t\t// Extract the notification type (first element)\n\t\t\tif notificationType, ok := notification[0].(string); ok {\n\t\t\t\t// Get the handler for this notification type\n\t\t\t\tif handler := p.registry.GetHandler(notificationType); handler != nil {\n\t\t\t\t\t// Handle the notification\n\t\t\t\t\terr := handler.HandlePushNotification(ctx, handlerCtx, notification)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tinternal.Logger.Printf(ctx, \"push: error handling push notification: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// VoidProcessor discards all push notifications without processing them\ntype VoidProcessor struct{}\n\n// NewVoidProcessor creates a new void push notification processor\nfunc NewVoidProcessor() *VoidProcessor {\n\treturn &VoidProcessor{}\n}\n\n// GetHandler returns nil for void processor since it doesn't maintain handlers\nfunc (v *VoidProcessor) GetHandler(_ string) NotificationHandler {\n\treturn nil\n}\n\n// RegisterHandler returns an error for void processor since it doesn't maintain handlers\nfunc (v *VoidProcessor) RegisterHandler(pushNotificationName string, _ NotificationHandler, _ bool) error {\n\treturn ErrVoidProcessorRegister(pushNotificationName)\n}\n\n// UnregisterHandler returns an error for void processor since it doesn't maintain handlers\nfunc (v *VoidProcessor) UnregisterHandler(pushNotificationName string) error {\n\treturn ErrVoidProcessorUnregister(pushNotificationName)\n}\n\n// ProcessPendingNotifications for VoidProcessor does nothing since push notifications\n// are only available in RESP3 and this processor is used for RESP2 connections.\n// This avoids unnecessary buffer scanning overhead.\n// It does however read and discard all push notifications from the buffer to avoid\n// them being interpreted as a reply.\n// This method should be called by the client in WithReader before reading the reply\n// to be sure there are no buffered push notifications.\n// It will try to read from the socket and if it is empty - it may block.\nfunc (v *VoidProcessor) ProcessPendingNotifications(_ context.Context, handlerCtx NotificationHandlerContext, rd *proto.Reader) error {\n\t// read and discard all push notifications\n\tif rd == nil {\n\t\treturn nil\n\t}\n\n\tfor {\n\t\t// Check if there's data available to read\n\t\treplyType, err := rd.PeekReplyType()\n\t\tif err != nil {\n\t\t\t// No more data available or error reading\n\t\t\t// if timeout, it will be handled by the caller\n\t\t\tbreak\n\t\t}\n\n\t\t// Only process push notifications (arrays starting with >)\n\t\tif replyType != proto.RespPush {\n\t\t\tbreak\n\t\t}\n\t\t// see if we should skip this notification\n\t\tnotificationName, err := rd.PeekPushNotificationName()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif willHandleNotificationInClient(notificationName) {\n\t\t\tbreak\n\t\t}\n\n\t\t// Read the push notification\n\t\t_, err = rd.ReadReply()\n\t\tif err != nil {\n\t\t\tinternal.Logger.Printf(context.Background(), \"push: error reading push notification: %v\", err)\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn nil\n}\n\n// willHandleNotificationInClient checks if a notification type should be ignored by the push notification\n// processor and handled by other specialized systems instead (pub/sub, streams, keyspace, etc.).\nfunc willHandleNotificationInClient(notificationType string) bool {\n\tswitch notificationType {\n\t// Pub/Sub notifications - handled by pub/sub system\n\tcase \"message\", // Regular pub/sub message\n\t\t\"pmessage\",     // Pattern pub/sub message\n\t\t\"subscribe\",    // Subscription confirmation\n\t\t\"unsubscribe\",  // Unsubscription confirmation\n\t\t\"psubscribe\",   // Pattern subscription confirmation\n\t\t\"punsubscribe\", // Pattern unsubscription confirmation\n\t\t\"smessage\",     // Sharded pub/sub message (Redis 7.0+)\n\t\t\"ssubscribe\",   // Sharded subscription confirmation\n\t\t\"sunsubscribe\": // Sharded unsubscription confirmation\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "push/processor_unit_test.go",
    "content": "package push\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n)\n\n// TestProcessorCreation tests processor creation and initialization\nfunc TestProcessorCreation(t *testing.T) {\n\tt.Run(\"NewProcessor\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\tif processor == nil {\n\t\t\tt.Fatal(\"NewProcessor should not return nil\")\n\t\t}\n\t\tif processor.registry == nil {\n\t\t\tt.Error(\"Processor should have a registry\")\n\t\t}\n\t})\n\n\tt.Run(\"NewVoidProcessor\", func(t *testing.T) {\n\t\tvoidProcessor := NewVoidProcessor()\n\t\tif voidProcessor == nil {\n\t\t\tt.Fatal(\"NewVoidProcessor should not return nil\")\n\t\t}\n\t})\n}\n\n// TestProcessorHandlerManagement tests handler registration and retrieval\nfunc TestProcessorHandlerManagement(t *testing.T) {\n\tprocessor := NewProcessor()\n\thandler := &UnitTestHandler{name: \"test-handler\"}\n\n\tt.Run(\"RegisterHandler\", func(t *testing.T) {\n\t\terr := processor.RegisterHandler(\"TEST\", handler, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Verify handler is registered\n\t\tretrievedHandler := processor.GetHandler(\"TEST\")\n\t\tif retrievedHandler != handler {\n\t\t\tt.Error(\"GetHandler should return the registered handler\")\n\t\t}\n\t})\n\n\tt.Run(\"RegisterProtectedHandler\", func(t *testing.T) {\n\t\tprotectedHandler := &UnitTestHandler{name: \"protected-handler\"}\n\t\terr := processor.RegisterHandler(\"PROTECTED\", protectedHandler, true)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error for protected handler: %v\", err)\n\t\t}\n\n\t\t// Verify handler is registered\n\t\tretrievedHandler := processor.GetHandler(\"PROTECTED\")\n\t\tif retrievedHandler != protectedHandler {\n\t\t\tt.Error(\"GetHandler should return the protected handler\")\n\t\t}\n\t})\n\n\tt.Run(\"GetNonExistentHandler\", func(t *testing.T) {\n\t\thandler := processor.GetHandler(\"NONEXISTENT\")\n\t\tif handler != nil {\n\t\t\tt.Error(\"GetHandler should return nil for non-existent handler\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterHandler\", func(t *testing.T) {\n\t\terr := processor.UnregisterHandler(\"TEST\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"UnregisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Verify handler is removed\n\t\tretrievedHandler := processor.GetHandler(\"TEST\")\n\t\tif retrievedHandler != nil {\n\t\t\tt.Error(\"GetHandler should return nil after unregistering\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterProtectedHandler\", func(t *testing.T) {\n\t\terr := processor.UnregisterHandler(\"PROTECTED\")\n\t\tif err == nil {\n\t\t\tt.Error(\"UnregisterHandler should error for protected handler\")\n\t\t}\n\n\t\t// Verify handler is still there\n\t\tretrievedHandler := processor.GetHandler(\"PROTECTED\")\n\t\tif retrievedHandler == nil {\n\t\t\tt.Error(\"Protected handler should not be removed\")\n\t\t}\n\t})\n}\n\n// TestVoidProcessorBehavior tests void processor behavior\nfunc TestVoidProcessorBehavior(t *testing.T) {\n\tvoidProcessor := NewVoidProcessor()\n\thandler := &UnitTestHandler{name: \"test-handler\"}\n\n\tt.Run(\"GetHandler\", func(t *testing.T) {\n\t\tretrievedHandler := voidProcessor.GetHandler(\"ANY\")\n\t\tif retrievedHandler != nil {\n\t\t\tt.Error(\"VoidProcessor GetHandler should always return nil\")\n\t\t}\n\t})\n\n\tt.Run(\"RegisterHandler\", func(t *testing.T) {\n\t\terr := voidProcessor.RegisterHandler(\"TEST\", handler, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"VoidProcessor RegisterHandler should return error\")\n\t\t}\n\n\t\t// Check error type\n\t\tif !IsVoidProcessorError(err) {\n\t\t\tt.Error(\"Error should be a VoidProcessorError\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterHandler\", func(t *testing.T) {\n\t\terr := voidProcessor.UnregisterHandler(\"TEST\")\n\t\tif err == nil {\n\t\t\tt.Error(\"VoidProcessor UnregisterHandler should return error\")\n\t\t}\n\n\t\t// Check error type\n\t\tif !IsVoidProcessorError(err) {\n\t\t\tt.Error(\"Error should be a VoidProcessorError\")\n\t\t}\n\t})\n}\n\n// TestProcessPendingNotificationsNilReader tests handling of nil reader\nfunc TestProcessPendingNotificationsNilReader(t *testing.T) {\n\tt.Run(\"ProcessorWithNilReader\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error with nil reader: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"VoidProcessorWithNilReader\", func(t *testing.T) {\n\t\tvoidProcessor := NewVoidProcessor()\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{}\n\n\t\terr := voidProcessor.ProcessPendingNotifications(ctx, handlerCtx, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"VoidProcessor ProcessPendingNotifications should not error with nil reader: %v\", err)\n\t\t}\n\t})\n}\n\n// TestWillHandleNotificationInClient tests the notification filtering logic\nfunc TestWillHandleNotificationInClient(t *testing.T) {\n\ttestCases := []struct {\n\t\tname             string\n\t\tnotificationType string\n\t\tshouldHandle     bool\n\t}{\n\t\t// Pub/Sub notifications (should be handled in client)\n\t\t{\"message\", \"message\", true},\n\t\t{\"pmessage\", \"pmessage\", true},\n\t\t{\"subscribe\", \"subscribe\", true},\n\t\t{\"unsubscribe\", \"unsubscribe\", true},\n\t\t{\"psubscribe\", \"psubscribe\", true},\n\t\t{\"punsubscribe\", \"punsubscribe\", true},\n\t\t{\"smessage\", \"smessage\", true},\n\t\t{\"ssubscribe\", \"ssubscribe\", true},\n\t\t{\"sunsubscribe\", \"sunsubscribe\", true},\n\n\t\t// Push notifications (should be handled by processor)\n\t\t{\"MOVING\", \"MOVING\", false},\n\t\t{\"MIGRATING\", \"MIGRATING\", false},\n\t\t{\"MIGRATED\", \"MIGRATED\", false},\n\t\t{\"FAILING_OVER\", \"FAILING_OVER\", false},\n\t\t{\"FAILED_OVER\", \"FAILED_OVER\", false},\n\t\t{\"custom\", \"custom\", false},\n\t\t{\"unknown\", \"unknown\", false},\n\t\t{\"empty\", \"\", false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := willHandleNotificationInClient(tc.notificationType)\n\t\t\tif result != tc.shouldHandle {\n\t\t\t\tt.Errorf(\"willHandleNotificationInClient(%q) = %v, want %v\", tc.notificationType, result, tc.shouldHandle)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestProcessorErrorHandlingUnit tests error handling scenarios\nfunc TestProcessorErrorHandlingUnit(t *testing.T) {\n\tprocessor := NewProcessor()\n\n\tt.Run(\"RegisterNilHandler\", func(t *testing.T) {\n\t\terr := processor.RegisterHandler(\"TEST\", nil, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error with nil handler\")\n\t\t}\n\n\t\t// Check error type\n\t\tif !IsHandlerNilError(err) {\n\t\t\tt.Error(\"Error should be a HandlerNilError\")\n\t\t}\n\t})\n\n\tt.Run(\"RegisterDuplicateHandler\", func(t *testing.T) {\n\t\thandler1 := &UnitTestHandler{name: \"handler1\"}\n\t\thandler2 := &UnitTestHandler{name: \"handler2\"}\n\n\t\t// Register first handler\n\t\terr := processor.RegisterHandler(\"DUPLICATE\", handler1, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"First RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Try to register second handler with same name\n\t\terr = processor.RegisterHandler(\"DUPLICATE\", handler2, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when registering duplicate handler\")\n\t\t}\n\n\t\t// Verify original handler is still there\n\t\tretrievedHandler := processor.GetHandler(\"DUPLICATE\")\n\t\tif retrievedHandler != handler1 {\n\t\t\tt.Error(\"Original handler should remain after failed duplicate registration\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterNonExistentHandler\", func(t *testing.T) {\n\t\terr := processor.UnregisterHandler(\"NONEXISTENT\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"UnregisterHandler should not error for non-existent handler: %v\", err)\n\t\t}\n\t})\n}\n\n// TestProcessorConcurrentAccess tests concurrent access to processor\nfunc TestProcessorConcurrentAccess(t *testing.T) {\n\tprocessor := NewProcessor()\n\n\tt.Run(\"ConcurrentRegisterAndGet\", func(t *testing.T) {\n\t\tdone := make(chan bool, 2)\n\n\t\t// Goroutine 1: Register handlers\n\t\tgo func() {\n\t\t\tdefer func() { done <- true }()\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\thandler := &UnitTestHandler{name: \"concurrent-handler\"}\n\t\t\t\tprocessor.RegisterHandler(\"CONCURRENT\", handler, false)\n\t\t\t\tprocessor.UnregisterHandler(\"CONCURRENT\")\n\t\t\t}\n\t\t}()\n\n\t\t// Goroutine 2: Get handlers\n\t\tgo func() {\n\t\t\tdefer func() { done <- true }()\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tprocessor.GetHandler(\"CONCURRENT\")\n\t\t\t}\n\t\t}()\n\n\t\t// Wait for both goroutines to complete\n\t\t<-done\n\t\t<-done\n\t})\n}\n\n// TestProcessorInterfaceCompliance tests interface compliance\nfunc TestProcessorInterfaceCompliance(t *testing.T) {\n\tt.Run(\"ProcessorImplementsInterface\", func(t *testing.T) {\n\t\tvar _ NotificationProcessor = (*Processor)(nil)\n\t})\n\n\tt.Run(\"VoidProcessorImplementsInterface\", func(t *testing.T) {\n\t\tvar _ NotificationProcessor = (*VoidProcessor)(nil)\n\t})\n}\n\n// UnitTestHandler is a test implementation of NotificationHandler\ntype UnitTestHandler struct {\n\tname             string\n\tlastNotification []interface{}\n\terrorToReturn    error\n\tcallCount        int\n}\n\nfunc (h *UnitTestHandler) HandlePushNotification(ctx context.Context, handlerCtx NotificationHandlerContext, notification []interface{}) error {\n\th.callCount++\n\th.lastNotification = notification\n\treturn h.errorToReturn\n}\n\n// Helper methods for UnitTestHandler\nfunc (h *UnitTestHandler) GetCallCount() int {\n\treturn h.callCount\n}\n\nfunc (h *UnitTestHandler) GetLastNotification() []interface{} {\n\treturn h.lastNotification\n}\n\nfunc (h *UnitTestHandler) SetErrorToReturn(err error) {\n\th.errorToReturn = err\n}\n\nfunc (h *UnitTestHandler) Reset() {\n\th.callCount = 0\n\th.lastNotification = nil\n\th.errorToReturn = nil\n}\n\n// TestErrorWrapping tests that error checking functions work with wrapped errors\nfunc TestErrorWrapping(t *testing.T) {\n\tt.Run(\"IsHandlerExistsError with wrapped error\", func(t *testing.T) {\n\t\t// Create a HandlerError\n\t\thandlerErr := ErrHandlerExists(\"test-notification\")\n\n\t\t// Wrap it\n\t\twrappedErr := fmt.Errorf(\"operation failed: %w\", handlerErr)\n\t\tdoubleWrappedErr := fmt.Errorf(\"context: %w\", wrappedErr)\n\n\t\t// Should still be detected through wrapping\n\t\tif !IsHandlerExistsError(doubleWrappedErr) {\n\t\t\tt.Errorf(\"IsHandlerExistsError should detect wrapped error\")\n\t\t}\n\n\t\t// Verify it doesn't match other error types\n\t\tif IsProtectedHandlerError(doubleWrappedErr) {\n\t\t\tt.Errorf(\"IsProtectedHandlerError should not match handler exists error\")\n\t\t}\n\t})\n\n\tt.Run(\"IsProtectedHandlerError with wrapped error\", func(t *testing.T) {\n\t\t// Create a protected handler error\n\t\tprotectedErr := ErrProtectedHandler(\"protected-notification\")\n\n\t\t// Wrap it\n\t\twrappedErr := fmt.Errorf(\"unregister failed: %w\", protectedErr)\n\n\t\t// Should still be detected through wrapping\n\t\tif !IsProtectedHandlerError(wrappedErr) {\n\t\t\tt.Errorf(\"IsProtectedHandlerError should detect wrapped error\")\n\t\t}\n\n\t\t// Verify it doesn't match other error types\n\t\tif IsHandlerExistsError(wrappedErr) {\n\t\t\tt.Errorf(\"IsHandlerExistsError should not match protected handler error\")\n\t\t}\n\t})\n\n\tt.Run(\"IsVoidProcessorError with wrapped error\", func(t *testing.T) {\n\t\t// Create a void processor error\n\t\tvoidErr := ErrVoidProcessorRegister(\"test-notification\")\n\n\t\t// Wrap it multiple times\n\t\twrappedErr := fmt.Errorf(\"register failed: %w\", voidErr)\n\t\tdoubleWrappedErr := fmt.Errorf(\"processor error: %w\", wrappedErr)\n\n\t\t// Should still be detected through wrapping\n\t\tif !IsVoidProcessorError(doubleWrappedErr) {\n\t\t\tt.Errorf(\"IsVoidProcessorError should detect wrapped error\")\n\t\t}\n\t})\n\n\tt.Run(\"IsHandlerNilError with wrapped error\", func(t *testing.T) {\n\t\t// Wrap the nil handler error\n\t\twrappedErr := fmt.Errorf(\"validation failed: %w\", ErrHandlerNil)\n\n\t\t// Should still be detected through wrapping\n\t\tif !IsHandlerNilError(wrappedErr) {\n\t\t\tt.Errorf(\"IsHandlerNilError should detect wrapped error\")\n\t\t}\n\t})\n\n\tt.Run(\"Error functions return false for non-matching errors\", func(t *testing.T) {\n\t\t// Create a different error\n\t\totherErr := fmt.Errorf(\"some other error\")\n\n\t\tif IsHandlerExistsError(otherErr) {\n\t\t\tt.Errorf(\"IsHandlerExistsError should return false for non-matching error\")\n\t\t}\n\t\tif IsProtectedHandlerError(otherErr) {\n\t\t\tt.Errorf(\"IsProtectedHandlerError should return false for non-matching error\")\n\t\t}\n\t\tif IsVoidProcessorError(otherErr) {\n\t\t\tt.Errorf(\"IsVoidProcessorError should return false for non-matching error\")\n\t\t}\n\t\tif IsHandlerNilError(otherErr) {\n\t\t\tt.Errorf(\"IsHandlerNilError should return false for non-matching error\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "push/push.go",
    "content": "// Package push provides push notifications for Redis.\n// This is an EXPERIMENTAL API for handling push notifications from Redis.\n// It is not yet stable and may change in the future.\n// Although this is in a public package, in its current form public use is not advised.\n// Pending push notifications should be processed before executing any readReply from the connection\n// as per RESP3 specification push notifications can be sent at any time.\npackage push\n"
  },
  {
    "path": "push/push_test.go",
    "content": "package push\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\n// TestHandler implements NotificationHandler interface for testing\ntype TestHandler struct {\n\tname        string\n\thandled     [][]interface{}\n\treturnError error\n}\n\nfunc NewTestHandler(name string) *TestHandler {\n\treturn &TestHandler{\n\t\tname:    name,\n\t\thandled: make([][]interface{}, 0),\n\t}\n}\n\n// MockNetConn implements net.Conn for testing\ntype MockNetConn struct{}\n\nfunc (m *MockNetConn) Read(b []byte) (n int, err error)   { return 0, nil }\nfunc (m *MockNetConn) Write(b []byte) (n int, err error)  { return len(b), nil }\nfunc (m *MockNetConn) Close() error                       { return nil }\nfunc (m *MockNetConn) LocalAddr() net.Addr                { return nil }\nfunc (m *MockNetConn) RemoteAddr() net.Addr               { return nil }\nfunc (m *MockNetConn) SetDeadline(t time.Time) error      { return nil }\nfunc (m *MockNetConn) SetReadDeadline(t time.Time) error  { return nil }\nfunc (m *MockNetConn) SetWriteDeadline(t time.Time) error { return nil }\n\nfunc (h *TestHandler) HandlePushNotification(ctx context.Context, handlerCtx NotificationHandlerContext, notification []interface{}) error {\n\th.handled = append(h.handled, notification)\n\treturn h.returnError\n}\n\nfunc (h *TestHandler) GetHandledNotifications() [][]interface{} {\n\treturn h.handled\n}\n\nfunc (h *TestHandler) SetReturnError(err error) {\n\th.returnError = err\n}\n\nfunc (h *TestHandler) Reset() {\n\th.handled = make([][]interface{}, 0)\n\th.returnError = nil\n}\n\n// Mock client types for testing\ntype MockClient struct {\n\tname string\n}\n\ntype MockConnPool struct {\n\tname string\n}\n\ntype MockPubSub struct {\n\tname string\n}\n\n// TestNotificationHandlerContext tests the handler context implementation\nfunc TestNotificationHandlerContext(t *testing.T) {\n\tt.Run(\"DirectObjectCreation\", func(t *testing.T) {\n\t\tclient := &MockClient{name: \"test-client\"}\n\t\tconnPool := &MockConnPool{name: \"test-pool\"}\n\t\tpubSub := &MockPubSub{name: \"test-pubsub\"}\n\t\tconn := &pool.Conn{}\n\n\t\tctx := NotificationHandlerContext{\n\t\t\tClient:     client,\n\t\t\tConnPool:   connPool,\n\t\t\tPubSub:     pubSub,\n\t\t\tConn:       conn,\n\t\t\tIsBlocking: true,\n\t\t}\n\n\t\tif ctx.Client != client {\n\t\t\tt.Error(\"Client field should contain the provided client\")\n\t\t}\n\n\t\tif ctx.ConnPool != connPool {\n\t\t\tt.Error(\"ConnPool field should contain the provided connection pool\")\n\t\t}\n\n\t\tif ctx.PubSub != pubSub {\n\t\t\tt.Error(\"PubSub field should contain the provided PubSub\")\n\t\t}\n\n\t\tif ctx.Conn != conn {\n\t\t\tt.Error(\"Conn field should contain the provided connection\")\n\t\t}\n\n\t\tif !ctx.IsBlocking {\n\t\t\tt.Error(\"IsBlocking field should be true\")\n\t\t}\n\t})\n\n\tt.Run(\"NilValues\", func(t *testing.T) {\n\t\tctx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\tif ctx.Client != nil {\n\t\t\tt.Error(\"Client field should be nil when client is nil\")\n\t\t}\n\n\t\tif ctx.ConnPool != nil {\n\t\t\tt.Error(\"ConnPool field should be nil when connPool is nil\")\n\t\t}\n\n\t\tif ctx.PubSub != nil {\n\t\t\tt.Error(\"PubSub field should be nil when pubSub is nil\")\n\t\t}\n\n\t\tif ctx.Conn != nil {\n\t\t\tt.Error(\"Conn field should be nil when conn is nil\")\n\t\t}\n\n\t\tif ctx.IsBlocking {\n\t\t\tt.Error(\"IsBlocking field should be false\")\n\t\t}\n\t})\n}\n\n// TestRegistry tests the registry implementation\nfunc TestRegistry(t *testing.T) {\n\tt.Run(\"NewRegistry\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\tif registry == nil {\n\t\t\tt.Fatal(\"NewRegistry should not return nil\")\n\t\t}\n\n\t\tif registry.handlers == nil {\n\t\t\tt.Error(\"Registry handlers map should be initialized\")\n\t\t}\n\n\t\tif registry.protected == nil {\n\t\t\tt.Error(\"Registry protected map should be initialized\")\n\t\t}\n\t})\n\n\tt.Run(\"RegisterHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\terr := registry.RegisterHandler(\"TEST\", handler, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\tretrievedHandler := registry.GetHandler(\"TEST\")\n\t\tif retrievedHandler != handler {\n\t\t\tt.Error(\"GetHandler should return the registered handler\")\n\t\t}\n\t})\n\n\tt.Run(\"RegisterNilHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\n\t\terr := registry.RegisterHandler(\"TEST\", nil, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when handler is nil\")\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"handler cannot be nil\") {\n\t\t\tt.Errorf(\"Error message should mention nil handler, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"RegisterProtectedHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\t// Register protected handler\n\t\terr := registry.RegisterHandler(\"TEST\", handler, true)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Try to overwrite any existing handler (protected or not)\n\t\tnewHandler := NewTestHandler(\"new\")\n\t\terr = registry.RegisterHandler(\"TEST\", newHandler, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when trying to overwrite existing handler\")\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"cannot overwrite existing handler\") {\n\t\t\tt.Errorf(\"Error message should mention existing handler, got: %v\", err)\n\t\t}\n\n\t\t// Original handler should still be there\n\t\tretrievedHandler := registry.GetHandler(\"TEST\")\n\t\tif retrievedHandler != handler {\n\t\t\tt.Error(\"Existing handler should not be overwritten\")\n\t\t}\n\t})\n\n\tt.Run(\"CannotOverwriteExistingHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler1 := NewTestHandler(\"test1\")\n\t\thandler2 := NewTestHandler(\"test2\")\n\n\t\t// Register non-protected handler\n\t\terr := registry.RegisterHandler(\"TEST\", handler1, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Try to overwrite with another handler (should fail)\n\t\terr = registry.RegisterHandler(\"TEST\", handler2, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when trying to overwrite existing handler\")\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"cannot overwrite existing handler\") {\n\t\t\tt.Errorf(\"Error message should mention existing handler, got: %v\", err)\n\t\t}\n\n\t\t// Original handler should still be there\n\t\tretrievedHandler := registry.GetHandler(\"TEST\")\n\t\tif retrievedHandler != handler1 {\n\t\t\tt.Error(\"Existing handler should not be overwritten\")\n\t\t}\n\t})\n\n\tt.Run(\"GetNonExistentHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\n\t\thandler := registry.GetHandler(\"NONEXISTENT\")\n\t\tif handler != nil {\n\t\t\tt.Error(\"GetHandler should return nil for non-existent handler\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\tregistry.RegisterHandler(\"TEST\", handler, false)\n\n\t\terr := registry.UnregisterHandler(\"TEST\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"UnregisterHandler should not error: %v\", err)\n\t\t}\n\n\t\tretrievedHandler := registry.GetHandler(\"TEST\")\n\t\tif retrievedHandler != nil {\n\t\t\tt.Error(\"GetHandler should return nil after unregistering\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterProtectedHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\t// Register protected handler\n\t\tregistry.RegisterHandler(\"TEST\", handler, true)\n\n\t\t// Try to unregister protected handler\n\t\terr := registry.UnregisterHandler(\"TEST\")\n\t\tif err == nil {\n\t\t\tt.Error(\"UnregisterHandler should error for protected handler\")\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"handler is protected\") {\n\t\t\tt.Errorf(\"Error message should mention handler is protected, got: %v\", err)\n\t\t}\n\n\t\t// Handler should still be there\n\t\tretrievedHandler := registry.GetHandler(\"TEST\")\n\t\tif retrievedHandler != handler {\n\t\t\tt.Error(\"Protected handler should still be registered\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterNonExistentHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\n\t\terr := registry.UnregisterHandler(\"NONEXISTENT\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"UnregisterHandler should not error for non-existent handler: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"CannotOverwriteExistingHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler1 := NewTestHandler(\"handler1\")\n\t\thandler2 := NewTestHandler(\"handler2\")\n\n\t\t// Register first handler (non-protected)\n\t\terr := registry.RegisterHandler(\"TEST_NOTIFICATION\", handler1, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"First RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Verify first handler is registered\n\t\tretrievedHandler := registry.GetHandler(\"TEST_NOTIFICATION\")\n\t\tif retrievedHandler != handler1 {\n\t\t\tt.Error(\"First handler should be registered correctly\")\n\t\t}\n\n\t\t// Attempt to overwrite with second handler (should fail)\n\t\terr = registry.RegisterHandler(\"TEST_NOTIFICATION\", handler2, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when trying to overwrite existing handler\")\n\t\t}\n\n\t\t// Verify error message mentions overwriting\n\t\tif !strings.Contains(err.Error(), \"cannot overwrite existing handler\") {\n\t\t\tt.Errorf(\"Error message should mention overwriting existing handler, got: %v\", err)\n\t\t}\n\n\t\t// Verify error message includes the notification name\n\t\tif !strings.Contains(err.Error(), \"TEST_NOTIFICATION\") {\n\t\t\tt.Errorf(\"Error message should include notification name, got: %v\", err)\n\t\t}\n\n\t\t// Verify original handler is still there (not overwritten)\n\t\tretrievedHandler = registry.GetHandler(\"TEST_NOTIFICATION\")\n\t\tif retrievedHandler != handler1 {\n\t\t\tt.Error(\"Original handler should still be registered (not overwritten)\")\n\t\t}\n\n\t\t// Verify second handler was NOT registered\n\t\tif retrievedHandler == handler2 {\n\t\t\tt.Error(\"Second handler should NOT be registered\")\n\t\t}\n\t})\n\n\tt.Run(\"CannotOverwriteProtectedHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\tprotectedHandler := NewTestHandler(\"protected\")\n\t\tnewHandler := NewTestHandler(\"new\")\n\n\t\t// Register protected handler\n\t\terr := registry.RegisterHandler(\"PROTECTED_NOTIFICATION\", protectedHandler, true)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error for protected handler: %v\", err)\n\t\t}\n\n\t\t// Attempt to overwrite protected handler (should fail)\n\t\terr = registry.RegisterHandler(\"PROTECTED_NOTIFICATION\", newHandler, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when trying to overwrite protected handler\")\n\t\t}\n\n\t\t// Verify error message\n\t\tif !strings.Contains(err.Error(), \"cannot overwrite existing handler\") {\n\t\t\tt.Errorf(\"Error message should mention overwriting existing handler, got: %v\", err)\n\t\t}\n\n\t\t// Verify protected handler is still there\n\t\tretrievedHandler := registry.GetHandler(\"PROTECTED_NOTIFICATION\")\n\t\tif retrievedHandler != protectedHandler {\n\t\t\tt.Error(\"Protected handler should still be registered\")\n\t\t}\n\t})\n\n\tt.Run(\"CanRegisterDifferentHandlers\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler1 := NewTestHandler(\"handler1\")\n\t\thandler2 := NewTestHandler(\"handler2\")\n\n\t\t// Register handlers for different notification names (should succeed)\n\t\terr := registry.RegisterHandler(\"NOTIFICATION_1\", handler1, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error for first notification: %v\", err)\n\t\t}\n\n\t\terr = registry.RegisterHandler(\"NOTIFICATION_2\", handler2, true)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error for second notification: %v\", err)\n\t\t}\n\n\t\t// Verify both handlers are registered correctly\n\t\tretrievedHandler1 := registry.GetHandler(\"NOTIFICATION_1\")\n\t\tif retrievedHandler1 != handler1 {\n\t\t\tt.Error(\"First handler should be registered correctly\")\n\t\t}\n\n\t\tretrievedHandler2 := registry.GetHandler(\"NOTIFICATION_2\")\n\t\tif retrievedHandler2 != handler2 {\n\t\t\tt.Error(\"Second handler should be registered correctly\")\n\t\t}\n\t})\n}\n\n// TestProcessor tests the processor implementation\nfunc TestProcessor(t *testing.T) {\n\tt.Run(\"NewProcessor\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\tif processor == nil {\n\t\t\tt.Fatal(\"NewProcessor should not return nil\")\n\t\t}\n\n\t\tif processor.registry == nil {\n\t\t\tt.Error(\"Processor should have a registry\")\n\t\t}\n\t})\n\n\tt.Run(\"RegisterAndGetHandler\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\terr := processor.RegisterHandler(\"TEST\", handler, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\tretrievedHandler := processor.GetHandler(\"TEST\")\n\t\tif retrievedHandler != handler {\n\t\t\tt.Error(\"GetHandler should return the registered handler\")\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterHandler\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\tprocessor.RegisterHandler(\"TEST\", handler, false)\n\n\t\terr := processor.UnregisterHandler(\"TEST\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"UnregisterHandler should not error: %v\", err)\n\t\t}\n\n\t\tretrievedHandler := processor.GetHandler(\"TEST\")\n\t\tif retrievedHandler != nil {\n\t\t\tt.Error(\"GetHandler should return nil after unregistering\")\n\t\t}\n\t})\n\n\tt.Run(\"ProcessPendingNotifications_NilReader\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error with nil reader: %v\", err)\n\t\t}\n\t})\n}\n\n// TestVoidProcessor tests the void processor implementation\nfunc TestVoidProcessor(t *testing.T) {\n\tt.Run(\"NewVoidProcessor\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\t\tif processor == nil {\n\t\t\tt.Error(\"NewVoidProcessor should not return nil\")\n\t\t}\n\t})\n\n\tt.Run(\"GetHandler\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\t\thandler := processor.GetHandler(\"TEST\")\n\t\tif handler != nil {\n\t\t\tt.Error(\"VoidProcessor GetHandler should always return nil\")\n\t\t}\n\t})\n\n\tt.Run(\"RegisterHandler\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\terr := processor.RegisterHandler(\"TEST\", handler, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"VoidProcessor RegisterHandler should return error\")\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"register failed\") {\n\t\t\tt.Errorf(\"Error message should mention registration failure, got: %v\", err)\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"push notifications are disabled\") {\n\t\t\tt.Errorf(\"Error message should mention disabled notifications, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"UnregisterHandler\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\n\t\terr := processor.UnregisterHandler(\"TEST\")\n\t\tif err == nil {\n\t\t\tt.Error(\"VoidProcessor UnregisterHandler should return error\")\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"unregister failed\") {\n\t\t\tt.Errorf(\"Error message should mention unregistration failure, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"ProcessPendingNotifications_NilReader\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"VoidProcessor ProcessPendingNotifications should never error, got: %v\", err)\n\t\t}\n\t})\n}\n\n// TestShouldSkipNotification tests the notification filtering logic\nfunc TestShouldSkipNotification(t *testing.T) {\n\ttestCases := []struct {\n\t\tname         string\n\t\tnotification string\n\t\tshouldSkip   bool\n\t}{\n\t\t// Pub/Sub notifications that should be skipped\n\t\t{\"message\", \"message\", true},\n\t\t{\"pmessage\", \"pmessage\", true},\n\t\t{\"subscribe\", \"subscribe\", true},\n\t\t{\"unsubscribe\", \"unsubscribe\", true},\n\t\t{\"psubscribe\", \"psubscribe\", true},\n\t\t{\"punsubscribe\", \"punsubscribe\", true},\n\t\t{\"smessage\", \"smessage\", true},\n\t\t{\"ssubscribe\", \"ssubscribe\", true},\n\t\t{\"sunsubscribe\", \"sunsubscribe\", true},\n\n\t\t// Push notifications that should NOT be skipped\n\t\t{\"MOVING\", \"MOVING\", false},\n\t\t{\"MIGRATING\", \"MIGRATING\", false},\n\t\t{\"MIGRATED\", \"MIGRATED\", false},\n\t\t{\"FAILING_OVER\", \"FAILING_OVER\", false},\n\t\t{\"FAILED_OVER\", \"FAILED_OVER\", false},\n\t\t{\"custom\", \"custom\", false},\n\t\t{\"unknown\", \"unknown\", false},\n\t\t{\"empty\", \"\", false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := willHandleNotificationInClient(tc.notification)\n\t\t\tif result != tc.shouldSkip {\n\t\t\t\tt.Errorf(\"willHandleNotificationInClient(%q) = %v, want %v\", tc.notification, result, tc.shouldSkip)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestNotificationHandlerInterface tests that our test handler implements the interface correctly\nfunc TestNotificationHandlerInterface(t *testing.T) {\n\tvar _ NotificationHandler = (*TestHandler)(nil)\n\n\thandler := NewTestHandler(\"test\")\n\tctx := context.Background()\n\thandlerCtx := NotificationHandlerContext{\n\t\tClient:     nil,\n\t\tConnPool:   nil,\n\t\tPubSub:     nil,\n\t\tConn:       nil,\n\t\tIsBlocking: false,\n\t}\n\tnotification := []interface{}{\"TEST\", \"data\"}\n\n\terr := handler.HandlePushNotification(ctx, handlerCtx, notification)\n\tif err != nil {\n\t\tt.Errorf(\"HandlePushNotification should not error: %v\", err)\n\t}\n\n\thandled := handler.GetHandledNotifications()\n\tif len(handled) != 1 {\n\t\tt.Errorf(\"Expected 1 handled notification, got %d\", len(handled))\n\t}\n\n\tif len(handled[0]) != 2 || handled[0][0] != \"TEST\" || handled[0][1] != \"data\" {\n\t\tt.Errorf(\"Handled notification should match input: %v\", handled[0])\n\t}\n}\n\n// TestNotificationHandlerError tests error handling in handlers\nfunc TestNotificationHandlerError(t *testing.T) {\n\thandler := NewTestHandler(\"test\")\n\texpectedError := errors.New(\"test error\")\n\thandler.SetReturnError(expectedError)\n\n\tctx := context.Background()\n\thandlerCtx := NotificationHandlerContext{\n\t\tClient:     nil,\n\t\tConnPool:   nil,\n\t\tPubSub:     nil,\n\t\tConn:       nil,\n\t\tIsBlocking: false,\n\t}\n\tnotification := []interface{}{\"TEST\", \"data\"}\n\n\terr := handler.HandlePushNotification(ctx, handlerCtx, notification)\n\tif err != expectedError {\n\t\tt.Errorf(\"HandlePushNotification should return the set error: got %v, want %v\", err, expectedError)\n\t}\n\n\t// Reset and test no error\n\thandler.Reset()\n\terr = handler.HandlePushNotification(ctx, handlerCtx, notification)\n\tif err != nil {\n\t\tt.Errorf(\"HandlePushNotification should not error after reset: %v\", err)\n\t}\n}\n\n// TestRegistryConcurrency tests concurrent access to registry\nfunc TestRegistryConcurrency(t *testing.T) {\n\tregistry := NewRegistry()\n\n\t// Test concurrent registration and access\n\tdone := make(chan bool, 10)\n\n\t// Start multiple goroutines registering handlers\n\tfor i := 0; i < 5; i++ {\n\t\tgo func(id int) {\n\t\t\thandler := NewTestHandler(\"test\")\n\t\t\terr := registry.RegisterHandler(fmt.Sprintf(\"TEST_%d\", id), handler, false)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t\t}\n\t\t\tdone <- true\n\t\t}(i)\n\t}\n\n\t// Start multiple goroutines reading handlers\n\tfor i := 0; i < 5; i++ {\n\t\tgo func(id int) {\n\t\t\tregistry.GetHandler(fmt.Sprintf(\"TEST_%d\", id))\n\t\t\tdone <- true\n\t\t}(i)\n\t}\n\n\t// Wait for all goroutines to complete\n\tfor i := 0; i < 10; i++ {\n\t\t<-done\n\t}\n}\n\n// TestProcessorConcurrency tests concurrent access to processor\nfunc TestProcessorConcurrency(t *testing.T) {\n\tprocessor := NewProcessor()\n\n\t// Test concurrent registration and access\n\tdone := make(chan bool, 10)\n\n\t// Start multiple goroutines registering handlers\n\tfor i := 0; i < 5; i++ {\n\t\tgo func(id int) {\n\t\t\thandler := NewTestHandler(\"test\")\n\t\t\terr := processor.RegisterHandler(fmt.Sprintf(\"TEST_%d\", id), handler, false)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t\t}\n\t\t\tdone <- true\n\t\t}(i)\n\t}\n\n\t// Start multiple goroutines reading handlers\n\tfor i := 0; i < 5; i++ {\n\t\tgo func(id int) {\n\t\t\tprocessor.GetHandler(fmt.Sprintf(\"TEST_%d\", id))\n\t\t\tdone <- true\n\t\t}(i)\n\t}\n\n\t// Wait for all goroutines to complete\n\tfor i := 0; i < 10; i++ {\n\t\t<-done\n\t}\n}\n\n// TestRegistryEdgeCases tests edge cases for registry\nfunc TestRegistryEdgeCases(t *testing.T) {\n\tt.Run(\"RegisterHandlerWithEmptyName\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\terr := registry.RegisterHandler(\"\", handler, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error with empty name: %v\", err)\n\t\t}\n\n\t\tretrievedHandler := registry.GetHandler(\"\")\n\t\tif retrievedHandler != handler {\n\t\t\tt.Error(\"GetHandler should return handler even with empty name\")\n\t\t}\n\t})\n\n\tt.Run(\"MultipleProtectedHandlers\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler1 := NewTestHandler(\"test1\")\n\t\thandler2 := NewTestHandler(\"test2\")\n\n\t\t// Register multiple protected handlers\n\t\terr := registry.RegisterHandler(\"TEST1\", handler1, true)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\terr = registry.RegisterHandler(\"TEST2\", handler2, true)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Try to unregister both\n\t\terr = registry.UnregisterHandler(\"TEST1\")\n\t\tif err == nil {\n\t\t\tt.Error(\"UnregisterHandler should error for protected handler\")\n\t\t}\n\n\t\terr = registry.UnregisterHandler(\"TEST2\")\n\t\tif err == nil {\n\t\t\tt.Error(\"UnregisterHandler should error for protected handler\")\n\t\t}\n\t})\n\n\tt.Run(\"CannotOverwriteAnyExistingHandler\", func(t *testing.T) {\n\t\tregistry := NewRegistry()\n\t\thandler1 := NewTestHandler(\"test1\")\n\t\thandler2 := NewTestHandler(\"test2\")\n\n\t\t// Register protected handler\n\t\terr := registry.RegisterHandler(\"TEST\", handler1, true)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"RegisterHandler should not error: %v\", err)\n\t\t}\n\n\t\t// Try to overwrite with another protected handler (should fail)\n\t\terr = registry.RegisterHandler(\"TEST\", handler2, true)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when trying to overwrite existing handler\")\n\t\t}\n\n\t\tif !strings.Contains(err.Error(), \"cannot overwrite existing handler\") {\n\t\t\tt.Errorf(\"Error message should mention existing handler, got: %v\", err)\n\t\t}\n\n\t\t// Original handler should still be there\n\t\tretrievedHandler := registry.GetHandler(\"TEST\")\n\t\tif retrievedHandler != handler1 {\n\t\t\tt.Error(\"Existing handler should not be overwritten\")\n\t\t}\n\t})\n}\n\n// TestProcessorEdgeCases tests edge cases for processor\nfunc TestProcessorEdgeCases(t *testing.T) {\n\tt.Run(\"ProcessorWithNilRegistry\", func(t *testing.T) {\n\t\t// This tests internal consistency - processor should always have a registry\n\t\tprocessor := &Processor{registry: nil}\n\n\t\t// This should panic or handle gracefully\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\t// Expected behavior - accessing nil registry should panic\n\t\t\t\tt.Logf(\"Expected panic when accessing nil registry: %v\", r)\n\t\t\t}\n\t\t}()\n\n\t\t// This will likely panic, which is expected behavior\n\t\tprocessor.GetHandler(\"TEST\")\n\t})\n\n\tt.Run(\"ProcessorRegisterNilHandler\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\n\t\terr := processor.RegisterHandler(\"TEST\", nil, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"RegisterHandler should error when handler is nil\")\n\t\t}\n\t})\n}\n\n// TestVoidProcessorEdgeCases tests edge cases for void processor\nfunc TestVoidProcessorEdgeCases(t *testing.T) {\n\tt.Run(\"VoidProcessorMultipleOperations\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\n\t\t// Multiple register attempts should all fail\n\t\tfor i := 0; i < 5; i++ {\n\t\t\terr := processor.RegisterHandler(fmt.Sprintf(\"TEST_%d\", i), handler, false)\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"VoidProcessor RegisterHandler should always return error\")\n\t\t\t}\n\t\t}\n\n\t\t// Multiple unregister attempts should all fail\n\t\tfor i := 0; i < 5; i++ {\n\t\t\terr := processor.UnregisterHandler(fmt.Sprintf(\"TEST_%d\", i))\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"VoidProcessor UnregisterHandler should always return error\")\n\t\t\t}\n\t\t}\n\n\t\t// Multiple get attempts should all return nil\n\t\tfor i := 0; i < 5; i++ {\n\t\t\thandler := processor.GetHandler(fmt.Sprintf(\"TEST_%d\", i))\n\t\t\tif handler != nil {\n\t\t\t\tt.Errorf(\"VoidProcessor GetHandler should always return nil\")\n\t\t\t}\n\t\t}\n\t})\n}\n\n// Helper functions to create fake RESP3 protocol data for testing\n\n// createFakeRESP3PushNotification creates a fake RESP3 push notification buffer\nfunc createFakeRESP3PushNotification(notificationType string, args ...string) *bytes.Buffer {\n\tbuf := &bytes.Buffer{}\n\n\t// RESP3 Push notification format: ><len>\\r\\n<elements>\\r\\n\n\ttotalElements := 1 + len(args) // notification type + arguments\n\tfmt.Fprintf(buf, \">%d\\r\\n\", totalElements)\n\n\t// Write notification type as bulk string\n\tfmt.Fprintf(buf, \"$%d\\r\\n%s\\r\\n\", len(notificationType), notificationType)\n\n\t// Write arguments as bulk strings\n\tfor _, arg := range args {\n\t\tfmt.Fprintf(buf, \"$%d\\r\\n%s\\r\\n\", len(arg), arg)\n\t}\n\n\treturn buf\n}\n\n// createReaderWithPrimedBuffer creates a reader (no longer needs priming)\nfunc createReaderWithPrimedBuffer(buf *bytes.Buffer) *proto.Reader {\n\treader := proto.NewReader(buf)\n\t// No longer need to prime the buffer - PeekPushNotificationName handles it automatically\n\treturn reader\n}\n\n// createMockConnection creates a mock connection for testing\nfunc createMockConnection() *pool.Conn {\n\tmockNetConn := &MockNetConn{}\n\treturn pool.NewConn(mockNetConn)\n}\n\n// createFakeRESP3Array creates a fake RESP3 array (not push notification)\nfunc createFakeRESP3Array(elements ...string) *bytes.Buffer {\n\tbuf := &bytes.Buffer{}\n\n\t// RESP3 Array format: *<len>\\r\\n<elements>\\r\\n\n\tfmt.Fprintf(buf, \"*%d\\r\\n\", len(elements))\n\n\t// Write elements as bulk strings\n\tfor _, element := range elements {\n\t\tfmt.Fprintf(buf, \"$%d\\r\\n%s\\r\\n\", len(element), element)\n\t}\n\n\treturn buf\n}\n\n// createFakeRESP3Error creates a fake RESP3 error\nfunc createFakeRESP3Error(message string) *bytes.Buffer {\n\tbuf := &bytes.Buffer{}\n\tfmt.Fprintf(buf, \"-%s\\r\\n\", message)\n\treturn buf\n}\n\n// createMultipleNotifications creates a buffer with multiple notifications\nfunc createMultipleNotifications(notifications ...[]string) *bytes.Buffer {\n\tbuf := &bytes.Buffer{}\n\n\tfor _, notification := range notifications {\n\t\tif len(notification) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tnotificationType := notification[0]\n\t\targs := notification[1:]\n\n\t\t// Determine if this should be a push notification or regular array\n\t\tif willHandleNotificationInClient(notificationType) {\n\t\t\t// Create as push notification (will be skipped)\n\t\t\tpushBuf := createFakeRESP3PushNotification(notificationType, args...)\n\t\t\tbuf.Write(pushBuf.Bytes())\n\t\t} else {\n\t\t\t// Create as push notification (will be processed)\n\t\t\tpushBuf := createFakeRESP3PushNotification(notificationType, args...)\n\t\t\tbuf.Write(pushBuf.Bytes())\n\t\t}\n\t}\n\n\treturn buf\n}\n\n// TestProcessorWithFakeBuffer tests ProcessPendingNotifications with fake RESP3 data\nfunc TestProcessorWithFakeBuffer(t *testing.T) {\n\tt.Run(\"ProcessValidPushNotification\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create fake RESP3 push notification\n\t\tbuf := createFakeRESP3PushNotification(\"MOVING\", \"slot\", \"123\", \"from\", \"node1\", \"to\", \"node2\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error: %v\", err)\n\t\t}\n\n\t\thandled := handler.GetHandledNotifications()\n\t\tif len(handled) != 1 {\n\t\t\tt.Errorf(\"Expected 1 handled notification, got %d\", len(handled))\n\t\t\treturn // Prevent panic if no notifications were handled\n\t\t}\n\n\t\tif len(handled[0]) != 7 || handled[0][0] != \"MOVING\" {\n\t\t\tt.Errorf(\"Handled notification should match input: %v\", handled[0])\n\t\t}\n\n\t\tif len(handled[0]) > 2 && (handled[0][1] != \"slot\" || handled[0][2] != \"123\") {\n\t\t\tt.Errorf(\"Notification arguments should match: %v\", handled[0])\n\t\t}\n\t})\n\n\tt.Run(\"ProcessSkippedPushNotification\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"message\", handler, false)\n\n\t\t// Create fake RESP3 push notification for pub/sub message (should be skipped)\n\t\tbuf := createFakeRESP3PushNotification(\"message\", \"channel\", \"hello world\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error: %v\", err)\n\t\t}\n\n\t\thandled := handler.GetHandledNotifications()\n\t\tif len(handled) != 0 {\n\t\t\tt.Errorf(\"Expected 0 handled notifications (should be skipped), got %d\", len(handled))\n\t\t}\n\t})\n\n\tt.Run(\"ProcessNotificationWithoutHandler\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\t// No handler registered for MOVING\n\n\t\t// Create fake RESP3 push notification\n\t\tbuf := createFakeRESP3PushNotification(\"MOVING\", \"slot\", \"123\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error when no handler: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"ProcessNotificationWithHandlerError\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\thandler.SetReturnError(errors.New(\"handler error\"))\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create fake RESP3 push notification\n\t\tbuf := createFakeRESP3PushNotification(\"MOVING\", \"slot\", \"123\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error even when handler errors: %v\", err)\n\t\t}\n\n\t\thandled := handler.GetHandledNotifications()\n\t\tif len(handled) != 1 {\n\t\t\tt.Errorf(\"Expected 1 handled notification even with error, got %d\", len(handled))\n\t\t}\n\t})\n\n\tt.Run(\"ProcessNonPushNotification\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create fake RESP3 array (not push notification)\n\t\tbuf := createFakeRESP3Array(\"MOVING\", \"slot\", \"123\")\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error: %v\", err)\n\t\t}\n\n\t\thandled := handler.GetHandledNotifications()\n\t\tif len(handled) != 0 {\n\t\t\tt.Errorf(\"Expected 0 handled notifications (not push type), got %d\", len(handled))\n\t\t}\n\t})\n\n\tt.Run(\"ProcessMultipleNotifications\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\tmovingHandler := NewTestHandler(\"moving\")\n\t\tmigratingHandler := NewTestHandler(\"migrating\")\n\t\tprocessor.RegisterHandler(\"MOVING\", movingHandler, false)\n\t\tprocessor.RegisterHandler(\"MIGRATING\", migratingHandler, false)\n\n\t\t// Create buffer with multiple notifications\n\t\tbuf := createMultipleNotifications(\n\t\t\t[]string{\"MOVING\", \"slot\", \"123\", \"from\", \"node1\", \"to\", \"node2\"},\n\t\t\t[]string{\"MIGRATING\", \"slot\", \"456\", \"from\", \"node2\", \"to\", \"node3\"},\n\t\t)\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should not error: %v\", err)\n\t\t}\n\n\t\t// Check MOVING handler\n\t\tmovingHandled := movingHandler.GetHandledNotifications()\n\t\tif len(movingHandled) != 1 {\n\t\t\tt.Errorf(\"Expected 1 MOVING notification, got %d\", len(movingHandled))\n\t\t}\n\t\tif len(movingHandled) > 0 && movingHandled[0][0] != \"MOVING\" {\n\t\t\tt.Errorf(\"Expected MOVING notification, got %v\", movingHandled[0][0])\n\t\t}\n\n\t\t// Check MIGRATING handler\n\t\tmigratingHandled := migratingHandler.GetHandledNotifications()\n\t\tif len(migratingHandled) != 1 {\n\t\t\tt.Errorf(\"Expected 1 MIGRATING notification, got %d\", len(migratingHandled))\n\t\t}\n\t\tif len(migratingHandled) > 0 && migratingHandled[0][0] != \"MIGRATING\" {\n\t\t\tt.Errorf(\"Expected MIGRATING notification, got %v\", migratingHandled[0][0])\n\t\t}\n\t})\n\n\tt.Run(\"ProcessEmptyNotification\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create fake RESP3 push notification with no elements\n\t\tbuf := &bytes.Buffer{}\n\t\tfmt.Fprint(buf, \">0\\r\\n\") // Empty push notification\n\t\treader := createReaderWithPrimedBuffer(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\t// This should panic due to empty notification array\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tt.Logf(\"ProcessPendingNotifications panicked as expected for empty notification: %v\", r)\n\t\t\t}\n\t\t}()\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Logf(\"ProcessPendingNotifications errored for empty notification: %v\", err)\n\t\t}\n\n\t\thandled := handler.GetHandledNotifications()\n\t\tif len(handled) != 0 {\n\t\t\tt.Errorf(\"Expected 0 handled notifications for empty notification, got %d\", len(handled))\n\t\t}\n\t})\n\n\tt.Run(\"ProcessNotificationWithNonStringType\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create fake RESP3 push notification with integer as first element\n\t\tbuf := &bytes.Buffer{}\n\t\tfmt.Fprint(buf, \">2\\r\\n\")         // 2 elements\n\t\tfmt.Fprint(buf, \":123\\r\\n\")       // Integer instead of string\n\t\tfmt.Fprint(buf, \"$4\\r\\ndata\\r\\n\") // String data\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       createMockConnection(),\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should handle non-string type gracefully: %v\", err)\n\t\t}\n\n\t\thandled := handler.GetHandledNotifications()\n\t\tif len(handled) != 0 {\n\t\t\tt.Errorf(\"Expected 0 handled notifications for non-string type, got %d\", len(handled))\n\t\t}\n\t})\n}\n\n// TestVoidProcessorWithFakeBuffer tests VoidProcessor with fake RESP3 data\nfunc TestVoidProcessorWithFakeBuffer(t *testing.T) {\n\tt.Run(\"ProcessPushNotifications\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\n\t\t// Create buffer with multiple push notifications\n\t\tbuf := createMultipleNotifications(\n\t\t\t[]string{\"MOVING\", \"slot\", \"123\"},\n\t\t\t[]string{\"MIGRATING\", \"slot\", \"456\"},\n\t\t\t[]string{\"FAILED_OVER\", \"node\", \"node1\"},\n\t\t)\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"VoidProcessor ProcessPendingNotifications should not error: %v\", err)\n\t\t}\n\n\t\t// VoidProcessor should discard all notifications without processing\n\t\t// We can't directly verify this, but the fact that it doesn't error is good\n\t})\n\n\tt.Run(\"ProcessSkippedNotifications\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\n\t\t// Create buffer with pub/sub notifications (should be skipped)\n\t\tbuf := createMultipleNotifications(\n\t\t\t[]string{\"message\", \"channel\", \"data\"},\n\t\t\t[]string{\"pmessage\", \"pattern\", \"channel\", \"data\"},\n\t\t\t[]string{\"subscribe\", \"channel\", \"1\"},\n\t\t)\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"VoidProcessor ProcessPendingNotifications should not error: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"ProcessMixedNotifications\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\n\t\t// Create buffer with mixed push notifications and regular arrays\n\t\tbuf := &bytes.Buffer{}\n\n\t\t// Add push notification\n\t\tpushBuf := createFakeRESP3PushNotification(\"MOVING\", \"slot\", \"123\")\n\t\tbuf.Write(pushBuf.Bytes())\n\n\t\t// Add regular array (should stop processing)\n\t\tarrayBuf := createFakeRESP3Array(\"SOME\", \"COMMAND\")\n\t\tbuf.Write(arrayBuf.Bytes())\n\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"VoidProcessor ProcessPendingNotifications should not error: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"ProcessInvalidNotificationFormat\", func(t *testing.T) {\n\t\tprocessor := NewVoidProcessor()\n\n\t\t// Create invalid RESP3 data\n\t\tbuf := &bytes.Buffer{}\n\t\tfmt.Fprint(buf, \">1\\r\\n\")      // Push notification with 1 element\n\t\tfmt.Fprint(buf, \"invalid\\r\\n\") // Invalid format (should be $<len>\\r\\n<data>\\r\\n)\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\t// VoidProcessor should handle errors gracefully\n\t\tif err != nil {\n\t\t\tt.Logf(\"VoidProcessor handled error gracefully: %v\", err)\n\t\t}\n\t})\n}\n\n// TestProcessorErrorHandling tests error handling scenarios\nfunc TestProcessorErrorHandling(t *testing.T) {\n\tt.Run(\"ProcessWithEmptyBuffer\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create empty buffer\n\t\tbuf := &bytes.Buffer{}\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"ProcessPendingNotifications should handle empty buffer gracefully: %v\", err)\n\t\t}\n\n\t\thandled := handler.GetHandledNotifications()\n\t\tif len(handled) != 0 {\n\t\t\tt.Errorf(\"Expected 0 handled notifications for empty buffer, got %d\", len(handled))\n\t\t}\n\t})\n\n\tt.Run(\"ProcessWithCorruptedData\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create buffer with corrupted RESP3 data\n\t\tbuf := &bytes.Buffer{}\n\t\tfmt.Fprint(buf, \">2\\r\\n\")           // Says 2 elements\n\t\tfmt.Fprint(buf, \"$6\\r\\nMOVING\\r\\n\") // First element OK\n\t\tfmt.Fprint(buf, \"corrupted\")        // Second element corrupted (no proper format)\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\t// Should handle corruption gracefully\n\t\tif err != nil {\n\t\t\tt.Logf(\"Processor handled corrupted data gracefully: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"ProcessWithPartialData\", func(t *testing.T) {\n\t\tprocessor := NewProcessor()\n\t\thandler := NewTestHandler(\"test\")\n\t\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\t\t// Create buffer with partial RESP3 data\n\t\tbuf := &bytes.Buffer{}\n\t\tfmt.Fprint(buf, \">2\\r\\n\")           // Says 2 elements\n\t\tfmt.Fprint(buf, \"$6\\r\\nMOVING\\r\\n\") // First element OK\n\t\t// Missing second element\n\t\treader := proto.NewReader(buf)\n\n\t\tctx := context.Background()\n\t\thandlerCtx := NotificationHandlerContext{\n\t\t\tClient:     nil,\n\t\t\tConnPool:   nil,\n\t\t\tPubSub:     nil,\n\t\t\tConn:       nil,\n\t\t\tIsBlocking: false,\n\t\t}\n\n\t\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\t\t// Should handle partial data gracefully\n\t\tif err != nil {\n\t\t\tt.Logf(\"Processor handled partial data gracefully: %v\", err)\n\t\t}\n\t})\n}\n\n// TestProcessorPerformanceWithFakeData tests performance with realistic data\nfunc TestProcessorPerformanceWithFakeData(t *testing.T) {\n\tprocessor := NewProcessor()\n\thandler := NewTestHandler(\"test\")\n\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\tprocessor.RegisterHandler(\"MIGRATING\", handler, false)\n\tprocessor.RegisterHandler(\"MIGRATED\", handler, false)\n\n\t// Create buffer with many notifications\n\tnotifications := make([][]string, 100)\n\tfor i := 0; i < 100; i++ {\n\t\tswitch i % 3 {\n\t\tcase 0:\n\t\t\tnotifications[i] = []string{\"MOVING\", \"slot\", fmt.Sprintf(\"%d\", i), \"from\", \"node1\", \"to\", \"node2\"}\n\t\tcase 1:\n\t\t\tnotifications[i] = []string{\"MIGRATING\", \"slot\", fmt.Sprintf(\"%d\", i), \"from\", \"node2\", \"to\", \"node3\"}\n\t\tcase 2:\n\t\t\tnotifications[i] = []string{\"MIGRATED\", \"slot\", fmt.Sprintf(\"%d\", i), \"from\", \"node3\", \"to\", \"node1\"}\n\t\t}\n\t}\n\n\tbuf := createMultipleNotifications(notifications...)\n\treader := proto.NewReader(buf)\n\n\tctx := context.Background()\n\thandlerCtx := NotificationHandlerContext{\n\t\tClient:     nil,\n\t\tConnPool:   nil,\n\t\tPubSub:     nil,\n\t\tConn:       createMockConnection(),\n\t\tIsBlocking: false,\n\t}\n\n\terr := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)\n\tif err != nil {\n\t\tt.Errorf(\"ProcessPendingNotifications should not error with many notifications: %v\", err)\n\t}\n\n\thandled := handler.GetHandledNotifications()\n\tif len(handled) != 100 {\n\t\tt.Errorf(\"Expected 100 handled notifications, got %d\", len(handled))\n\t}\n}\n\n// TestInterfaceCompliance tests that all types implement their interfaces correctly\nfunc TestInterfaceCompliance(t *testing.T) {\n\t// Test that Processor implements NotificationProcessor\n\tvar _ NotificationProcessor = (*Processor)(nil)\n\n\t// Test that VoidProcessor implements NotificationProcessor\n\tvar _ NotificationProcessor = (*VoidProcessor)(nil)\n\n\t// Test that NotificationHandlerContext is a concrete struct (no interface needed)\n\tvar _ NotificationHandlerContext = NotificationHandlerContext{}\n\n\t// Test that TestHandler implements NotificationHandler\n\tvar _ NotificationHandler = (*TestHandler)(nil)\n\n\t// Test that error types implement error interface\n\tvar _ error = (*HandlerError)(nil)\n\tvar _ error = (*ProcessorError)(nil)\n}\n\n// TestErrors tests the error definitions and helper functions\nfunc TestErrors(t *testing.T) {\n\tt.Run(\"ErrHandlerNil\", func(t *testing.T) {\n\t\terr := ErrHandlerNil\n\t\tif err == nil {\n\t\t\tt.Error(\"ErrHandlerNil should not be nil\")\n\t\t}\n\n\t\tif err.Error() != \"handler cannot be nil\" {\n\t\t\tt.Errorf(\"ErrHandlerNil message should be 'handler cannot be nil', got: %s\", err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"ErrHandlerExists\", func(t *testing.T) {\n\t\tnotificationName := \"TEST_NOTIFICATION\"\n\t\terr := ErrHandlerExists(notificationName)\n\n\t\tif err == nil {\n\t\t\tt.Error(\"ErrHandlerExists should not return nil\")\n\t\t}\n\n\t\texpectedMsg := \"handler register failed for 'TEST_NOTIFICATION': cannot overwrite existing handler\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"ErrHandlerExists message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"ErrProtectedHandler\", func(t *testing.T) {\n\t\tnotificationName := \"PROTECTED_NOTIFICATION\"\n\t\terr := ErrProtectedHandler(notificationName)\n\n\t\tif err == nil {\n\t\t\tt.Error(\"ErrProtectedHandler should not return nil\")\n\t\t}\n\n\t\texpectedMsg := \"handler unregister failed for 'PROTECTED_NOTIFICATION': handler is protected\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"ErrProtectedHandler message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"ErrVoidProcessorRegister\", func(t *testing.T) {\n\t\tnotificationName := \"VOID_TEST\"\n\t\terr := ErrVoidProcessorRegister(notificationName)\n\n\t\tif err == nil {\n\t\t\tt.Error(\"ErrVoidProcessorRegister should not return nil\")\n\t\t}\n\n\t\texpectedMsg := \"void_processor register failed for 'VOID_TEST': push notifications are disabled\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"ErrVoidProcessorRegister message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"ErrVoidProcessorUnregister\", func(t *testing.T) {\n\t\tnotificationName := \"VOID_TEST\"\n\t\terr := ErrVoidProcessorUnregister(notificationName)\n\n\t\tif err == nil {\n\t\t\tt.Error(\"ErrVoidProcessorUnregister should not return nil\")\n\t\t}\n\n\t\texpectedMsg := \"void_processor unregister failed for 'VOID_TEST': push notifications are disabled\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"ErrVoidProcessorUnregister message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\t})\n}\n\n// TestHandlerError tests the HandlerError structured error type\nfunc TestHandlerError(t *testing.T) {\n\tt.Run(\"HandlerErrorWithoutWrappedError\", func(t *testing.T) {\n\t\terr := NewHandlerError(\"register\", \"TEST_NOTIFICATION\", \"handler already exists\", nil)\n\n\t\tif err == nil {\n\t\t\tt.Error(\"NewHandlerError should not return nil\")\n\t\t}\n\n\t\texpectedMsg := \"handler register failed for 'TEST_NOTIFICATION': handler already exists\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"HandlerError message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\n\t\tif err.Operation != \"register\" {\n\t\t\tt.Errorf(\"HandlerError Operation should be 'register', got: %s\", err.Operation)\n\t\t}\n\n\t\tif err.PushNotificationName != \"TEST_NOTIFICATION\" {\n\t\t\tt.Errorf(\"HandlerError PushNotificationName should be 'TEST_NOTIFICATION', got: %s\", err.PushNotificationName)\n\t\t}\n\n\t\tif err.Reason != \"handler already exists\" {\n\t\t\tt.Errorf(\"HandlerError Reason should be 'handler already exists', got: %s\", err.Reason)\n\t\t}\n\n\t\tif err.Unwrap() != nil {\n\t\t\tt.Error(\"HandlerError Unwrap should return nil when no wrapped error\")\n\t\t}\n\t})\n\n\tt.Run(\"HandlerErrorWithWrappedError\", func(t *testing.T) {\n\t\twrappedErr := errors.New(\"underlying error\")\n\t\terr := NewHandlerError(\"unregister\", \"PROTECTED_NOTIFICATION\", \"protected handler\", wrappedErr)\n\n\t\texpectedMsg := \"handler unregister failed for 'PROTECTED_NOTIFICATION': protected handler (underlying error)\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"HandlerError message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\n\t\tif err.Unwrap() != wrappedErr {\n\t\t\tt.Error(\"HandlerError Unwrap should return the wrapped error\")\n\t\t}\n\t})\n}\n\n// TestProcessorError tests the ProcessorError structured error type\nfunc TestProcessorError(t *testing.T) {\n\tt.Run(\"ProcessorErrorWithoutWrappedError\", func(t *testing.T) {\n\t\terr := NewProcessorError(\"processor\", \"process\", \"\", \"invalid notification format\", nil)\n\n\t\tif err == nil {\n\t\t\tt.Error(\"NewProcessorError should not return nil\")\n\t\t}\n\n\t\texpectedMsg := \"processor process failed: invalid notification format\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"ProcessorError message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\n\t\tif err.ProcessorType != \"processor\" {\n\t\t\tt.Errorf(\"ProcessorError ProcessorType should be 'processor', got: %s\", err.ProcessorType)\n\t\t}\n\n\t\tif err.Operation != \"process\" {\n\t\t\tt.Errorf(\"ProcessorError Operation should be 'process', got: %s\", err.Operation)\n\t\t}\n\n\t\tif err.Reason != \"invalid notification format\" {\n\t\t\tt.Errorf(\"ProcessorError Reason should be 'invalid notification format', got: %s\", err.Reason)\n\t\t}\n\n\t\tif err.Unwrap() != nil {\n\t\t\tt.Error(\"ProcessorError Unwrap should return nil when no wrapped error\")\n\t\t}\n\t})\n\n\tt.Run(\"ProcessorErrorWithWrappedError\", func(t *testing.T) {\n\t\twrappedErr := errors.New(\"network error\")\n\t\terr := NewProcessorError(\"void_processor\", \"register\", \"\", \"disabled\", wrappedErr)\n\n\t\texpectedMsg := \"void_processor register failed: disabled (network error)\"\n\t\tif err.Error() != expectedMsg {\n\t\t\tt.Errorf(\"ProcessorError message should be '%s', got: %s\", expectedMsg, err.Error())\n\t\t}\n\n\t\tif err.Unwrap() != wrappedErr {\n\t\t\tt.Error(\"ProcessorError Unwrap should return the wrapped error\")\n\t\t}\n\t})\n}\n\n// TestErrorHelperFunctions tests the error checking helper functions\nfunc TestErrorHelperFunctions(t *testing.T) {\n\tt.Run(\"IsHandlerNilError\", func(t *testing.T) {\n\t\t// Test with ErrHandlerNil\n\t\tif !IsHandlerNilError(ErrHandlerNil) {\n\t\t\tt.Error(\"IsHandlerNilError should return true for ErrHandlerNil\")\n\t\t}\n\n\t\t// Test with other error\n\t\totherErr := ErrHandlerExists(\"TEST\")\n\t\tif IsHandlerNilError(otherErr) {\n\t\t\tt.Error(\"IsHandlerNilError should return false for other errors\")\n\t\t}\n\n\t\t// Test with nil\n\t\tif IsHandlerNilError(nil) {\n\t\t\tt.Error(\"IsHandlerNilError should return false for nil\")\n\t\t}\n\t})\n\n\tt.Run(\"IsVoidProcessorError\", func(t *testing.T) {\n\t\t// Test with void processor register error\n\t\tregisterErr := ErrVoidProcessorRegister(\"TEST\")\n\t\tif !IsVoidProcessorError(registerErr) {\n\t\t\tt.Error(\"IsVoidProcessorError should return true for void processor register error\")\n\t\t}\n\n\t\t// Test with void processor unregister error\n\t\tunregisterErr := ErrVoidProcessorUnregister(\"TEST\")\n\t\tif !IsVoidProcessorError(unregisterErr) {\n\t\t\tt.Error(\"IsVoidProcessorError should return true for void processor unregister error\")\n\t\t}\n\n\t\t// Test with other error\n\t\totherErr := ErrHandlerNil\n\t\tif IsVoidProcessorError(otherErr) {\n\t\t\tt.Error(\"IsVoidProcessorError should return false for other errors\")\n\t\t}\n\n\t\t// Test with nil\n\t\tif IsVoidProcessorError(nil) {\n\t\t\tt.Error(\"IsVoidProcessorError should return false for nil\")\n\t\t}\n\t})\n}\n\n// TestErrorConstants tests the error reason constants\nfunc TestErrorConstants(t *testing.T) {\n\tt.Run(\"ErrorReasonConstants\", func(t *testing.T) {\n\t\tif ReasonHandlerNil != \"handler cannot be nil\" {\n\t\t\tt.Errorf(\"ReasonHandlerNil should be 'handler cannot be nil', got: %s\", ReasonHandlerNil)\n\t\t}\n\n\t\tif ReasonHandlerExists != \"cannot overwrite existing handler\" {\n\t\t\tt.Errorf(\"ReasonHandlerExists should be 'cannot overwrite existing handler', got: %s\", ReasonHandlerExists)\n\t\t}\n\n\t\tif ReasonHandlerProtected != \"handler is protected\" {\n\t\t\tt.Errorf(\"ReasonHandlerProtected should be 'handler is protected', got: %s\", ReasonHandlerProtected)\n\t\t}\n\n\t\tif ReasonPushNotificationsDisabled != \"push notifications are disabled\" {\n\t\t\tt.Errorf(\"ReasonPushNotificationsDisabled should be 'push notifications are disabled', got: %s\", ReasonPushNotificationsDisabled)\n\t\t}\n\t})\n}\n\n// Benchmark tests for performance\nfunc BenchmarkRegistry(b *testing.B) {\n\tregistry := NewRegistry()\n\thandler := NewTestHandler(\"test\")\n\n\tb.Run(\"RegisterHandler\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tregistry.RegisterHandler(\"TEST\", handler, false)\n\t\t}\n\t})\n\n\tb.Run(\"GetHandler\", func(b *testing.B) {\n\t\tregistry.RegisterHandler(\"TEST\", handler, false)\n\t\tb.ResetTimer()\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tregistry.GetHandler(\"TEST\")\n\t\t}\n\t})\n}\n\nfunc BenchmarkProcessor(b *testing.B) {\n\tprocessor := NewProcessor()\n\thandler := NewTestHandler(\"test\")\n\tprocessor.RegisterHandler(\"MOVING\", handler, false)\n\n\tb.Run(\"RegisterHandler\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tprocessor.RegisterHandler(\"TEST\", handler, false)\n\t\t}\n\t})\n\n\tb.Run(\"GetHandler\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tprocessor.GetHandler(\"MOVING\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "push/registry.go",
    "content": "package push\n\nimport (\n\t\"sync\"\n)\n\n// Registry manages push notification handlers\ntype Registry struct {\n\tmu        sync.RWMutex\n\thandlers  map[string]NotificationHandler\n\tprotected map[string]bool\n}\n\n// NewRegistry creates a new push notification registry\nfunc NewRegistry() *Registry {\n\treturn &Registry{\n\t\thandlers:  make(map[string]NotificationHandler),\n\t\tprotected: make(map[string]bool),\n\t}\n}\n\n// RegisterHandler registers a handler for a specific push notification name\nfunc (r *Registry) RegisterHandler(pushNotificationName string, handler NotificationHandler, protected bool) error {\n\tif handler == nil {\n\t\treturn ErrHandlerNil\n\t}\n\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\t// Check if handler already exists\n\tif _, exists := r.protected[pushNotificationName]; exists {\n\t\treturn ErrHandlerExists(pushNotificationName)\n\t}\n\n\tr.handlers[pushNotificationName] = handler\n\tr.protected[pushNotificationName] = protected\n\treturn nil\n}\n\n// GetHandler returns the handler for a specific push notification name\nfunc (r *Registry) GetHandler(pushNotificationName string) NotificationHandler {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\treturn r.handlers[pushNotificationName]\n}\n\n// UnregisterHandler removes a handler for a specific push notification name\nfunc (r *Registry) UnregisterHandler(pushNotificationName string) error {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\t// Check if handler is protected\n\tif protected, exists := r.protected[pushNotificationName]; exists && protected {\n\t\treturn ErrProtectedHandler(pushNotificationName)\n\t}\n\n\tdelete(r.handlers, pushNotificationName)\n\tdelete(r.protected, pushNotificationName)\n\treturn nil\n}\n"
  },
  {
    "path": "push_notifications.go",
    "content": "package redis\n\nimport (\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// NewPushNotificationProcessor creates a new push notification processor\n// This processor maintains a registry of handlers and processes push notifications\n// It is used for RESP3 connections where push notifications are available\nfunc NewPushNotificationProcessor() push.NotificationProcessor {\n\treturn push.NewProcessor()\n}\n\n// NewVoidPushNotificationProcessor creates a new void push notification processor\n// This processor does not maintain any handlers and always returns nil for all operations\n// It is used for RESP2 connections where push notifications are not available\n// It can also be used to disable push notifications for RESP3 connections, where\n// it will discard all push notifications without processing them\nfunc NewVoidPushNotificationProcessor() push.NotificationProcessor {\n\treturn push.NewVoidProcessor()\n}\n"
  },
  {
    "path": "race_test.go",
    "content": "package redis_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"races\", func() {\n\tvar client *redis.Client\n\tvar C, N int\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).To(BeNil())\n\n\t\tC, N = 10, 1000\n\t\tif testing.Short() {\n\t\t\tC = 4\n\t\t\tN = 100\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\terr := client.Close()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should echo\", func() {\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tmsg := fmt.Sprintf(\"echo %d %d\", id, i)\n\t\t\t\techo, err := client.Echo(ctx, msg).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(echo).To(Equal(msg))\n\t\t\t}\n\t\t})\n\t})\n\n\tIt(\"should incr\", func() {\n\t\tkey := \"TestIncrFromGoroutines\"\n\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := client.Incr(ctx, key).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t})\n\n\t\tval, err := client.Get(ctx, key).Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(int64(C * N)))\n\t})\n\n\tIt(\"should handle many keys\", func() {\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := client.Set(\n\t\t\t\t\tctx,\n\t\t\t\t\tfmt.Sprintf(\"keys.key-%d-%d\", id, i),\n\t\t\t\t\tfmt.Sprintf(\"hello-%d-%d\", id, i),\n\t\t\t\t\t0,\n\t\t\t\t).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t})\n\n\t\tkeys := client.Keys(ctx, \"keys.*\")\n\t\tExpect(keys.Err()).NotTo(HaveOccurred())\n\t\tExpect(len(keys.Val())).To(Equal(C * N))\n\t})\n\n\tIt(\"should handle many keys 2\", func() {\n\t\tperform(C, func(id int) {\n\t\t\tkeys := []string{\"non-existent-key\"}\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tkey := fmt.Sprintf(\"keys.key-%d\", i)\n\t\t\t\tkeys = append(keys, key)\n\n\t\t\t\terr := client.Set(ctx, key, fmt.Sprintf(\"hello-%d\", i), 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t\tkeys = append(keys, \"non-existent-key\")\n\n\t\t\tvals, err := client.MGet(ctx, keys...).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(vals)).To(Equal(N + 2))\n\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tExpect(vals[i+1]).To(Equal(fmt.Sprintf(\"hello-%d\", i)))\n\t\t\t}\n\n\t\t\tExpect(vals[0]).To(BeNil())\n\t\t\tExpect(vals[N+1]).To(BeNil())\n\t\t})\n\t})\n\n\tIt(\"should handle big vals in Get\", func() {\n\t\tC, N := 4, 100\n\n\t\tbigVal := bigVal()\n\n\t\terr := client.Set(ctx, \"key\", bigVal, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Reconnect to get new connection.\n\t\tExpect(client.Close()).To(BeNil())\n\t\tclient = redis.NewClient(redisOptions())\n\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tgot, err := client.Get(ctx, \"key\").Bytes()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(got).To(Equal(bigVal))\n\t\t\t}\n\t\t})\n\t})\n\n\tIt(\"should handle big vals in Set\", func() {\n\t\tC, N := 4, 100\n\n\t\tbigVal := bigVal()\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := client.Set(ctx, \"key\", bigVal, 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t})\n\t})\n\n\tIt(\"should select db\", Label(\"NonRedisEnterprise\"), func() {\n\t\terr := client.Set(ctx, \"db\", 0, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tperform(C, func(id int) {\n\t\t\topt := redisOptions()\n\t\t\topt.DB = id\n\t\t\tclient := redis.NewClient(opt)\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := client.Set(ctx, \"db\", id, 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tn, err := client.Get(ctx, \"db\").Int64()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(n).To(Equal(int64(id)))\n\t\t\t}\n\t\t\terr := client.Close()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tn, err := client.Get(ctx, \"db\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(0)))\n\t})\n\n\tIt(\"should select DB with read timeout\", func() {\n\t\tperform(C, func(id int) {\n\t\t\topt := redisOptions()\n\t\t\topt.DB = id\n\t\t\topt.ReadTimeout = time.Nanosecond\n\t\t\tclient := redis.NewClient(opt)\n\n\t\t\tperform(C, func(id int) {\n\t\t\t\terr := client.Ping(ctx).Err()\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t\t})\n\n\t\t\terr := client.Close()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\t})\n\n\tIt(\"should Watch/Unwatch\", func() {\n\t\terr := client.Set(ctx, \"key\", \"0\", 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t\tval, err := tx.Get(ctx, \"key\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(val).NotTo(Equal(redis.Nil))\n\n\t\t\t\t\tnum, err := strconv.ParseInt(val, 10, 64)\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tcmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\t\tpipe.Set(ctx, \"key\", strconv.FormatInt(num+1, 10), 0)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\t\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\t\treturn err\n\t\t\t\t}, \"key\")\n\t\t\t\tif err == redis.TxFailedErr {\n\t\t\t\t\ti--\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t})\n\n\t\tval, err := client.Get(ctx, \"key\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(int64(C * N)))\n\t})\n\n\tIt(\"should BLPop\", func() {\n\t\tC := 5\n\t\tN := 5\n\t\tvar received uint32\n\n\t\twg := performAsync(C, func(id int) {\n\t\t\tfor {\n\t\t\t\tv, err := client.BLPop(ctx, time.Second, \"list\").Result()\n\t\t\t\tif err != nil {\n\t\t\t\t\tif err == redis.Nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t\tExpect(v).To(Equal([]string{\"list\", \"hello\"}))\n\t\t\t\tatomic.AddUint32(&received, 1)\n\t\t\t}\n\t\t})\n\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := client.LPush(ctx, \"list\", \"hello\").Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t})\n\n\t\twg.Wait()\n\t\tExpect(atomic.LoadUint32(&received)).To(Equal(uint32(C * N)))\n\t})\n})\n\nvar _ = Describe(\"cluster races\", Label(\"NonRedisEnterprise\"), func() {\n\tvar client *redis.ClusterClient\n\tvar C, N int\n\n\tBeforeEach(func() {\n\t\topt := redisClusterOptions()\n\t\tclient = cluster.newClusterClient(ctx, opt)\n\n\t\tC, N = 10, 1000\n\t\tif testing.Short() {\n\t\t\tC = 4\n\t\t\tN = 100\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\terr := client.Close()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should echo\", func() {\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tmsg := fmt.Sprintf(\"echo %d %d\", id, i)\n\t\t\t\techo, err := client.Echo(ctx, msg).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(echo).To(Equal(msg))\n\t\t\t}\n\t\t})\n\t})\n\n\tIt(\"should get\", func() {\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\tkey := fmt.Sprintf(\"key_%d_%d\", id, i)\n\t\t\t\t_, err := client.Get(ctx, key).Result()\n\t\t\t\tExpect(err).To(Equal(redis.Nil))\n\t\t\t}\n\t\t})\n\t})\n\n\tIt(\"should incr\", func() {\n\t\tkey := \"TestIncrFromGoroutines\"\n\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := client.Incr(ctx, key).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t})\n\n\t\tval, err := client.Get(ctx, key).Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(int64(C * N)))\n\t})\n\n\tIt(\"write cmd data-race\", func() {\n\t\tpubsub := client.Subscribe(ctx)\n\t\tdefer pubsub.Close()\n\n\t\tpubsub.Channel(redis.WithChannelHealthCheckInterval(time.Millisecond))\n\t\tfor i := 0; i < 100; i++ {\n\t\t\tkey := fmt.Sprintf(\"channel_%d\", i)\n\t\t\tpubsub.Subscribe(ctx, key)\n\t\t\tpubsub.Unsubscribe(ctx, key)\n\t\t}\n\t})\n})\n\nfunc bigVal() []byte {\n\treturn bytes.Repeat([]byte{'*'}, 1<<17) // 128kb\n}\n"
  },
  {
    "path": "redis.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/auth/streaming\"\n\t\"github.com/redis/go-redis/v9/internal/hscan\"\n\t\"github.com/redis/go-redis/v9/internal/otel\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// Scanner internal/hscan.Scanner exposed interface.\ntype Scanner = hscan.Scanner\n\n// Nil reply returned by Redis when key does not exist.\nconst Nil = proto.Nil\n\n// SetLogger set custom log\n// Use with VoidLogger to disable logging.\n// If logger is nil, the call is ignored and the existing logger is kept.\nfunc SetLogger(logger internal.Logging) {\n\tif logger == nil {\n\t\treturn\n\t}\n\tinternal.Logger = logger\n}\n\n// SetLogLevel sets the log level for the library.\nfunc SetLogLevel(logLevel internal.LogLevelT) {\n\tinternal.LogLevel = logLevel\n}\n\n//------------------------------------------------------------------------------\n\ntype Hook interface {\n\tDialHook(next DialHook) DialHook\n\tProcessHook(next ProcessHook) ProcessHook\n\tProcessPipelineHook(next ProcessPipelineHook) ProcessPipelineHook\n}\n\ntype (\n\tDialHook            func(ctx context.Context, network, addr string) (net.Conn, error)\n\tProcessHook         func(ctx context.Context, cmd Cmder) error\n\tProcessPipelineHook func(ctx context.Context, cmds []Cmder) error\n)\n\ntype hooksMixin struct {\n\thooksMu *sync.RWMutex\n\n\tslice   []Hook\n\tinitial hooks\n\tcurrent hooks\n}\n\nfunc (hs *hooksMixin) initHooks(hooks hooks) {\n\ths.hooksMu = new(sync.RWMutex)\n\ths.initial = hooks\n\ths.chain()\n}\n\ntype hooks struct {\n\tdial       DialHook\n\tprocess    ProcessHook\n\tpipeline   ProcessPipelineHook\n\ttxPipeline ProcessPipelineHook\n}\n\nfunc (h *hooks) setDefaults() {\n\tif h.dial == nil {\n\t\th.dial = func(ctx context.Context, network, addr string) (net.Conn, error) { return nil, nil }\n\t}\n\tif h.process == nil {\n\t\th.process = func(ctx context.Context, cmd Cmder) error { return nil }\n\t}\n\tif h.pipeline == nil {\n\t\th.pipeline = func(ctx context.Context, cmds []Cmder) error { return nil }\n\t}\n\tif h.txPipeline == nil {\n\t\th.txPipeline = func(ctx context.Context, cmds []Cmder) error { return nil }\n\t}\n}\n\n// AddHook is to add a hook to the queue.\n// Hook is a function executed during network connection, command execution, and pipeline,\n// it is a first-in-first-out stack queue (FIFO).\n// You need to execute the next hook in each hook, unless you want to terminate the execution of the command.\n// For example, you added hook-1, hook-2:\n//\n//\tclient.AddHook(hook-1, hook-2)\n//\n// hook-1:\n//\n//\tfunc (Hook1) ProcessHook(next redis.ProcessHook) redis.ProcessHook {\n//\t \treturn func(ctx context.Context, cmd Cmder) error {\n//\t\t \tprint(\"hook-1 start\")\n//\t\t \tnext(ctx, cmd)\n//\t\t \tprint(\"hook-1 end\")\n//\t\t \treturn nil\n//\t \t}\n//\t}\n//\n// hook-2:\n//\n//\tfunc (Hook2) ProcessHook(next redis.ProcessHook) redis.ProcessHook {\n//\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n//\t\t\tprint(\"hook-2 start\")\n//\t\t\tnext(ctx, cmd)\n//\t\t\tprint(\"hook-2 end\")\n//\t\t\treturn nil\n//\t\t}\n//\t}\n//\n// The execution sequence is:\n//\n//\thook-1 start -> hook-2 start -> exec redis cmd -> hook-2 end -> hook-1 end\n//\n// Please note: \"next(ctx, cmd)\" is very important, it will call the next hook,\n// if \"next(ctx, cmd)\" is not executed, the redis command will not be executed.\nfunc (hs *hooksMixin) AddHook(hook Hook) {\n\ths.slice = append(hs.slice, hook)\n\ths.chain()\n}\n\nfunc (hs *hooksMixin) chain() {\n\ths.initial.setDefaults()\n\n\ths.hooksMu.Lock()\n\tdefer hs.hooksMu.Unlock()\n\n\ths.current.dial = hs.initial.dial\n\ths.current.process = hs.initial.process\n\ths.current.pipeline = hs.initial.pipeline\n\ths.current.txPipeline = hs.initial.txPipeline\n\n\tfor i := len(hs.slice) - 1; i >= 0; i-- {\n\t\tif wrapped := hs.slice[i].DialHook(hs.current.dial); wrapped != nil {\n\t\t\ths.current.dial = wrapped\n\t\t}\n\t\tif wrapped := hs.slice[i].ProcessHook(hs.current.process); wrapped != nil {\n\t\t\ths.current.process = wrapped\n\t\t}\n\t\tif wrapped := hs.slice[i].ProcessPipelineHook(hs.current.pipeline); wrapped != nil {\n\t\t\ths.current.pipeline = wrapped\n\t\t}\n\t\tif wrapped := hs.slice[i].ProcessPipelineHook(hs.current.txPipeline); wrapped != nil {\n\t\t\ths.current.txPipeline = wrapped\n\t\t}\n\t}\n}\n\nfunc (hs *hooksMixin) clone() hooksMixin {\n\ths.hooksMu.Lock()\n\tdefer hs.hooksMu.Unlock()\n\n\tclone := *hs\n\tl := len(clone.slice)\n\tclone.slice = clone.slice[:l:l]\n\tclone.hooksMu = new(sync.RWMutex)\n\treturn clone\n}\n\nfunc (hs *hooksMixin) withProcessHook(ctx context.Context, cmd Cmder, hook ProcessHook) error {\n\tfor i := len(hs.slice) - 1; i >= 0; i-- {\n\t\tif wrapped := hs.slice[i].ProcessHook(hook); wrapped != nil {\n\t\t\thook = wrapped\n\t\t}\n\t}\n\treturn hook(ctx, cmd)\n}\n\nfunc (hs *hooksMixin) withProcessPipelineHook(\n\tctx context.Context, cmds []Cmder, hook ProcessPipelineHook,\n) error {\n\tfor i := len(hs.slice) - 1; i >= 0; i-- {\n\t\tif wrapped := hs.slice[i].ProcessPipelineHook(hook); wrapped != nil {\n\t\t\thook = wrapped\n\t\t}\n\t}\n\treturn hook(ctx, cmds)\n}\n\nfunc (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) {\n\t// Access to hs.current is guarded by a read-only lock since it may be mutated by AddHook(...)\n\t// while this dialer is concurrently accessed by the background connection pool population\n\t// routine when MinIdleConns > 0.\n\ths.hooksMu.RLock()\n\tcurrent := hs.current\n\ths.hooksMu.RUnlock()\n\n\treturn current.dial(ctx, network, addr)\n}\n\nfunc (hs *hooksMixin) processHook(ctx context.Context, cmd Cmder) error {\n\treturn hs.current.process(ctx, cmd)\n}\n\nfunc (hs *hooksMixin) processPipelineHook(ctx context.Context, cmds []Cmder) error {\n\treturn hs.current.pipeline(ctx, cmds)\n}\n\nfunc (hs *hooksMixin) processTxPipelineHook(ctx context.Context, cmds []Cmder) error {\n\treturn hs.current.txPipeline(ctx, cmds)\n}\n\n//------------------------------------------------------------------------------\n\ntype baseClient struct {\n\topt        *Options\n\toptLock    sync.RWMutex\n\tconnPool   pool.Pooler\n\tpubSubPool *pool.PubSubPool\n\thooksMixin\n\n\tonClose func() error // hook called when client is closed\n\n\t// Push notification processing\n\tpushProcessor push.NotificationProcessor\n\n\t// Maintenance notifications manager\n\tmaintNotificationsManager     *maintnotifications.Manager\n\tmaintNotificationsManagerLock sync.RWMutex\n\n\t// streamingCredentialsManager is used to manage streaming credentials\n\tstreamingCredentialsManager *streaming.Manager\n}\n\nfunc (c *baseClient) clone() *baseClient {\n\tc.maintNotificationsManagerLock.RLock()\n\tmaintNotificationsManager := c.maintNotificationsManager\n\tc.maintNotificationsManagerLock.RUnlock()\n\n\tclone := &baseClient{\n\t\topt:                         c.opt,\n\t\tconnPool:                    c.connPool,\n\t\tpubSubPool:                  c.pubSubPool,\n\t\tonClose:                     c.onClose,\n\t\tpushProcessor:               c.pushProcessor,\n\t\tmaintNotificationsManager:   maintNotificationsManager,\n\t\tstreamingCredentialsManager: c.streamingCredentialsManager,\n\t}\n\treturn clone\n}\n\n// cloneOpt clones c.opt while holding optLock to prevent races with initConn\n// which writes to MaintNotificationsConfig.Mode under the same lock.\nfunc (c *baseClient) cloneOpt() *Options {\n\tc.optLock.RLock()\n\tclone := c.opt.clone()\n\tc.optLock.RUnlock()\n\treturn clone\n}\n\nfunc (c *baseClient) withTimeout(timeout time.Duration) *baseClient {\n\topt := c.cloneOpt()\n\topt.ReadTimeout = timeout\n\topt.WriteTimeout = timeout\n\n\tclone := c.clone()\n\tclone.opt = opt\n\n\treturn clone\n}\n\nfunc (c *baseClient) String() string {\n\treturn fmt.Sprintf(\"Redis<%s db:%d>\", c.getAddr(), c.opt.DB)\n}\n\nfunc (c *baseClient) getConn(ctx context.Context) (*pool.Conn, error) {\n\tif c.opt.Limiter != nil {\n\t\terr := c.opt.Limiter.Allow()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tcn, err := c._getConn(ctx)\n\tif err != nil {\n\t\tif c.opt.Limiter != nil {\n\t\t\tc.opt.Limiter.ReportResult(err)\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn cn, nil\n}\n\nfunc (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) {\n\tcn, err := c.connPool.Get(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif cn.IsInited() {\n\t\treturn cn, nil\n\t}\n\n\tif err := c.initConn(ctx, cn); err != nil {\n\t\tc.connPool.Remove(ctx, cn, err)\n\t\tif err := errors.Unwrap(err); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tif dialStartNs := cn.GetDialStartNs(); dialStartNs > 0 {\n\t\tif cb := pool.GetMetricConnectionCreateTimeCallback(); cb != nil {\n\t\t\tduration := time.Duration(time.Now().UnixNano() - dialStartNs)\n\t\t\tcb(ctx, duration, cn)\n\t\t}\n\t}\n\n\t// initConn will transition to IDLE state, so we need to acquire it\n\t// before returning it to the user.\n\tif !cn.TryAcquire() {\n\t\treturn nil, fmt.Errorf(\"redis: connection is not usable\")\n\t}\n\n\treturn cn, nil\n}\n\nfunc (c *baseClient) reAuthConnection() func(poolCn *pool.Conn, credentials auth.Credentials) error {\n\treturn func(poolCn *pool.Conn, credentials auth.Credentials) error {\n\t\tvar err error\n\t\tusername, password := credentials.BasicAuth()\n\n\t\t// Use background context - timeout is handled by ReadTimeout in WithReader/WithWriter\n\t\tctx := context.Background()\n\n\t\tconnPool := pool.NewSingleConnPool(c.connPool, poolCn)\n\n\t\t// Pass hooks so that reauth commands are recorded/traced\n\t\tcn := newConn(c.opt, connPool, &c.hooksMixin)\n\n\t\tif username != \"\" {\n\t\t\terr = cn.AuthACL(ctx, username, password).Err()\n\t\t} else {\n\t\t\terr = cn.Auth(ctx, password).Err()\n\t\t}\n\n\t\treturn err\n\t}\n}\nfunc (c *baseClient) onAuthenticationErr() func(poolCn *pool.Conn, err error) {\n\treturn func(poolCn *pool.Conn, err error) {\n\t\tif err != nil {\n\t\t\tif isBadConn(err, false, c.opt.Addr) {\n\t\t\t\t// Close the connection to force a reconnection.\n\t\t\t\t// Re-auth happens on connections that were idle in the pool (the pool hook\n\t\t\t\t// waits for IDLE state before transitioning to UNUSABLE for re-auth).\n\t\t\t\t// From metrics perspective, the connection was never \"used\" by a client.\n\t\t\t\t// Note: Using context.Background() as this callback doesn't have access to caller's context.\n\t\t\t\terr := c.connPool.CloseConn(context.Background(), poolCn, pool.CloseReasonAuthError, pool.MetricStateIdle)\n\t\t\t\tif err != nil {\n\t\t\t\t\tinternal.Logger.Printf(context.Background(), \"redis: failed to close connection: %v\", err)\n\t\t\t\t\t// try to close the network connection directly\n\t\t\t\t\t// so that no resource is leaked\n\t\t\t\t\terr := poolCn.Close()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tinternal.Logger.Printf(context.Background(), \"redis: failed to close network connection: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tinternal.Logger.Printf(context.Background(), \"redis: re-authentication failed: %v\", err)\n\t\t}\n\t}\n}\n\nfunc (c *baseClient) wrappedOnClose(newOnClose func() error) func() error {\n\tonClose := c.onClose\n\treturn func() error {\n\t\tvar firstErr error\n\t\terr := newOnClose()\n\t\t// Even if we have an error we would like to execute the onClose hook\n\t\t// if it exists. We will return the first error that occurred.\n\t\t// This is to keep error handling consistent with the rest of the code.\n\t\tif err != nil {\n\t\t\tfirstErr = err\n\t\t}\n\t\tif onClose != nil {\n\t\t\terr = onClose()\n\t\t\tif err != nil && firstErr == nil {\n\t\t\t\tfirstErr = err\n\t\t\t}\n\t\t}\n\t\treturn firstErr\n\t}\n}\n\nfunc (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {\n\t// This function is called in two scenarios:\n\t// 1. First-time init: Connection is in CREATED state (from pool.Get())\n\t//    - We need to transition CREATED → INITIALIZING and do the initialization\n\t//    - If another goroutine is already initializing, we WAIT for it to finish\n\t// 2. Re-initialization: Connection is in INITIALIZING state (from SetNetConnAndInitConn())\n\t//    - We're already in INITIALIZING, so just proceed with initialization\n\n\tcurrentState := cn.GetStateMachine().GetState()\n\n\t// Fast path: Check if already initialized (IDLE or IN_USE)\n\tif currentState == pool.StateIdle || currentState == pool.StateInUse {\n\t\treturn nil\n\t}\n\n\t// If in CREATED state, try to transition to INITIALIZING\n\tif currentState == pool.StateCreated {\n\t\tfinalState, err := cn.GetStateMachine().TryTransition([]pool.ConnState{pool.StateCreated}, pool.StateInitializing)\n\t\tif err != nil {\n\t\t\t// Another goroutine is initializing or connection is in unexpected state\n\t\t\t// Check what state we're in now\n\t\t\tif finalState == pool.StateIdle || finalState == pool.StateInUse {\n\t\t\t\t// Already initialized by another goroutine\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif finalState == pool.StateInitializing {\n\t\t\t\t// Another goroutine is initializing - WAIT for it to complete\n\t\t\t\t// Use a context with timeout = min(remaining command timeout, DialTimeout)\n\t\t\t\t// This prevents waiting too long while respecting the caller's deadline\n\t\t\t\tvar waitCtx context.Context\n\t\t\t\tvar cancel context.CancelFunc\n\t\t\t\tdialTimeout := c.opt.DialTimeout\n\n\t\t\t\tif cmdDeadline, hasCmdDeadline := ctx.Deadline(); hasCmdDeadline {\n\t\t\t\t\t// Calculate remaining time until command deadline\n\t\t\t\t\tremainingTime := time.Until(cmdDeadline)\n\t\t\t\t\t// Use the minimum of remaining time and DialTimeout\n\t\t\t\t\tif remainingTime < dialTimeout {\n\t\t\t\t\t\t// Command deadline is sooner, use it\n\t\t\t\t\t\twaitCtx = ctx\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// DialTimeout is shorter, cap the wait at DialTimeout\n\t\t\t\t\t\twaitCtx, cancel = context.WithTimeout(ctx, dialTimeout)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No command deadline, use DialTimeout to prevent waiting indefinitely\n\t\t\t\t\twaitCtx, cancel = context.WithTimeout(ctx, dialTimeout)\n\t\t\t\t}\n\t\t\t\tif cancel != nil {\n\t\t\t\t\tdefer cancel()\n\t\t\t\t}\n\n\t\t\t\tfinalState, err := cn.GetStateMachine().AwaitAndTransition(\n\t\t\t\t\twaitCtx,\n\t\t\t\t\t[]pool.ConnState{pool.StateIdle, pool.StateInUse},\n\t\t\t\t\tpool.StateIdle, // Target is IDLE (but we're already there, so this is a no-op)\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// Verify we're now initialized\n\t\t\t\tif finalState == pool.StateIdle || finalState == pool.StateInUse {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\t// Unexpected state after waiting\n\t\t\t\treturn fmt.Errorf(\"connection in unexpected state after initialization: %s\", finalState)\n\t\t\t}\n\n\t\t\t// Unexpected state (CLOSED, UNUSABLE, etc.)\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// At this point, we're in INITIALIZING state and we own the initialization\n\t// If we fail, we must transition to CLOSED\n\tvar initErr error\n\tconnPool := pool.NewSingleConnPool(c.connPool, cn)\n\tconn := newConn(c.opt, connPool, &c.hooksMixin)\n\n\tusername, password := \"\", \"\"\n\tif c.opt.StreamingCredentialsProvider != nil {\n\t\tcredListener, initErr := c.streamingCredentialsManager.Listener(\n\t\t\tcn,\n\t\t\tc.reAuthConnection(),\n\t\t\tc.onAuthenticationErr(),\n\t\t)\n\t\tif initErr != nil {\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn fmt.Errorf(\"failed to create credentials listener: %w\", initErr)\n\t\t}\n\n\t\tcredentials, unsubscribeFromCredentialsProvider, initErr := c.opt.StreamingCredentialsProvider.\n\t\t\tSubscribe(credListener)\n\t\tif initErr != nil {\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn fmt.Errorf(\"failed to subscribe to streaming credentials: %w\", initErr)\n\t\t}\n\n\t\tc.onClose = c.wrappedOnClose(unsubscribeFromCredentialsProvider)\n\t\tcn.SetOnClose(unsubscribeFromCredentialsProvider)\n\n\t\tusername, password = credentials.BasicAuth()\n\t} else if c.opt.CredentialsProviderContext != nil {\n\t\tusername, password, initErr = c.opt.CredentialsProviderContext(ctx)\n\t\tif initErr != nil {\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn fmt.Errorf(\"failed to get credentials from context provider: %w\", initErr)\n\t\t}\n\t} else if c.opt.CredentialsProvider != nil {\n\t\tusername, password = c.opt.CredentialsProvider()\n\t} else if c.opt.Username != \"\" || c.opt.Password != \"\" {\n\t\tusername, password = c.opt.Username, c.opt.Password\n\t}\n\n\t// for redis-server versions that do not support the HELLO command,\n\t// RESP2 will continue to be used.\n\tif initErr = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); initErr == nil {\n\t\t// Authentication successful with HELLO command\n\t} else if !isRedisError(initErr) {\n\t\t// When the server responds with the RESP protocol and the result is not a normal\n\t\t// execution result of the HELLO command, we consider it to be an indication that\n\t\t// the server does not support the HELLO command.\n\t\t// The server may be a redis-server that does not support the HELLO command,\n\t\t// or it could be DragonflyDB or a third-party redis-proxy. They all respond\n\t\t// with different error string results for unsupported commands, making it\n\t\t// difficult to rely on error strings to determine all results.\n\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\treturn initErr\n\t} else if password != \"\" {\n\t\t// Try legacy AUTH command if HELLO failed\n\t\tif username != \"\" {\n\t\t\tinitErr = conn.AuthACL(ctx, username, password).Err()\n\t\t} else {\n\t\t\tinitErr = conn.Auth(ctx, password).Err()\n\t\t}\n\t\tif initErr != nil {\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn fmt.Errorf(\"failed to authenticate: %w\", initErr)\n\t\t}\n\t}\n\n\t_, initErr = conn.Pipelined(ctx, func(pipe Pipeliner) error {\n\t\tif c.opt.DB > 0 {\n\t\t\tpipe.Select(ctx, c.opt.DB)\n\t\t}\n\n\t\tif c.opt.readOnly {\n\t\t\tpipe.ReadOnly(ctx)\n\t\t}\n\n\t\tif c.opt.ClientName != \"\" {\n\t\t\tpipe.ClientSetName(ctx, c.opt.ClientName)\n\t\t}\n\n\t\treturn nil\n\t})\n\tif initErr != nil {\n\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\treturn fmt.Errorf(\"failed to initialize connection options: %w\", initErr)\n\t}\n\n\t// Enable maintnotifications if maintnotifications are configured\n\tc.optLock.RLock()\n\tmaintNotifEnabled := c.opt.MaintNotificationsConfig != nil && c.opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled\n\tprotocol := c.opt.Protocol\n\tvar endpointType maintnotifications.EndpointType\n\tif maintNotifEnabled {\n\t\tendpointType = c.opt.MaintNotificationsConfig.EndpointType\n\t}\n\tc.optLock.RUnlock()\n\tvar maintNotifHandshakeErr error\n\tif maintNotifEnabled && protocol == 3 {\n\t\tmaintNotifHandshakeErr = conn.ClientMaintNotifications(\n\t\t\tctx,\n\t\t\ttrue,\n\t\t\tendpointType.String(),\n\t\t).Err()\n\t\tif maintNotifHandshakeErr != nil {\n\t\t\tif !isRedisError(maintNotifHandshakeErr) {\n\t\t\t\t// if not redis error, fail the connection\n\t\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\t\treturn maintNotifHandshakeErr\n\t\t\t}\n\t\t\tc.optLock.Lock()\n\t\t\t// handshake failed - check and modify config atomically\n\t\t\tswitch c.opt.MaintNotificationsConfig.Mode {\n\t\t\tcase maintnotifications.ModeEnabled:\n\t\t\t\t// enabled mode, fail the connection\n\t\t\t\tc.optLock.Unlock()\n\t\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\n\t\t\t\t// Record handshake failure metric\n\t\t\t\tif errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {\n\t\t\t\t\terrorCallback(ctx, \"HANDSHAKE_FAILED\", cn, \"HANDSHAKE_FAILED\", true, 0)\n\t\t\t\t}\n\n\t\t\t\treturn fmt.Errorf(\"failed to enable maintnotifications: %w\", maintNotifHandshakeErr)\n\t\t\tdefault: // will handle auto and any other\n\t\t\t\t// Disabling logging here as it's too noisy.\n\t\t\t\t// TODO: Enable when we have a better logging solution for log levels\n\t\t\t\t// internal.Logger.Printf(ctx, \"auto mode fallback: maintnotifications disabled due to handshake error: %v\", maintNotifHandshakeErr)\n\t\t\t\tc.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeDisabled\n\t\t\t\tc.optLock.Unlock()\n\t\t\t\t// auto mode, disable maintnotifications and continue\n\t\t\t\tif initErr := c.disableMaintNotificationsUpgrades(); initErr != nil {\n\t\t\t\t\t// Log error but continue - auto mode should be resilient\n\t\t\t\t\tinternal.Logger.Printf(ctx, \"failed to disable maintnotifications in auto mode: %v\", initErr)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// handshake was executed successfully\n\t\t\t// to make sure that the handshake will be executed on other connections as well if it was successfully\n\t\t\t// executed on this connection, we will force the handshake to be executed on all connections\n\t\t\tc.optLock.Lock()\n\t\t\tc.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeEnabled\n\t\t\tc.optLock.Unlock()\n\t\t}\n\t}\n\n\tif !c.opt.DisableIdentity && !c.opt.DisableIndentity {\n\t\tlibName := \"\"\n\t\tlibVer := Version()\n\t\tif c.opt.IdentitySuffix != \"\" {\n\t\t\tlibName = c.opt.IdentitySuffix\n\t\t}\n\t\tp := conn.Pipeline()\n\t\tp.ClientSetInfo(ctx, WithLibraryName(libName))\n\t\tp.ClientSetInfo(ctx, WithLibraryVersion(libVer))\n\t\t// Handle network errors (e.g. timeouts) in CLIENT SETINFO to avoid\n\t\t// out of order responses later on.\n\t\tif _, initErr = p.Exec(ctx); initErr != nil && !isRedisError(initErr) {\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn initErr\n\t\t}\n\t}\n\n\t// Set the connection initialization function for potential reconnections\n\t// This must be set before transitioning to IDLE so that handoff/reauth can use it\n\tcn.SetInitConnFunc(c.createInitConnFunc())\n\n\t// Initialization succeeded - transition to IDLE state\n\t// This marks the connection as initialized and ready for use\n\t// NOTE: The connection is still owned by the calling goroutine at this point\n\t// and won't be available to other goroutines until it's Put() back into the pool\n\tcn.GetStateMachine().Transition(pool.StateIdle)\n\n\t// Call OnConnect hook if configured\n\t// The connection is in IDLE state but still owned by this goroutine\n\t// If OnConnect needs to send commands, it can use the connection safely\n\tif c.opt.OnConnect != nil {\n\t\tif initErr = c.opt.OnConnect(ctx, conn); initErr != nil {\n\t\t\t// OnConnect failed - transition to closed\n\t\t\tcn.GetStateMachine().Transition(pool.StateClosed)\n\t\t\treturn initErr\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) {\n\tif c.opt.Limiter != nil {\n\t\tc.opt.Limiter.ReportResult(err)\n\t}\n\n\tif isBadConn(err, false, c.opt.Addr) {\n\t\tc.connPool.Remove(ctx, cn, err)\n\t} else {\n\t\t// process any pending push notifications before returning the connection to the pool\n\t\tif err := c.processPushNotifications(ctx, cn); err != nil {\n\t\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before releasing connection: %v\", err)\n\t\t}\n\t\tc.connPool.Put(ctx, cn)\n\t}\n}\n\nfunc (c *baseClient) withConn(\n\tctx context.Context, fn func(context.Context, *pool.Conn) error,\n) error {\n\tcn, err := c.getConn(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar fnErr error\n\tdefer func() {\n\t\tc.releaseConn(ctx, cn, fnErr)\n\t}()\n\n\tfnErr = fn(ctx, cn)\n\n\treturn fnErr\n}\n\nfunc (c *baseClient) dial(ctx context.Context, network, addr string) (net.Conn, error) {\n\treturn c.opt.Dialer(ctx, network, addr)\n}\n\nfunc (c *baseClient) process(ctx context.Context, cmd Cmder) error {\n\t// Start measuring total operation duration (includes all retries)\n\t// Only call time.Now() if operation duration callback is set to avoid overhead\n\tvar operationStart time.Time\n\topDurationCallback := otel.GetOperationDurationCallback()\n\tif opDurationCallback != nil {\n\t\toperationStart = time.Now()\n\t}\n\tvar lastConn *pool.Conn\n\n\tvar lastErr error\n\ttotalAttempts := 0\n\tfor attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {\n\t\ttotalAttempts++\n\t\tattempt := attempt\n\n\t\tretry, cn, err := c._process(ctx, cmd, attempt)\n\t\tif cn != nil {\n\t\t\tlastConn = cn\n\t\t}\n\t\tif err == nil || !retry {\n\t\t\t// Record total operation duration\n\t\t\tif opDurationCallback != nil {\n\t\t\t\toperationDuration := time.Since(operationStart)\n\t\t\t\topDurationCallback(ctx, operationDuration, cmd, totalAttempts, err, lastConn, c.opt.DB)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tif errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {\n\t\t\t\t\terrorType, statusCode, isInternal := classifyCommandError(err)\n\t\t\t\t\terrorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tlastErr = err\n\t}\n\n\t// Record failed operation after all retries\n\tif opDurationCallback != nil {\n\t\toperationDuration := time.Since(operationStart)\n\t\topDurationCallback(ctx, operationDuration, cmd, totalAttempts, lastErr, lastConn, c.opt.DB)\n\t}\n\n\t// Record error metric for exhausted retries\n\tif errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {\n\t\terrorType, statusCode, isInternal := classifyCommandError(lastErr)\n\t\terrorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)\n\t}\n\n\treturn lastErr\n}\n\n// classifyCommandError classifies an error for metrics reporting.\n// Returns: errorType, statusCode, isInternal\n// - errorType: A string describing the error type (e.g., \"TIMEOUT\", \"NETWORK\", \"ERR\")\n// - statusCode: The Redis error prefix or error category\n// - isInternal: true for network/timeout errors, false for Redis server errors\nfunc classifyCommandError(err error) (errorType, statusCode string, isInternal bool) {\n\tif err == nil {\n\t\treturn \"\", \"\", false\n\t}\n\n\terrStr := err.Error()\n\n\t// Check for timeout errors\n\tif netErr, ok := err.(net.Error); ok && netErr.Timeout() {\n\t\treturn \"TIMEOUT\", \"TIMEOUT\", true\n\t}\n\n\t// Check for network errors\n\tif _, ok := err.(net.Error); ok {\n\t\treturn \"NETWORK\", \"NETWORK\", true\n\t}\n\n\t// Check for context errors\n\tif errors.Is(err, context.Canceled) {\n\t\treturn \"CONTEXT_CANCELED\", \"CONTEXT_CANCELED\", true\n\t}\n\tif errors.Is(err, context.DeadlineExceeded) {\n\t\treturn \"CONTEXT_TIMEOUT\", \"CONTEXT_TIMEOUT\", true\n\t}\n\n\t// Check for Redis errors\n\t// Examples: \"ERR ...\", \"WRONGTYPE ...\", \"CLUSTERDOWN ...\"\n\tif len(errStr) > 0 {\n\t\t// Find the first space to extract the prefix\n\t\tspaceIdx := 0\n\t\tfor i, c := range errStr {\n\t\t\tif c == ' ' {\n\t\t\t\tspaceIdx = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif spaceIdx == 0 {\n\t\t\tspaceIdx = len(errStr)\n\t\t}\n\t\tprefix := errStr[:spaceIdx]\n\t\tisUppercase := true\n\t\tfor _, c := range prefix {\n\t\t\tif c < 'A' || c > 'Z' {\n\t\t\t\tisUppercase = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif isUppercase && len(prefix) > 0 {\n\t\t\treturn prefix, prefix, false\n\t\t}\n\t}\n\n\treturn \"UNKNOWN\", \"UNKNOWN\", true\n}\n\nfunc (c *baseClient) assertUnstableCommand(cmd Cmder) (bool, error) {\n\tswitch cmd.(type) {\n\tcase *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd:\n\t\tif c.opt.UnstableResp3 {\n\t\t\treturn true, nil\n\t\t} else {\n\t\t\treturn false, fmt.Errorf(\"RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3. See the README and the release notes for guidance\")\n\t\t}\n\tdefault:\n\t\treturn false, nil\n\t}\n}\n\nfunc (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, *pool.Conn, error) {\n\tif attempt > 0 {\n\t\tif err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {\n\t\t\treturn false, nil, err\n\t\t}\n\t}\n\n\tvar usedConn *pool.Conn\n\tretryTimeout := uint32(0)\n\tif err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {\n\t\tusedConn = cn\n\t\t// Process any pending push notifications before executing the command\n\t\tif err := c.processPushNotifications(ctx, cn); err != nil {\n\t\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before command: %v\", err)\n\t\t}\n\n\t\tif err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {\n\t\t\treturn writeCmd(wr, cmd)\n\t\t}); err != nil {\n\t\t\tatomic.StoreUint32(&retryTimeout, 1)\n\t\t\treturn err\n\t\t}\n\t\treadReplyFunc := cmd.readReply\n\t\t// Apply unstable RESP3 search module.\n\t\tif c.opt.Protocol != 2 {\n\t\t\tuseRawReply, err := c.assertUnstableCommand(cmd)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif useRawReply {\n\t\t\t\treadReplyFunc = cmd.readRawReply\n\t\t\t}\n\t\t}\n\t\tif err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), func(rd *proto.Reader) error {\n\t\t\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\t\t\tif err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\t\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t\t\t}\n\t\t\treturn readReplyFunc(rd)\n\t\t}); err != nil {\n\t\t\tif cmd.readTimeout() == nil {\n\t\t\t\tatomic.StoreUint32(&retryTimeout, 1)\n\t\t\t} else {\n\t\t\t\tatomic.StoreUint32(&retryTimeout, 0)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}); err != nil {\n\t\tretry := shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1)\n\t\treturn retry, usedConn, err\n\t}\n\n\treturn false, usedConn, nil\n}\n\nfunc (c *baseClient) retryBackoff(attempt int) time.Duration {\n\treturn internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)\n}\n\nfunc (c *baseClient) cmdTimeout(cmd Cmder) time.Duration {\n\tif timeout := cmd.readTimeout(); timeout != nil {\n\t\tt := *timeout\n\t\tif t == 0 {\n\t\t\treturn 0\n\t\t}\n\t\treturn t + 10*time.Second\n\t}\n\treturn c.opt.ReadTimeout\n}\n\n// context returns the context for the current connection.\n// If the context timeout is enabled, it returns the original context.\n// Otherwise, it returns a new background context.\nfunc (c *baseClient) context(ctx context.Context) context.Context {\n\tif c.opt.ContextTimeoutEnabled {\n\t\treturn ctx\n\t}\n\treturn context.Background()\n}\n\n// createInitConnFunc creates a connection initialization function that can be used for reconnections.\nfunc (c *baseClient) createInitConnFunc() func(context.Context, *pool.Conn) error {\n\treturn func(ctx context.Context, cn *pool.Conn) error {\n\t\treturn c.initConn(ctx, cn)\n\t}\n}\n\n// enableMaintNotificationsUpgrades initializes the maintnotifications upgrade manager and pool hook.\n// This function is called during client initialization.\n// will register push notification handlers for all maintenance upgrade events.\n// will start background workers for handoff processing in the pool hook.\nfunc (c *baseClient) enableMaintNotificationsUpgrades() error {\n\t// Create client adapter\n\tclientAdapterInstance := newClientAdapter(c)\n\n\t// Create maintnotifications manager directly\n\tmanager, err := maintnotifications.NewManager(clientAdapterInstance, c.connPool, c.opt.MaintNotificationsConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Set the manager reference and initialize pool hook\n\tc.maintNotificationsManagerLock.Lock()\n\tc.maintNotificationsManager = manager\n\tc.maintNotificationsManagerLock.Unlock()\n\n\t// Initialize pool hook (safe to call without lock since manager is now set)\n\tmanager.InitPoolHook(c.dialHook)\n\treturn nil\n}\n\nfunc (c *baseClient) disableMaintNotificationsUpgrades() error {\n\tc.maintNotificationsManagerLock.Lock()\n\tdefer c.maintNotificationsManagerLock.Unlock()\n\n\t// Close the maintnotifications manager\n\tif c.maintNotificationsManager != nil {\n\t\t// Closing the manager will also shutdown the pool hook\n\t\t// and remove it from the pool\n\t\tc.maintNotificationsManager.Close()\n\t\tc.maintNotificationsManager = nil\n\t}\n\treturn nil\n}\n\n// Close closes the client, releasing any open resources.\n//\n// It is rare to Close a Client, as the Client is meant to be\n// long-lived and shared between many goroutines.\nfunc (c *baseClient) Close() error {\n\tvar firstErr error\n\n\t// Close maintnotifications manager first\n\tif err := c.disableMaintNotificationsUpgrades(); err != nil {\n\t\tfirstErr = err\n\t}\n\n\tif c.onClose != nil {\n\t\tif err := c.onClose(); err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\n\t// Unregister pools from OTel before closing them\n\totel.UnregisterPools(c.connPool, c.pubSubPool)\n\n\tif c.connPool != nil {\n\t\tif err := c.connPool.Close(); err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\tif c.pubSubPool != nil {\n\t\tif err := c.pubSubPool.Close(); err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\treturn firstErr\n}\n\nfunc (c *baseClient) getAddr() string {\n\treturn c.opt.Addr\n}\n\nfunc (c *baseClient) processPipeline(ctx context.Context, cmds []Cmder) error {\n\tif err := c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds, \"PIPELINE\"); err != nil {\n\t\treturn err\n\t}\n\treturn cmdsFirstErr(cmds)\n}\n\nfunc (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {\n\tif err := c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds, \"MULTI\"); err != nil {\n\t\treturn err\n\t}\n\treturn cmdsFirstErr(cmds)\n}\n\ntype pipelineProcessor func(context.Context, *pool.Conn, []Cmder) (bool, error)\n\nfunc (c *baseClient) generalProcessPipeline(\n\tctx context.Context, cmds []Cmder, p pipelineProcessor, operationName string,\n) error {\n\t// Only call time.Now() if pipeline operation duration callback is set to avoid overhead\n\tvar operationStart time.Time\n\tpipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback()\n\tif pipelineOpDurationCallback != nil {\n\t\toperationStart = time.Now()\n\t}\n\tvar lastConn *pool.Conn\n\ttotalAttempts := 0\n\n\tvar lastErr error\n\tfor attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {\n\t\ttotalAttempts++\n\t\tif attempt > 0 {\n\t\t\tif err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {\n\t\t\t\tsetCmdsErr(cmds, err)\n\t\t\t\tif pipelineOpDurationCallback != nil {\n\t\t\t\t\toperationDuration := time.Since(operationStart)\n\t\t\t\t\tpipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, err, lastConn, c.opt.DB)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// Enable retries by default to retry dial errors returned by withConn.\n\t\tcanRetry := true\n\t\tlastErr = c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {\n\t\t\tlastConn = cn\n\t\t\t// Process any pending push notifications before executing the pipeline\n\t\t\tif err := c.processPushNotifications(ctx, cn); err != nil {\n\t\t\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before processing pipeline: %v\", err)\n\t\t\t}\n\t\t\tvar err error\n\t\t\tcanRetry, err = p(ctx, cn, cmds)\n\t\t\treturn err\n\t\t})\n\t\tif lastErr == nil || !canRetry || !shouldRetry(lastErr, true) {\n\t\t\t// The error should be set here only when failing to obtain the conn.\n\t\t\tif !isRedisError(lastErr) {\n\t\t\t\tsetCmdsErr(cmds, lastErr)\n\t\t\t}\n\t\t\tif pipelineOpDurationCallback != nil {\n\t\t\t\toperationDuration := time.Since(operationStart)\n\t\t\t\tpipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, lastErr, lastConn, c.opt.DB)\n\t\t\t}\n\n\t\t\tif lastErr != nil {\n\t\t\t\tif errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {\n\t\t\t\t\terrorType, statusCode, isInternal := classifyCommandError(lastErr)\n\t\t\t\t\terrorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn lastErr\n\t\t}\n\t}\n\n\tif pipelineOpDurationCallback != nil {\n\t\toperationDuration := time.Since(operationStart)\n\t\tpipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, lastErr, lastConn, c.opt.DB)\n\t}\n\n\tif errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {\n\t\terrorType, statusCode, isInternal := classifyCommandError(lastErr)\n\t\terrorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)\n\t}\n\n\treturn lastErr\n}\n\nfunc (c *baseClient) pipelineProcessCmds(\n\tctx context.Context, cn *pool.Conn, cmds []Cmder,\n) (bool, error) {\n\t// Process any pending push notifications before executing the pipeline\n\tif err := c.processPushNotifications(ctx, cn); err != nil {\n\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before writing pipeline: %v\", err)\n\t}\n\n\tif err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {\n\t\treturn writeCmds(wr, cmds)\n\t}); err != nil {\n\t\tsetCmdsErr(cmds, err)\n\t\treturn true, err\n\t}\n\n\tif err := cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {\n\t\t// read all replies\n\t\treturn c.pipelineReadCmds(ctx, cn, rd, cmds)\n\t}); err != nil {\n\t\treturn true, err\n\t}\n\n\treturn false, nil\n}\n\nfunc (c *baseClient) pipelineReadCmds(ctx context.Context, cn *pool.Conn, rd *proto.Reader, cmds []Cmder) error {\n\tfor i, cmd := range cmds {\n\t\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\t\tif err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t\t}\n\t\terr := cmd.readReply(rd)\n\t\tcmd.SetErr(err)\n\t\tif err != nil && !isRedisError(err) {\n\t\t\tsetCmdsErr(cmds[i+1:], err)\n\t\t\treturn err\n\t\t}\n\t}\n\t// Retry errors like \"LOADING redis is loading the dataset in memory\".\n\treturn cmds[0].Err()\n}\n\nfunc (c *baseClient) txPipelineProcessCmds(\n\tctx context.Context, cn *pool.Conn, cmds []Cmder,\n) (bool, error) {\n\t// Process any pending push notifications before executing the transaction pipeline\n\tif err := c.processPushNotifications(ctx, cn); err != nil {\n\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before transaction: %v\", err)\n\t}\n\n\tif err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {\n\t\treturn writeCmds(wr, cmds)\n\t}); err != nil {\n\t\tsetCmdsErr(cmds, err)\n\t\treturn true, err\n\t}\n\n\tif err := cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {\n\t\tstatusCmd := cmds[0].(*StatusCmd)\n\t\t// Trim multi and exec.\n\t\ttrimmedCmds := cmds[1 : len(cmds)-1]\n\n\t\tif err := c.txPipelineReadQueued(ctx, cn, rd, statusCmd, trimmedCmds); err != nil {\n\t\t\tsetCmdsErr(cmds, err)\n\t\t\treturn err\n\t\t}\n\n\t\t// Read replies.\n\t\treturn c.pipelineReadCmds(ctx, cn, rd, trimmedCmds)\n\t}); err != nil {\n\t\treturn false, err\n\t}\n\n\treturn false, nil\n}\n\n// txPipelineReadQueued reads queued replies from the Redis server.\n// It returns an error if the server returns an error or if the number of replies does not match the number of commands.\nfunc (c *baseClient) txPipelineReadQueued(ctx context.Context, cn *pool.Conn, rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) error {\n\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\tif err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t}\n\t// Parse +OK.\n\tif err := statusCmd.readReply(rd); err != nil {\n\t\treturn err\n\t}\n\n\t// Parse +QUEUED.\n\tfor _, cmd := range cmds {\n\t\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\t\tif err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t\t}\n\t\tif err := statusCmd.readReply(rd); err != nil {\n\t\t\tcmd.SetErr(err)\n\t\t\tif !isRedisError(err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// To be sure there are no buffered push notifications, we process them before reading the reply\n\tif err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {\n\t\tinternal.Logger.Printf(ctx, \"push: error processing pending notifications before reading reply: %v\", err)\n\t}\n\t// Parse number of replies.\n\tline, err := rd.ReadLine()\n\tif err != nil {\n\t\tif err == Nil {\n\t\t\terr = TxFailedErr\n\t\t}\n\t\treturn err\n\t}\n\n\tif line[0] != proto.RespArray {\n\t\treturn fmt.Errorf(\"redis: expected '*', but got line %q\", line)\n\t}\n\n\treturn nil\n}\n\n//------------------------------------------------------------------------------\n\n// Client is a Redis client representing a pool of zero or more underlying connections.\n// It's safe for concurrent use by multiple goroutines.\n//\n// Client creates and frees connections automatically; it also maintains a free pool\n// of idle connections. You can control the pool size with Config.PoolSize option.\ntype Client struct {\n\t*baseClient\n\tcmdable\n}\n\n// NewClient returns a client to the Redis Server specified by Options.\nfunc NewClient(opt *Options) *Client {\n\tif opt == nil {\n\t\tpanic(\"redis: NewClient nil options\")\n\t}\n\t// clone to not share options with the caller\n\topt = opt.clone()\n\topt.init()\n\n\t// Push notifications are always enabled for RESP3 (cannot be disabled)\n\n\tc := Client{\n\t\tbaseClient: &baseClient{\n\t\t\topt: opt,\n\t\t},\n\t}\n\tc.init()\n\n\t// Initialize push notification processor using shared helper\n\t// Use void processor for RESP2 connections (push notifications not available)\n\tc.pushProcessor = initializePushProcessor(opt)\n\t// set opt push processor for child clients\n\tc.opt.PushNotificationProcessor = c.pushProcessor\n\n\t// Generate unique pool names for metrics\n\tuniqueID := generateUniqueID()\n\tmainPoolName := opt.Addr + \"_\" + uniqueID\n\tpubsubPoolName := opt.Addr + \"_\" + uniqueID + \"_pubsub\"\n\n\t// Create connection pools\n\tvar err error\n\tc.connPool, err = newConnPool(opt, c.dialHook, mainPoolName)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"redis: failed to create connection pool: %w\", err))\n\t}\n\tc.pubSubPool, err = newPubSubPool(opt, c.dialHook, pubsubPoolName)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"redis: failed to create pubsub pool: %w\", err))\n\t}\n\n\tif opt.StreamingCredentialsProvider != nil {\n\t\tc.streamingCredentialsManager = streaming.NewManager(c.connPool, c.opt.PoolTimeout)\n\t\tc.connPool.AddPoolHook(c.streamingCredentialsManager.PoolHook())\n\t}\n\n\t// Initialize maintnotifications first if enabled and protocol is RESP3\n\tif opt.MaintNotificationsConfig != nil && opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled && opt.Protocol == 3 {\n\t\terr := c.enableMaintNotificationsUpgrades()\n\t\tif err != nil {\n\t\t\tinternal.Logger.Printf(context.Background(), \"failed to initialize maintnotifications: %v\", err)\n\t\t\tif opt.MaintNotificationsConfig.Mode == maintnotifications.ModeEnabled {\n\t\t\t\t/*\n\t\t\t\t\tDesign decision: panic here to fail fast if maintnotifications cannot be enabled when explicitly requested.\n\t\t\t\t\tWe choose to panic instead of returning an error to avoid breaking the existing client API, which does not expect\n\t\t\t\t\tan error from NewClient. This ensures that misconfiguration or critical initialization failures are surfaced\n\t\t\t\t\timmediately, rather than allowing the client to continue in a partially initialized or inconsistent state.\n\t\t\t\t\tClients relying on maintnotifications should be aware that initialization errors will cause a panic, and should\n\t\t\t\t\thandle this accordingly (e.g., via recover or by validating configuration before calling NewClient).\n\t\t\t\t\tThis approach is only used when MaintNotificationsConfig.Mode is MaintNotificationsEnabled, indicating that maintnotifications\n\t\t\t\t\tupgrades are required for correct operation. In other modes, initialization failures are logged but do not panic.\n\t\t\t\t*/\n\t\t\t\tpanic(fmt.Errorf(\"failed to enable maintnotifications: %w\", err))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Register pools with OTel recorder if it supports pool registration\n\t// This allows async gauge metrics to pull stats from pools periodically\n\totel.RegisterPools(c.connPool, c.pubSubPool, opt.Addr)\n\n\treturn &c\n}\n\nfunc (c *Client) init() {\n\tc.cmdable = c.Process\n\tc.initHooks(hooks{\n\t\tdial:       c.baseClient.dial,\n\t\tprocess:    c.baseClient.process,\n\t\tpipeline:   c.baseClient.processPipeline,\n\t\ttxPipeline: c.baseClient.processTxPipeline,\n\t})\n}\n\nfunc (c *Client) WithTimeout(timeout time.Duration) *Client {\n\tclone := *c\n\tclone.baseClient = c.baseClient.withTimeout(timeout)\n\tclone.init()\n\treturn &clone\n}\n\nfunc (c *Client) Conn() *Conn {\n\treturn newConn(c.opt, pool.NewStickyConnPool(c.connPool), &c.hooksMixin)\n}\n\nfunc (c *Client) Process(ctx context.Context, cmd Cmder) error {\n\terr := c.processHook(ctx, cmd)\n\tcmd.SetErr(err)\n\treturn err\n}\n\n// Options returns read-only Options that were used to create the client.\nfunc (c *Client) Options() *Options {\n\treturn c.opt\n}\n\n// NodeAddress returns the address of the Redis node as reported by the server.\n// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation\n// (e.g., loopback replacement). For standalone clients, this defaults to Addr.\n//\n// This is useful for matching the source field in maintenance notifications\n// (e.g. SMIGRATED).\nfunc (c *Client) NodeAddress() string {\n\treturn c.opt.NodeAddress\n}\n\n// GetMaintNotificationsManager returns the maintnotifications manager instance for monitoring and control.\n// Returns nil if maintnotifications are not enabled.\nfunc (c *Client) GetMaintNotificationsManager() *maintnotifications.Manager {\n\tc.maintNotificationsManagerLock.RLock()\n\tdefer c.maintNotificationsManagerLock.RUnlock()\n\treturn c.maintNotificationsManager\n}\n\n// initializePushProcessor initializes the push notification processor for any client type.\n// This is a shared helper to avoid duplication across NewClient, NewFailoverClient, and NewSentinelClient.\nfunc initializePushProcessor(opt *Options) push.NotificationProcessor {\n\t// Always use custom processor if provided\n\tif opt.PushNotificationProcessor != nil {\n\t\treturn opt.PushNotificationProcessor\n\t}\n\n\t// Push notifications are always enabled for RESP3, disabled for RESP2\n\tif opt.Protocol == 3 {\n\t\t// Create default processor for RESP3 connections\n\t\treturn NewPushNotificationProcessor()\n\t}\n\n\t// Create void processor for RESP2 connections (push notifications not available)\n\treturn NewVoidPushNotificationProcessor()\n}\n\n// RegisterPushNotificationHandler registers a handler for a specific push notification name.\n// Returns an error if a handler is already registered for this push notification name.\n// If protected is true, the handler cannot be unregistered.\nfunc (c *Client) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error {\n\treturn c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected)\n}\n\n// GetPushNotificationHandler returns the handler for a specific push notification name.\n// Returns nil if no handler is registered for the given name.\nfunc (c *Client) GetPushNotificationHandler(pushNotificationName string) push.NotificationHandler {\n\treturn c.pushProcessor.GetHandler(pushNotificationName)\n}\n\ntype PoolStats pool.Stats\n\n// PoolStats returns connection pool stats.\nfunc (c *Client) PoolStats() *PoolStats {\n\tstats := c.connPool.Stats()\n\tstats.PubSubStats = *(c.pubSubPool.Stats())\n\treturn (*PoolStats)(stats)\n}\n\nfunc (c *Client) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.Pipeline().Pipelined(ctx, fn)\n}\n\nfunc (c *Client) Pipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: pipelineExecer(c.processPipelineHook),\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc (c *Client) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.TxPipeline().Pipelined(ctx, fn)\n}\n\n// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.\nfunc (c *Client) TxPipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: func(ctx context.Context, cmds []Cmder) error {\n\t\t\tcmds = wrapMultiExec(ctx, cmds)\n\t\t\treturn c.processTxPipelineHook(ctx, cmds)\n\t\t},\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc (c *Client) pubSub() *PubSub {\n\tpubsub := &PubSub{\n\t\topt: c.opt,\n\t\tnewConn: func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) {\n\t\t\tcn, err := c.pubSubPool.NewConn(ctx, c.opt.Network, addr, channels)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// will return nil if already initialized\n\t\t\terr = c.initConn(ctx, cn)\n\t\t\tif err != nil {\n\t\t\t\t_ = cn.Close()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// Track connection in PubSubPool\n\t\t\tc.pubSubPool.TrackConn(cn)\n\t\t\treturn cn, nil\n\t\t},\n\t\tcloseConn: func(cn *pool.Conn) error {\n\t\t\t// Untrack connection from PubSubPool\n\t\t\tc.pubSubPool.UntrackConn(cn)\n\t\t\t_ = cn.Close()\n\t\t\treturn nil\n\t\t},\n\t\tpushProcessor: c.pushProcessor,\n\t}\n\tpubsub.init()\n\n\treturn pubsub\n}\n\n// Subscribe subscribes the client to the specified channels.\n// Channels can be omitted to create empty subscription.\n// Note that this method does not wait on a response from Redis, so the\n// subscription may not be active immediately. To force the connection to wait,\n// you may call the Receive() method on the returned *PubSub like so:\n//\n//\tsub := client.Subscribe(queryResp)\n//\tiface, err := sub.Receive()\n//\tif err != nil {\n//\t    // handle error\n//\t}\n//\n//\t// Should be *Subscription, but others are possible if other actions have been\n//\t// taken on sub since it was created.\n//\tswitch iface.(type) {\n//\tcase *Subscription:\n//\t    // subscribe succeeded\n//\tcase *Message:\n//\t    // received first message\n//\tcase *Pong:\n//\t    // pong received\n//\tdefault:\n//\t    // handle error\n//\t}\n//\n//\tch := sub.Channel()\nfunc (c *Client) Subscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.Subscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\n// PSubscribe subscribes the client to the given patterns.\n// Patterns can be omitted to create empty subscription.\nfunc (c *Client) PSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.PSubscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\n// SSubscribe Subscribes the client to the specified shard channels.\n// Channels can be omitted to create empty subscription.\nfunc (c *Client) SSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.SSubscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\n//------------------------------------------------------------------------------\n\n// Conn represents a single Redis connection rather than a pool of connections.\n// Prefer running commands from Client unless there is a specific need\n// for a continuous single Redis connection.\ntype Conn struct {\n\tbaseClient\n\tcmdable\n\tstatefulCmdable\n}\n\n// newConn is a helper func to create a new Conn instance.\n// the Conn instance is not thread-safe and should not be shared between goroutines.\n// the parentHooks will be cloned, no need to clone before passing it.\nfunc newConn(opt *Options, connPool pool.Pooler, parentHooks *hooksMixin) *Conn {\n\tc := Conn{\n\t\tbaseClient: baseClient{\n\t\t\topt:      opt,\n\t\t\tconnPool: connPool,\n\t\t},\n\t}\n\n\tif parentHooks != nil {\n\t\tc.hooksMixin = parentHooks.clone()\n\t}\n\n\t// Initialize push notification processor using shared helper\n\t// Use void processor for RESP2 connections (push notifications not available)\n\tc.pushProcessor = initializePushProcessor(opt)\n\n\tc.cmdable = c.Process\n\tc.statefulCmdable = c.Process\n\tc.initHooks(hooks{\n\t\tdial:       c.baseClient.dial,\n\t\tprocess:    c.baseClient.process,\n\t\tpipeline:   c.baseClient.processPipeline,\n\t\ttxPipeline: c.baseClient.processTxPipeline,\n\t})\n\n\treturn &c\n}\n\nfunc (c *Conn) Process(ctx context.Context, cmd Cmder) error {\n\terr := c.processHook(ctx, cmd)\n\tcmd.SetErr(err)\n\treturn err\n}\n\n// RegisterPushNotificationHandler registers a handler for a specific push notification name.\n// Returns an error if a handler is already registered for this push notification name.\n// If protected is true, the handler cannot be unregistered.\nfunc (c *Conn) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error {\n\treturn c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected)\n}\n\nfunc (c *Conn) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.Pipeline().Pipelined(ctx, fn)\n}\n\nfunc (c *Conn) Pipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: c.processPipelineHook,\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc (c *Conn) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.TxPipeline().Pipelined(ctx, fn)\n}\n\n// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.\nfunc (c *Conn) TxPipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: func(ctx context.Context, cmds []Cmder) error {\n\t\t\tcmds = wrapMultiExec(ctx, cmds)\n\t\t\treturn c.processTxPipelineHook(ctx, cmds)\n\t\t},\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\n// processPushNotifications processes all pending push notifications on a connection\n// This ensures that cluster topology changes are handled immediately before the connection is used\n// This method should be called by the client before using WithReader for command execution\n//\n// Performance optimization: Skip the expensive MaybeHasData() syscall if a health check\n// was performed recently (within 5 seconds). The health check already verified the connection\n// is healthy and checked for unexpected data (push notifications).\nfunc (c *baseClient) processPushNotifications(ctx context.Context, cn *pool.Conn) error {\n\t// Only process push notifications for RESP3 connections with a processor\n\tif c.opt.Protocol != 3 || c.pushProcessor == nil {\n\t\treturn nil\n\t}\n\n\t// Performance optimization: Skip MaybeHasData() syscall if health check was recent\n\t// If the connection was health-checked within the last 5 seconds, we can skip the\n\t// expensive syscall since the health check already verified no unexpected data.\n\t// This is safe because:\n\t// 0. lastHealthCheckNs is set in pool/conn.go:putConn() after a successful health check\n\t// 1. Health check (connCheck) uses the same syscall (Recvfrom with MSG_PEEK)\n\t// 2. If push notifications arrived, they would have been detected by health check\n\t// 3. 5 seconds is short enough that connection state is still fresh\n\t// 4. Push notifications will be processed by the next WithReader call\n\t// used it is set on getConn, so we should use another timer (lastPutAt?)\n\tlastHealthCheckNs := cn.LastPutAtNs()\n\tif lastHealthCheckNs > 0 {\n\t\t// Use pool's cached time to avoid expensive time.Now() syscall\n\t\tnowNs := pool.GetCachedTimeNs()\n\t\tif nowNs-lastHealthCheckNs < int64(5*time.Second) {\n\t\t\t// Recent health check confirmed no unexpected data, skip the syscall\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Check if there is any data to read before processing\n\t// This is an optimization on UNIX systems where MaybeHasData is a syscall\n\t// On Windows, MaybeHasData always returns true, so this check is a no-op\n\tif !cn.MaybeHasData() {\n\t\treturn nil\n\t}\n\n\t// Use WithReader to access the reader and process push notifications\n\t// This is critical for maintnotifications to work properly\n\t// NOTE: almost no timeouts are set for this read, so it should not block\n\t// longer than necessary, 10us should be plenty of time to read if there are any push notifications\n\t// on the socket.\n\treturn cn.WithReader(ctx, 10*time.Microsecond, func(rd *proto.Reader) error {\n\t\t// Create handler context with client, connection pool, and connection information\n\t\thandlerCtx := c.pushNotificationHandlerContext(cn)\n\t\treturn c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd)\n\t})\n}\n\n// processPendingPushNotificationWithReader processes all pending push notifications on a connection\n// This method should be called by the client in WithReader before reading the reply\nfunc (c *baseClient) processPendingPushNotificationWithReader(ctx context.Context, cn *pool.Conn, rd *proto.Reader) error {\n\t// if we have the reader, we don't need to check for data on the socket, we are waiting\n\t// for either a reply or a push notification, so we can block until we get a reply or reach the timeout\n\tif c.opt.Protocol != 3 || c.pushProcessor == nil {\n\t\treturn nil\n\t}\n\n\t// Create handler context with client, connection pool, and connection information\n\thandlerCtx := c.pushNotificationHandlerContext(cn)\n\treturn c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd)\n}\n\n// pushNotificationHandlerContext creates a handler context for push notification processing\nfunc (c *baseClient) pushNotificationHandlerContext(cn *pool.Conn) push.NotificationHandlerContext {\n\treturn push.NotificationHandlerContext{\n\t\tClient:   c,\n\t\tConnPool: c.connPool,\n\t\tConn:     cn, // Wrap in adapter for easier interface access\n\t}\n}\n"
  },
  {
    "path": "redis_test.go",
    "content": "package redis_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/auth\"\n)\n\ntype redisHookError struct{}\n\nvar _ redis.Hook = redisHookError{}\n\nfunc (redisHookError) DialHook(hook redis.DialHook) redis.DialHook {\n\treturn hook\n}\n\nfunc (redisHookError) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {\n\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\treturn errors.New(\"hook error\")\n\t}\n}\n\nfunc (redisHookError) ProcessPipelineHook(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\treturn hook\n}\n\nfunc TestHookError(t *testing.T) {\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: \":6379\",\n\t})\n\trdb.AddHook(redisHookError{})\n\n\terr := rdb.Ping(ctx).Err()\n\tif err == nil {\n\t\tt.Fatalf(\"got nil, expected an error\")\n\t}\n\n\twanted := \"hook error\"\n\tif err.Error() != wanted {\n\t\tt.Fatalf(`got %q, wanted %q`, err, wanted)\n\t}\n}\n\n//------------------------------------------------------------------------------\n\nvar _ = Describe(\"Client\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tclient.Close()\n\t})\n\n\tIt(\"should Stringer\", func() {\n\t\tExpect(client.String()).To(Equal(fmt.Sprintf(\"Redis<:%s db:0>\", redisPort)))\n\t})\n\n\tIt(\"supports context\", func() {\n\t\tctx, cancel := context.WithCancel(ctx)\n\t\tcancel()\n\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).To(MatchError(\"context canceled\"))\n\t})\n\n\tIt(\"supports WithTimeout\", Label(\"NonRedisEnterprise\"), func() {\n\t\terr := client.ClientPause(ctx, time.Second).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\terr = client.WithTimeout(10 * time.Millisecond).Ping(ctx).Err()\n\t\tExpect(err).To(HaveOccurred())\n\n\t\terr = client.Ping(ctx).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should support PubSub with WithTimeout\", func() {\n\t\t// Create a new client with a custom timeout\n\t\tnewClient := client.WithTimeout(5 * time.Second)\n\n\t\t// Subscribe to a channel using the cloned client\n\t\tpubsub := newClient.Subscribe(ctx, \"test-channel\")\n\t\tdefer pubsub.Close()\n\n\t\t// Verify that we can receive the subscription confirmation\n\t\t_, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Publish a message using the original client\n\t\terr = client.Publish(ctx, \"test-channel\", \"test-message\").Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Receive the message\n\t\tmsg, err := pubsub.ReceiveTimeout(ctx, time.Second)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(msg).NotTo(BeNil())\n\t})\n\n\tIt(\"do\", func() {\n\t\tval, err := client.Do(ctx, \"ping\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"PONG\"))\n\t})\n\n\tIt(\"should ping\", func() {\n\t\tval, err := client.Ping(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"PONG\"))\n\t})\n\n\tIt(\"should return pool stats\", func() {\n\t\tExpect(client.PoolStats()).To(BeAssignableToTypeOf(&redis.PoolStats{}))\n\t})\n\n\tIt(\"should support custom dialers\", func() {\n\t\tcustom := redis.NewClient(&redis.Options{\n\t\t\tNetwork: \"tcp\",\n\t\t\tAddr:    redisAddr,\n\t\t\tDialer: func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t\tvar d net.Dialer\n\t\t\t\treturn d.DialContext(ctx, network, addr)\n\t\t\t},\n\t\t})\n\n\t\tval, err := custom.Ping(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"PONG\"))\n\t\tExpect(custom.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should close\", func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).To(MatchError(\"redis: client is closed\"))\n\t})\n\n\tIt(\"should close pubsub without closing the client\", func() {\n\t\tpubsub := client.Subscribe(ctx)\n\t\tExpect(pubsub.Close()).NotTo(HaveOccurred())\n\n\t\t_, err := pubsub.Receive(ctx)\n\t\tExpect(err).To(MatchError(\"redis: client is closed\"))\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should close Tx without closing the client\", func() {\n\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\treturn err\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should close pubsub when client is closed\", func() {\n\t\tpubsub := client.Subscribe(ctx)\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\n\t\t_, err := pubsub.Receive(ctx)\n\t\tExpect(err).To(MatchError(\"redis: client is closed\"))\n\n\t\tExpect(pubsub.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should select DB\", Label(\"NonRedisEnterprise\"), func() {\n\t\tdb2 := redis.NewClient(&redis.Options{\n\t\t\tAddr: redisAddr,\n\t\t\tDB:   2,\n\t\t})\n\t\tExpect(db2.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t\tExpect(db2.Get(ctx, \"db\").Err()).To(Equal(redis.Nil))\n\t\tExpect(db2.Set(ctx, \"db\", 2, 0).Err()).NotTo(HaveOccurred())\n\n\t\tn, err := db2.Get(ctx, \"db\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(2)))\n\n\t\tExpect(client.Get(ctx, \"db\").Err()).To(Equal(redis.Nil))\n\n\t\tExpect(db2.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t\tExpect(db2.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should client setname\", func() {\n\t\topt := redisOptions()\n\t\topt.ClientName = \"hi\"\n\t\tdb := redis.NewClient(opt)\n\n\t\tdefer func() {\n\t\t\tExpect(db.Close()).NotTo(HaveOccurred())\n\t\t}()\n\n\t\tExpect(db.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t\tval, err := db.ClientList(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).Should(ContainSubstring(\"name=hi\"))\n\t})\n\n\tIt(\"should attempt to set client name in HELLO\", func() {\n\t\topt := redisOptions()\n\t\topt.ClientName = \"hi\"\n\t\tdb := redis.NewClient(opt)\n\n\t\tdefer func() {\n\t\t\tExpect(db.Close()).NotTo(HaveOccurred())\n\t\t}()\n\n\t\t// Client name should be already set on any successfully initialized connection\n\t\tname, err := db.ClientGetName(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(name).Should(Equal(\"hi\"))\n\n\t\t// HELLO should be able to explicitly overwrite the client name\n\t\tconn := db.Conn()\n\t\thello, err := conn.Hello(ctx, 3, \"\", \"\", \"hi2\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(hello[\"proto\"]).Should(Equal(int64(3)))\n\t\tname, err = conn.ClientGetName(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(name).Should(Equal(\"hi2\"))\n\t\terr = conn.Close()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should client PROTO 2\", func() {\n\t\topt := redisOptions()\n\t\topt.Protocol = 2\n\t\tdb := redis.NewClient(opt)\n\n\t\tdefer func() {\n\t\t\tExpect(db.Close()).NotTo(HaveOccurred())\n\t\t}()\n\n\t\tval, err := db.Do(ctx, \"HELLO\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).Should(ContainElements(\"proto\", int64(2)))\n\t})\n\n\tIt(\"should client PROTO 3\", func() {\n\t\topt := redisOptions()\n\t\tdb := redis.NewClient(opt)\n\n\t\tdefer func() {\n\t\t\tExpect(db.Close()).NotTo(HaveOccurred())\n\t\t}()\n\n\t\tval, err := db.Do(ctx, \"HELLO\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).Should(HaveKeyWithValue(\"proto\", int64(3)))\n\t})\n\n\tIt(\"should initialize idle connections created by MinIdleConns\", Label(\"NonRedisEnterprise\"), func() {\n\t\topt := redisOptions()\n\t\tpasswrd := \"asdf\"\n\t\tdb0 := redis.NewClient(opt)\n\t\t// set password\n\t\terr := db0.Do(ctx, \"CONFIG\", \"SET\", \"requirepass\", passwrd).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tdefer func() {\n\t\t\terr = db0.Do(ctx, \"CONFIG\", \"SET\", \"requirepass\", \"\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(db0.Close()).NotTo(HaveOccurred())\n\t\t}()\n\t\topt.MinIdleConns = 5\n\t\topt.Password = passwrd\n\t\topt.DB = 1 // Set DB to require SELECT\n\n\t\tdb := redis.NewClient(opt)\n\t\tdefer func() {\n\t\t\tExpect(db.Close()).NotTo(HaveOccurred())\n\t\t}()\n\n\t\t// Wait for minIdle connections to be created\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Verify that idle connections were created\n\t\tstats := db.PoolStats()\n\t\tExpect(stats.IdleConns).To(BeNumerically(\">=\", 5))\n\n\t\t// Now use these connections - they should be properly initialized\n\t\t// If they're not initialized, we'll get NOAUTH or WRONGDB errors\n\t\tvar wg sync.WaitGroup\n\t\tfor i := 0; i < 10; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func(id int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t// Each goroutine performs multiple operations\n\t\t\t\tfor j := 0; j < 5; j++ {\n\t\t\t\t\tkey := fmt.Sprintf(\"test_key_%d_%d\", id, j)\n\t\t\t\t\terr := db.Set(ctx, key, \"value\", 0).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tval, err := db.Get(ctx, key).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(val).To(Equal(\"value\"))\n\n\t\t\t\t\terr = db.Del(ctx, key).Err()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t}(i)\n\t\t}\n\t\twg.Wait()\n\n\t\t// Verify no errors occurred\n\t\tExpect(db.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"processes custom commands\", func() {\n\t\tcmd := redis.NewCmd(ctx, \"PING\")\n\t\t_ = client.Process(ctx, cmd)\n\n\t\t// Flush buffers.\n\t\tExpect(client.Echo(ctx, \"hello\").Err()).NotTo(HaveOccurred())\n\n\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\tExpect(cmd.Val()).To(Equal(\"PONG\"))\n\t})\n\n\tIt(\"should retry command on network error\", func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\n\t\tclient = redis.NewClient(&redis.Options{\n\t\t\tAddr:       redisAddr,\n\t\t\tMaxRetries: 1,\n\t\t})\n\n\t\t// Put bad connection in the pool.\n\t\tcn, err := client.Pool().Get(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tcn.SetNetConn(&badConn{})\n\t\tclient.Pool().Put(ctx, cn)\n\n\t\terr = client.Ping(ctx).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should retry with backoff\", func() {\n\t\tclientNoRetry := redis.NewClient(&redis.Options{\n\t\t\tAddr:       \":1234\",\n\t\t\tMaxRetries: -1,\n\t\t})\n\t\tdefer clientNoRetry.Close()\n\n\t\tclientRetry := redis.NewClient(&redis.Options{\n\t\t\tAddr:            \":1234\",\n\t\t\tMaxRetries:      5,\n\t\t\tMaxRetryBackoff: 128 * time.Millisecond,\n\t\t})\n\t\tdefer clientRetry.Close()\n\n\t\tstartNoRetry := time.Now()\n\t\terr := clientNoRetry.Ping(ctx).Err()\n\t\tExpect(err).To(HaveOccurred())\n\t\telapseNoRetry := time.Since(startNoRetry)\n\n\t\tstartRetry := time.Now()\n\t\terr = clientRetry.Ping(ctx).Err()\n\t\tExpect(err).To(HaveOccurred())\n\t\telapseRetry := time.Since(startRetry)\n\n\t\tExpect(elapseRetry).To(BeNumerically(\">\", elapseNoRetry, 10*time.Millisecond))\n\t})\n\n\tIt(\"should update conn.UsedAt on read/write\", func() {\n\t\tcn, err := client.Pool().Get(context.Background())\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(cn.UsedAt).NotTo(BeZero())\n\n\t\t// set cn.SetUsedAt(time) or time.Sleep(>1*time.Second)\n\t\t// simulate the last time Conn was used\n\t\t// time.Sleep() is not the standard sleep time\n\t\t// link: https://go-review.googlesource.com/c/go/+/232298\n\t\tcn.SetUsedAt(time.Now().Add(-1 * time.Second))\n\t\tcreatedAt := cn.UsedAt()\n\n\t\tclient.Pool().Put(ctx, cn)\n\t\tExpect(cn.UsedAt().Equal(createdAt)).To(BeTrue())\n\n\t\terr = client.Ping(ctx).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tcn, err = client.Pool().Get(context.Background())\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(cn).NotTo(BeNil())\n\t\tExpect(cn.UsedAt().UnixNano()).To(BeNumerically(\">\", createdAt.UnixNano()))\n\t\tExpect(cn.UsedAt().After(createdAt)).To(BeTrue())\n\t})\n\n\tIt(\"should process command with special chars\", func() {\n\t\tset := client.Set(ctx, \"key\", \"hello1\\r\\nhello2\\r\\n\", 0)\n\t\tExpect(set.Err()).NotTo(HaveOccurred())\n\t\tExpect(set.Val()).To(Equal(\"OK\"))\n\n\t\tget := client.Get(ctx, \"key\")\n\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\tExpect(get.Val()).To(Equal(\"hello1\\r\\nhello2\\r\\n\"))\n\t})\n\n\tIt(\"should handle big vals\", func() {\n\t\tbigVal := bytes.Repeat([]byte{'*'}, 2e6)\n\n\t\terr := client.Set(ctx, \"key\", bigVal, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Reconnect to get new connection.\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\tclient = redis.NewClient(redisOptions())\n\n\t\tgot, err := client.Get(ctx, \"key\").Bytes()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(got).To(Equal(bigVal))\n\t})\n\n\tIt(\"should set and scan time\", func() {\n\t\ttm := time.Now()\n\t\terr := client.Set(ctx, \"now\", tm, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tvar tm2 time.Time\n\t\terr = client.Get(ctx, \"now\").Scan(&tm2)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(tm2).To(BeTemporally(\"==\", tm))\n\t})\n\n\tIt(\"should set and scan durations\", func() {\n\t\tduration := 10 * time.Minute\n\t\terr := client.Set(ctx, \"duration\", duration, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tvar duration2 time.Duration\n\t\terr = client.Get(ctx, \"duration\").Scan(&duration2)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(duration2).To(Equal(duration))\n\t})\n\n\tIt(\"should Conn\", func() {\n\t\terr := client.Conn().Get(ctx, \"this-key-does-not-exist\").Err()\n\t\tExpect(err).To(Equal(redis.Nil))\n\t})\n\n\tIt(\"should set and scan net.IP\", func() {\n\t\tip := net.ParseIP(\"192.168.1.1\")\n\t\terr := client.Set(ctx, \"ip\", ip, 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tvar ip2 net.IP\n\t\terr = client.Get(ctx, \"ip\").Scan(&ip2)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(ip2).To(Equal(ip))\n\t})\n})\n\nvar _ = Describe(\"Client timeout\", func() {\n\tvar opt *redis.Options\n\tvar client *redis.Client\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\ttestTimeout := func() {\n\t\tIt(\"SETINFO timeouts\", func() {\n\t\t\tconn := client.Conn()\n\t\t\terr := conn.Ping(ctx).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"Ping timeouts\", func() {\n\t\t\terr := client.Ping(ctx).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"Pipeline timeouts\", func() {\n\t\t\t_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"Subscribe timeouts\", func() {\n\t\t\tif opt.WriteTimeout == 0 {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpubsub := client.Subscribe(ctx)\n\t\t\tdefer pubsub.Close()\n\n\t\t\terr := pubsub.Subscribe(ctx, \"_\")\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"Tx timeouts\", func() {\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\treturn tx.Ping(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"Tx Pipeline timeouts\", func() {\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Ping(ctx)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn err\n\t\t\t})\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\t}\n\n\tContext(\"read timeout\", func() {\n\t\tBeforeEach(func() {\n\t\t\topt = redisOptions()\n\t\t\topt.ReadTimeout = time.Nanosecond\n\t\t\topt.WriteTimeout = -1\n\t\t\tclient = redis.NewClient(opt)\n\t\t})\n\n\t\ttestTimeout()\n\t})\n\n\tContext(\"write timeout\", func() {\n\t\tBeforeEach(func() {\n\t\t\topt = redisOptions()\n\t\t\topt.ReadTimeout = -1\n\t\t\topt.WriteTimeout = time.Nanosecond\n\t\t\tclient = redis.NewClient(opt)\n\t\t})\n\n\t\ttestTimeout()\n\t})\n})\n\nvar _ = Describe(\"Client OnConnect\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\topt := redisOptions()\n\t\topt.DB = 0\n\t\topt.OnConnect = func(ctx context.Context, cn *redis.Conn) error {\n\t\t\treturn cn.ClientSetName(ctx, \"on_connect\").Err()\n\t\t}\n\n\t\tclient = redis.NewClient(opt)\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"calls OnConnect\", func() {\n\t\tname, err := client.ClientGetName(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(name).To(Equal(\"on_connect\"))\n\t})\n})\n\nvar _ = Describe(\"Client context cancellation\", func() {\n\tvar opt *redis.Options\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\topt = redisOptions()\n\t\topt.ReadTimeout = -1\n\t\topt.WriteTimeout = -1\n\t\tclient = redis.NewClient(opt)\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"Blocking operation cancellation\", func() {\n\t\tctx, cancel := context.WithCancel(ctx)\n\t\tcancel()\n\n\t\terr := client.BLPop(ctx, 1*time.Second, \"test\").Err()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err).To(BeIdenticalTo(context.Canceled))\n\t})\n})\n\nvar _ = Describe(\"Conn\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\terr := client.Close()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"TxPipeline\", Label(\"NonRedisEnterprise\"), func() {\n\t\ttx := client.Conn().TxPipeline()\n\t\ttx.SwapDB(ctx, 0, 2)\n\t\ttx.SwapDB(ctx, 1, 0)\n\t\t_, err := tx.Exec(ctx)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n})\n\nvar _ = Describe(\"Hook\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\terr := client.Close()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"fifo\", func() {\n\t\tvar res []string\n\t\tclient.AddHook(&hook{\n\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\tres = append(res, \"hook-1-process-start\")\n\t\t\t\t\terr := hook(ctx, cmd)\n\t\t\t\t\tres = append(res, \"hook-1-process-end\")\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t},\n\t\t})\n\t\tclient.AddHook(&hook{\n\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\tres = append(res, \"hook-2-process-start\")\n\t\t\t\t\terr := hook(ctx, cmd)\n\t\t\t\t\tres = append(res, \"hook-2-process-end\")\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t},\n\t\t})\n\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(res).To(Equal([]string{\n\t\t\t\"hook-1-process-start\",\n\t\t\t\"hook-2-process-start\",\n\t\t\t\"hook-2-process-end\",\n\t\t\t\"hook-1-process-end\",\n\t\t}))\n\t})\n\n\tIt(\"wrapped error in a hook\", func() {\n\t\tclient.AddHook(&hook{\n\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\tif err := hook(ctx, cmd); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"wrapped error: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t})\n\t\tclient.ScriptFlush(ctx)\n\n\t\tscript := redis.NewScript(`return 'Script and hook'`)\n\n\t\tcmd := script.Run(ctx, client, nil)\n\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\tExpect(cmd.Val()).To(Equal(\"Script and hook\"))\n\t})\n})\n\nvar _ = Describe(\"Hook with MinIdleConns\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\toptions := redisOptions()\n\t\toptions.MinIdleConns = 1\n\t\tclient = redis.NewClient(options)\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\terr := client.Close()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"fifo\", func() {\n\t\tvar res []string\n\t\tclient.AddHook(&hook{\n\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\tres = append(res, \"hook-1-process-start\")\n\t\t\t\t\terr := hook(ctx, cmd)\n\t\t\t\t\tres = append(res, \"hook-1-process-end\")\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t},\n\t\t})\n\t\tclient.AddHook(&hook{\n\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\tres = append(res, \"hook-2-process-start\")\n\t\t\t\t\terr := hook(ctx, cmd)\n\t\t\t\t\tres = append(res, \"hook-2-process-end\")\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t},\n\t\t})\n\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(res).To(Equal([]string{\n\t\t\t\"hook-1-process-start\",\n\t\t\t\"hook-2-process-start\",\n\t\t\t\"hook-2-process-end\",\n\t\t\t\"hook-1-process-end\",\n\t\t}))\n\t})\n})\n\nvar _ = Describe(\"Dialer connection timeouts\", func() {\n\tvar client *redis.Client\n\n\tconst dialSimulatedDelay = 1 * time.Second\n\n\tBeforeEach(func() {\n\t\toptions := redisOptions()\n\t\toptions.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t// Simulated slow dialer.\n\t\t\t// Note that the following sleep is deliberately not context-aware.\n\t\t\ttime.Sleep(dialSimulatedDelay)\n\t\t\treturn net.Dial(\"tcp\", options.Addr)\n\t\t}\n\t\toptions.MinIdleConns = 1\n\t\tclient = redis.NewClient(options)\n\t})\n\n\tAfterEach(func() {\n\t\terr := client.Close()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"does not contend on connection dial for concurrent commands\", func() {\n\t\tvar wg sync.WaitGroup\n\n\t\tconst concurrency = 10\n\n\t\tdurations := make(chan time.Duration, concurrency)\n\t\terrs := make(chan error, concurrency)\n\n\t\tstart := time.Now()\n\t\twg.Add(concurrency)\n\n\t\tfor i := 0; i < concurrency; i++ {\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\tstart := time.Now()\n\t\t\t\terr := client.Ping(ctx).Err()\n\t\t\t\tdurations <- time.Since(start)\n\t\t\t\terrs <- err\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\t\tclose(durations)\n\t\tclose(errs)\n\n\t\t// All commands should eventually succeed, after acquiring a connection.\n\t\tfor err := range errs {\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\n\t\t// Each individual command should complete within the simulated dial duration bound.\n\t\tfor duration := range durations {\n\t\t\tExpect(duration).To(BeNumerically(\"<\", 2*dialSimulatedDelay))\n\t\t}\n\n\t\t// Due to concurrent execution, the entire test suite should also complete within\n\t\t// the same dial duration bound applied for individual commands.\n\t\tExpect(time.Since(start)).To(BeNumerically(\"<\", 2*dialSimulatedDelay))\n\t})\n})\n\nvar _ = Describe(\"Credentials Provider Priority\", func() {\n\tvar client *redis.Client\n\tvar opt *redis.Options\n\tvar recorder *commandRecorder\n\n\tBeforeEach(func() {\n\t\trecorder = newCommandRecorder(10)\n\t})\n\n\tAfterEach(func() {\n\t\tif client != nil {\n\t\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t\t}\n\t})\n\n\tIt(\"should use streaming provider when available\", func() {\n\t\tstreamingCreds := auth.NewBasicCredentials(\"streaming_user\", \"streaming_pass\")\n\t\tctxCreds := auth.NewBasicCredentials(\"ctx_user\", \"ctx_pass\")\n\t\tproviderCreds := auth.NewBasicCredentials(\"provider_user\", \"provider_pass\")\n\n\t\topt = &redis.Options{\n\t\t\tUsername: \"field_user\",\n\t\t\tPassword: \"field_pass\",\n\t\t\tCredentialsProvider: func() (string, string) {\n\t\t\t\tusername, password := providerCreds.BasicAuth()\n\t\t\t\treturn username, password\n\t\t\t},\n\t\t\tCredentialsProviderContext: func(ctx context.Context) (string, string, error) {\n\t\t\t\tusername, password := ctxCreds.BasicAuth()\n\t\t\t\treturn username, password, nil\n\t\t\t},\n\t\t\tStreamingCredentialsProvider: &mockStreamingProvider{\n\t\t\t\tcredentials: streamingCreds,\n\t\t\t\tupdates:     make(chan auth.Credentials, 1),\n\t\t\t},\n\t\t}\n\n\t\tclient = redis.NewClient(opt)\n\t\tclient.AddHook(recorder.Hook())\n\t\t// wrongpass\n\t\tExpect(client.Ping(context.Background()).Err()).To(HaveOccurred())\n\t\tExpect(recorder.Contains(\"AUTH streaming_user\")).To(BeTrue())\n\t})\n\n\tIt(\"should use context provider when streaming provider is not available\", func() {\n\t\tctxCreds := auth.NewBasicCredentials(\"ctx_user\", \"ctx_pass\")\n\t\tproviderCreds := auth.NewBasicCredentials(\"provider_user\", \"provider_pass\")\n\n\t\topt = &redis.Options{\n\t\t\tUsername: \"field_user\",\n\t\t\tPassword: \"field_pass\",\n\t\t\tCredentialsProvider: func() (string, string) {\n\t\t\t\tusername, password := providerCreds.BasicAuth()\n\t\t\t\treturn username, password\n\t\t\t},\n\t\t\tCredentialsProviderContext: func(ctx context.Context) (string, string, error) {\n\t\t\t\tusername, password := ctxCreds.BasicAuth()\n\t\t\t\treturn username, password, nil\n\t\t\t},\n\t\t}\n\n\t\tclient = redis.NewClient(opt)\n\t\tclient.AddHook(recorder.Hook())\n\t\t// wrongpass\n\t\tExpect(client.Ping(context.Background()).Err()).To(HaveOccurred())\n\t\tExpect(recorder.Contains(\"AUTH ctx_user\")).To(BeTrue())\n\t})\n\n\tIt(\"should use regular provider when streaming and context providers are not available\", func() {\n\t\tproviderCreds := auth.NewBasicCredentials(\"provider_user\", \"provider_pass\")\n\n\t\topt = &redis.Options{\n\t\t\tUsername: \"field_user\",\n\t\t\tPassword: \"field_pass\",\n\t\t\tCredentialsProvider: func() (string, string) {\n\t\t\t\tusername, password := providerCreds.BasicAuth()\n\t\t\t\treturn username, password\n\t\t\t},\n\t\t}\n\n\t\tclient = redis.NewClient(opt)\n\t\tclient.AddHook(recorder.Hook())\n\t\t// wrongpass\n\t\tExpect(client.Ping(context.Background()).Err()).To(HaveOccurred())\n\t\tExpect(recorder.Contains(\"AUTH provider_user\")).To(BeTrue())\n\t})\n\n\tIt(\"should use username/password fields when no providers are set\", func() {\n\t\topt = &redis.Options{\n\t\t\tUsername: \"field_user\",\n\t\t\tPassword: \"field_pass\",\n\t\t}\n\n\t\tclient = redis.NewClient(opt)\n\t\tclient.AddHook(recorder.Hook())\n\t\t// wrongpass\n\t\tExpect(client.Ping(context.Background()).Err()).To(HaveOccurred())\n\t\tExpect(recorder.Contains(\"AUTH field_user\")).To(BeTrue())\n\t})\n\n\tIt(\"should use empty credentials when nothing is set\", func() {\n\t\topt = &redis.Options{}\n\n\t\tclient = redis.NewClient(opt)\n\t\tclient.AddHook(recorder.Hook())\n\t\t// no pass, ok\n\t\tExpect(client.Ping(context.Background()).Err()).NotTo(HaveOccurred())\n\t\tExpect(recorder.Contains(\"AUTH\")).To(BeFalse())\n\t})\n\n\tIt(\"should handle credential updates from streaming provider\", func() {\n\t\tinitialCreds := auth.NewBasicCredentials(\"initial_user\", \"initial_pass\")\n\t\tupdatedCreds := auth.NewBasicCredentials(\"updated_user\", \"updated_pass\")\n\t\tupdatesChan := make(chan auth.Credentials, 1)\n\n\t\topt = &redis.Options{\n\t\t\tStreamingCredentialsProvider: &mockStreamingProvider{\n\t\t\t\tcredentials: initialCreds,\n\t\t\t\tupdates:     updatesChan,\n\t\t\t},\n\t\t\tPoolSize: 1, // Force single connection to ensure reauth is tested\n\t\t}\n\n\t\tclient = redis.NewClient(opt)\n\t\tclient.AddHook(recorder.Hook())\n\t\t// wrongpass\n\t\tExpect(client.Ping(context.Background()).Err()).To(HaveOccurred())\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tExpect(recorder.Contains(\"AUTH initial_user\")).To(BeTrue())\n\n\t\t// Update credentials\n\t\topt.StreamingCredentialsProvider.(*mockStreamingProvider).updates <- updatedCreds\n\n\t\t// Wait for reauth to complete and verify updated credentials are used\n\t\t// We need to keep trying Ping until we see the updated AUTH command\n\t\t// because the reauth happens asynchronously\n\t\tEventually(func() bool {\n\t\t\t// wrongpass\n\t\t\t_ = client.Ping(context.Background()).Err()\n\t\t\treturn recorder.Contains(\"AUTH updated_user\")\n\t\t}, \"1s\", \"50ms\").Should(BeTrue())\n\n\t\tclose(updatesChan)\n\t})\n})\n\ntype mockStreamingProvider struct {\n\tmu          sync.RWMutex\n\tcredentials auth.Credentials\n\terr         error\n\tupdates     chan auth.Credentials\n}\n\nfunc (m *mockStreamingProvider) Subscribe(listener auth.CredentialsListener) (auth.Credentials, auth.UnsubscribeFunc, error) {\n\tif m.err != nil {\n\t\treturn nil, nil, m.err\n\t}\n\n\tif listener == nil {\n\t\treturn nil, nil, errors.New(\"listener cannot be nil\")\n\t}\n\n\t// Create a done channel to stop the goroutine\n\tdone := make(chan struct{})\n\n\t// Start goroutine to handle updates\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\t// this is just a mock:\n\t\t\t\t// allow panics to be caught without crashing\n\t\t\t}\n\t\t}()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\treturn\n\t\t\tcase creds, ok := <-m.updates:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tm.mu.Lock()\n\t\t\t\tm.credentials = creds\n\t\t\t\tm.mu.Unlock()\n\t\t\t\tlistener.OnNext(creds)\n\t\t\t}\n\t\t}\n\t}()\n\n\tm.mu.RLock()\n\tcurrentCreds := m.credentials\n\tm.mu.RUnlock()\n\n\treturn currentCreds, func() (err error) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\t// this is just a mock:\n\t\t\t\t// allow multiple closes from multiple listeners\n\t\t\t}\n\t\t}()\n\t\tclose(done)\n\t\treturn\n\t}, nil\n}\n\nvar _ = Describe(\"Client creation\", func() {\n\tContext(\"simple client with nil options\", func() {\n\t\tIt(\"panics\", func() {\n\t\t\tExpect(func() {\n\t\t\t\tredis.NewClient(nil)\n\t\t\t}).To(Panic())\n\t\t})\n\t})\n\tContext(\"cluster client with nil options\", func() {\n\t\tIt(\"panics\", func() {\n\t\t\tExpect(func() {\n\t\t\t\tredis.NewClusterClient(nil)\n\t\t\t}).To(Panic())\n\t\t})\n\t})\n\tContext(\"ring client with nil options\", func() {\n\t\tIt(\"panics\", func() {\n\t\t\tExpect(func() {\n\t\t\t\tredis.NewRing(nil)\n\t\t\t}).To(Panic())\n\t\t})\n\t})\n\tContext(\"universal client with nil options\", func() {\n\t\tIt(\"panics\", func() {\n\t\t\tExpect(func() {\n\t\t\t\tredis.NewUniversalClient(nil)\n\t\t\t}).To(Panic())\n\t\t})\n\t})\n\tContext(\"failover client with nil options\", func() {\n\t\tIt(\"panics\", func() {\n\t\t\tExpect(func() {\n\t\t\t\tredis.NewFailoverClient(nil)\n\t\t\t}).To(Panic())\n\t\t})\n\t})\n\tContext(\"failover cluster client with nil options\", func() {\n\t\tIt(\"panics\", func() {\n\t\t\tExpect(func() {\n\t\t\t\tredis.NewFailoverClusterClient(nil)\n\t\t\t}).To(Panic())\n\t\t})\n\t})\n\tContext(\"sentinel client with nil options\", func() {\n\t\tIt(\"panics\", func() {\n\t\t\tExpect(func() {\n\t\t\t\tredis.NewSentinelClient(nil)\n\t\t\t}).To(Panic())\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "result.go",
    "content": "package redis\n\nimport \"time\"\n\n// NewCmdResult returns a Cmd initialised with val and err for testing.\nfunc NewCmdResult(val interface{}, err error) *Cmd {\n\tvar cmd Cmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewSliceResult returns a SliceCmd initialised with val and err for testing.\nfunc NewSliceResult(val []interface{}, err error) *SliceCmd {\n\tvar cmd SliceCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewStatusResult returns a StatusCmd initialised with val and err for testing.\nfunc NewStatusResult(val string, err error) *StatusCmd {\n\tvar cmd StatusCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewIntResult returns an IntCmd initialised with val and err for testing.\nfunc NewIntResult(val int64, err error) *IntCmd {\n\tvar cmd IntCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewDurationResult returns a DurationCmd initialised with val and err for testing.\nfunc NewDurationResult(val time.Duration, err error) *DurationCmd {\n\tvar cmd DurationCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewBoolResult returns a BoolCmd initialised with val and err for testing.\nfunc NewBoolResult(val bool, err error) *BoolCmd {\n\tvar cmd BoolCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewStringResult returns a StringCmd initialised with val and err for testing.\nfunc NewStringResult(val string, err error) *StringCmd {\n\tvar cmd StringCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewFloatResult returns a FloatCmd initialised with val and err for testing.\nfunc NewFloatResult(val float64, err error) *FloatCmd {\n\tvar cmd FloatCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewStringSliceResult returns a StringSliceCmd initialised with val and err for testing.\nfunc NewStringSliceResult(val []string, err error) *StringSliceCmd {\n\tvar cmd StringSliceCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewBoolSliceResult returns a BoolSliceCmd initialised with val and err for testing.\nfunc NewBoolSliceResult(val []bool, err error) *BoolSliceCmd {\n\tvar cmd BoolSliceCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewFloatSliceResult returns a FloatSliceCmd initialised with val and err for testing.\nfunc NewFloatSliceResult(val []float64, err error) *FloatSliceCmd {\n\tvar cmd FloatSliceCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewMapStringStringResult returns a MapStringStringCmd initialised with val and err for testing.\nfunc NewMapStringStringResult(val map[string]string, err error) *MapStringStringCmd {\n\tvar cmd MapStringStringCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewMapStringIntCmdResult returns a MapStringIntCmd initialised with val and err for testing.\nfunc NewMapStringIntCmdResult(val map[string]int64, err error) *MapStringIntCmd {\n\tvar cmd MapStringIntCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewTimeCmdResult returns a TimeCmd initialised with val and err for testing.\nfunc NewTimeCmdResult(val time.Time, err error) *TimeCmd {\n\tvar cmd TimeCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewZSliceCmdResult returns a ZSliceCmd initialised with val and err for testing.\nfunc NewZSliceCmdResult(val []Z, err error) *ZSliceCmd {\n\tvar cmd ZSliceCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewZWithKeyCmdResult returns a ZWithKeyCmd initialised with val and err for testing.\nfunc NewZWithKeyCmdResult(val *ZWithKey, err error) *ZWithKeyCmd {\n\tvar cmd ZWithKeyCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewScanCmdResult returns a ScanCmd initialised with val and err for testing.\nfunc NewScanCmdResult(keys []string, cursor uint64, err error) *ScanCmd {\n\tvar cmd ScanCmd\n\tcmd.page = keys\n\tcmd.cursor = cursor\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewClusterSlotsCmdResult returns a ClusterSlotsCmd initialised with val and err for testing.\nfunc NewClusterSlotsCmdResult(val []ClusterSlot, err error) *ClusterSlotsCmd {\n\tvar cmd ClusterSlotsCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewGeoLocationCmdResult returns a GeoLocationCmd initialised with val and err for testing.\nfunc NewGeoLocationCmdResult(val []GeoLocation, err error) *GeoLocationCmd {\n\tvar cmd GeoLocationCmd\n\tcmd.locations = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewGeoPosCmdResult returns a GeoPosCmd initialised with val and err for testing.\nfunc NewGeoPosCmdResult(val []*GeoPos, err error) *GeoPosCmd {\n\tvar cmd GeoPosCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewCommandsInfoCmdResult returns a CommandsInfoCmd initialised with val and err for testing.\nfunc NewCommandsInfoCmdResult(val map[string]*CommandInfo, err error) *CommandsInfoCmd {\n\tvar cmd CommandsInfoCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewXMessageSliceCmdResult returns a XMessageSliceCmd initialised with val and err for testing.\nfunc NewXMessageSliceCmdResult(val []XMessage, err error) *XMessageSliceCmd {\n\tvar cmd XMessageSliceCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewXStreamSliceCmdResult returns a XStreamSliceCmd initialised with val and err for testing.\nfunc NewXStreamSliceCmdResult(val []XStream, err error) *XStreamSliceCmd {\n\tvar cmd XStreamSliceCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n\n// NewXPendingResult returns a XPendingCmd initialised with val and err for testing.\nfunc NewXPendingResult(val *XPending, err error) *XPendingCmd {\n\tvar cmd XPendingCmd\n\tcmd.val = val\n\tcmd.SetErr(err)\n\treturn &cmd\n}\n"
  },
  {
    "path": "ring.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/cespare/xxhash/v2\"\n\t\"github.com/dgryski/go-rendezvous\" //nolint\n\t\"github.com/redis/go-redis/v9/auth\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/rand\"\n)\n\nvar errRingShardsDown = errors.New(\"redis: all ring shards are down\")\n\n// defaultHeartbeatFn is the default function used to check the shard liveness\nvar defaultHeartbeatFn = func(ctx context.Context, client *Client) bool {\n\terr := client.Ping(ctx).Err()\n\treturn err == nil || err == pool.ErrPoolTimeout\n}\n\n//------------------------------------------------------------------------------\n\ntype ConsistentHash interface {\n\tGet(string) string\n}\n\ntype rendezvousWrapper struct {\n\t*rendezvous.Rendezvous\n}\n\nfunc (w rendezvousWrapper) Get(key string) string {\n\treturn w.Lookup(key)\n}\n\nfunc newRendezvous(shards []string) ConsistentHash {\n\treturn rendezvousWrapper{rendezvous.New(shards, xxhash.Sum64String)}\n}\n\n//------------------------------------------------------------------------------\n\n// RingOptions are used to configure a ring client and should be\n// passed to NewRing.\ntype RingOptions struct {\n\t// Map of name => host:port addresses of ring shards.\n\tAddrs map[string]string\n\n\t// NewClient creates a shard client with provided options.\n\tNewClient func(opt *Options) *Client\n\n\t// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.\n\tClientName string\n\n\t// Frequency of executing HeartbeatFn to check shards availability.\n\t// Shard is considered down after 3 subsequent failed checks.\n\tHeartbeatFrequency time.Duration\n\n\t// A function used to check the shard liveness\n\t// if not set, defaults to defaultHeartbeatFn\n\tHeartbeatFn func(ctx context.Context, client *Client) bool\n\n\t// NewConsistentHash returns a consistent hash that is used\n\t// to distribute keys across the shards.\n\t//\n\t// See https://medium.com/@dgryski/consistent-hashing-algorithmic-tradeoffs-ef6b8e2fcae8\n\t// for consistent hashing algorithmic tradeoffs.\n\tNewConsistentHash func(shards []string) ConsistentHash\n\n\t// Following options are copied from Options struct.\n\n\tDialer    func(ctx context.Context, network, addr string) (net.Conn, error)\n\tOnConnect func(ctx context.Context, cn *Conn) error\n\n\tProtocol int\n\tUsername string\n\tPassword string\n\t// CredentialsProvider allows the username and password to be updated\n\t// before reconnecting. It should return the current username and password.\n\tCredentialsProvider func() (username string, password string)\n\n\t// CredentialsProviderContext is an enhanced parameter of CredentialsProvider,\n\t// done to maintain API compatibility. In the future,\n\t// there might be a merge between CredentialsProviderContext and CredentialsProvider.\n\t// There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider.\n\tCredentialsProviderContext func(ctx context.Context) (username string, password string, err error)\n\n\t// StreamingCredentialsProvider is used to retrieve the credentials\n\t// for the connection from an external source. Those credentials may change\n\t// during the connection lifetime. This is useful for managed identity\n\t// scenarios where the credentials are retrieved from an external source.\n\t//\n\t// Currently, this is a placeholder for the future implementation.\n\tStreamingCredentialsProvider auth.StreamingCredentialsProvider\n\tDB                           int\n\n\tMaxRetries      int\n\tMinRetryBackoff time.Duration\n\tMaxRetryBackoff time.Duration\n\n\tDialTimeout time.Duration\n\n\t// DialerRetries is the maximum number of retry attempts when dialing fails.\n\t//\n\t// default: 5\n\tDialerRetries int\n\n\t// DialerRetryTimeout is the backoff duration between retry attempts.\n\t//\n\t// default: 100 milliseconds\n\tDialerRetryTimeout time.Duration\n\n\t// DialerRetryBackoff controls the delay between dial retry attempts.\n\t// See Options.DialerRetryBackoff for details.\n\tDialerRetryBackoff func(attempt int) time.Duration\n\n\tReadTimeout           time.Duration\n\tWriteTimeout          time.Duration\n\tContextTimeoutEnabled bool\n\n\t// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).\n\tPoolFIFO bool\n\n\tPoolSize              int\n\tPoolTimeout           time.Duration\n\tMinIdleConns          int\n\tMaxIdleConns          int\n\tMaxActiveConns        int\n\tConnMaxIdleTime       time.Duration\n\tConnMaxLifetime       time.Duration\n\tConnMaxLifetimeJitter time.Duration\n\n\t// ReadBufferSize is the size of the bufio.Reader buffer for each connection.\n\t// Larger buffers can improve performance for commands that return large responses.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tReadBufferSize int\n\n\t// WriteBufferSize is the size of the bufio.Writer buffer for each connection.\n\t// Larger buffers can improve performance for large pipelines and commands with many arguments.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tWriteBufferSize int\n\n\tTLSConfig *tls.Config\n\tLimiter   Limiter\n\n\t// DisableIndentity - Disable set-lib on connect.\n\t//\n\t// default: false\n\t//\n\t// Deprecated: Use DisableIdentity instead.\n\tDisableIndentity bool\n\n\t// DisableIdentity is used to disable CLIENT SETINFO command on connect.\n\t//\n\t// default: false\n\tDisableIdentity bool\n\tIdentitySuffix  string\n\tUnstableResp3   bool\n}\n\nfunc (opt *RingOptions) init() {\n\tif opt.NewClient == nil {\n\t\topt.NewClient = func(opt *Options) *Client {\n\t\t\treturn NewClient(opt)\n\t\t}\n\t}\n\n\tif opt.HeartbeatFrequency == 0 {\n\t\topt.HeartbeatFrequency = 500 * time.Millisecond\n\t}\n\n\tif opt.HeartbeatFn == nil {\n\t\topt.HeartbeatFn = defaultHeartbeatFn\n\t}\n\n\tif opt.NewConsistentHash == nil {\n\t\topt.NewConsistentHash = newRendezvous\n\t}\n\n\tswitch opt.MaxRetries {\n\tcase -1:\n\t\topt.MaxRetries = 0\n\tcase 0:\n\t\topt.MaxRetries = 3\n\t}\n\tswitch opt.MinRetryBackoff {\n\tcase -1:\n\t\topt.MinRetryBackoff = 0\n\tcase 0:\n\t\topt.MinRetryBackoff = 8 * time.Millisecond\n\t}\n\tswitch opt.MaxRetryBackoff {\n\tcase -1:\n\t\topt.MaxRetryBackoff = 0\n\tcase 0:\n\t\topt.MaxRetryBackoff = 512 * time.Millisecond\n\t}\n\n\tif opt.ReadBufferSize == 0 {\n\t\topt.ReadBufferSize = proto.DefaultBufferSize\n\t}\n\tif opt.WriteBufferSize == 0 {\n\t\topt.WriteBufferSize = proto.DefaultBufferSize\n\t}\n}\n\nfunc (opt *RingOptions) clientOptions() *Options {\n\treturn &Options{\n\t\tClientName: opt.ClientName,\n\t\tDialer:     opt.Dialer,\n\t\tOnConnect:  opt.OnConnect,\n\n\t\tProtocol:                     opt.Protocol,\n\t\tUsername:                     opt.Username,\n\t\tPassword:                     opt.Password,\n\t\tCredentialsProvider:          opt.CredentialsProvider,\n\t\tCredentialsProviderContext:   opt.CredentialsProviderContext,\n\t\tStreamingCredentialsProvider: opt.StreamingCredentialsProvider,\n\t\tDB:                           opt.DB,\n\n\t\tMaxRetries: -1,\n\n\t\tDialTimeout:           opt.DialTimeout,\n\t\tDialerRetries:         opt.DialerRetries,\n\t\tDialerRetryTimeout:    opt.DialerRetryTimeout,\n\t\tDialerRetryBackoff:    opt.DialerRetryBackoff,\n\t\tReadTimeout:           opt.ReadTimeout,\n\t\tWriteTimeout:          opt.WriteTimeout,\n\t\tContextTimeoutEnabled: opt.ContextTimeoutEnabled,\n\n\t\tPoolFIFO:              opt.PoolFIFO,\n\t\tPoolSize:              opt.PoolSize,\n\t\tPoolTimeout:           opt.PoolTimeout,\n\t\tMinIdleConns:          opt.MinIdleConns,\n\t\tMaxIdleConns:          opt.MaxIdleConns,\n\t\tMaxActiveConns:        opt.MaxActiveConns,\n\t\tConnMaxIdleTime:       opt.ConnMaxIdleTime,\n\t\tConnMaxLifetime:       opt.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,\n\t\tReadBufferSize:        opt.ReadBufferSize,\n\t\tWriteBufferSize:       opt.WriteBufferSize,\n\n\t\tTLSConfig: opt.TLSConfig,\n\t\tLimiter:   opt.Limiter,\n\n\t\tDisableIdentity:  opt.DisableIdentity,\n\t\tDisableIndentity: opt.DisableIndentity,\n\n\t\tIdentitySuffix: opt.IdentitySuffix,\n\t\tUnstableResp3:  opt.UnstableResp3,\n\t}\n}\n\n//------------------------------------------------------------------------------\n\ntype ringShard struct {\n\tClient *Client\n\tdown   int32\n\taddr   string\n}\n\nfunc newRingShard(opt *RingOptions, addr string) *ringShard {\n\tclopt := opt.clientOptions()\n\tclopt.Addr = addr\n\n\treturn &ringShard{\n\t\tClient: opt.NewClient(clopt),\n\t\taddr:   addr,\n\t}\n}\n\nfunc (shard *ringShard) String() string {\n\tvar state string\n\tif shard.IsUp() {\n\t\tstate = \"up\"\n\t} else {\n\t\tstate = \"down\"\n\t}\n\treturn fmt.Sprintf(\"%s is %s\", shard.Client, state)\n}\n\nfunc (shard *ringShard) IsDown() bool {\n\tconst threshold = 3\n\treturn atomic.LoadInt32(&shard.down) >= threshold\n}\n\nfunc (shard *ringShard) IsUp() bool {\n\treturn !shard.IsDown()\n}\n\n// Vote votes to set shard state and returns true if state was changed.\nfunc (shard *ringShard) Vote(up bool) bool {\n\tif up {\n\t\tchanged := shard.IsDown()\n\t\tatomic.StoreInt32(&shard.down, 0)\n\t\treturn changed\n\t}\n\n\tif shard.IsDown() {\n\t\treturn false\n\t}\n\n\tatomic.AddInt32(&shard.down, 1)\n\treturn shard.IsDown()\n}\n\n//------------------------------------------------------------------------------\n\ntype ringSharding struct {\n\topt *RingOptions\n\n\tmu        sync.RWMutex\n\tshards    *ringShards\n\tclosed    bool\n\thash      ConsistentHash\n\tnumShard  int\n\tonNewNode []func(rdb *Client)\n\n\t// ensures exclusive access to SetAddrs so there is no need\n\t// to hold mu for the duration of potentially long shard creation\n\tsetAddrsMu sync.Mutex\n}\n\ntype ringShards struct {\n\tm    map[string]*ringShard\n\tlist []*ringShard\n}\n\nfunc newRingSharding(opt *RingOptions) *ringSharding {\n\tc := &ringSharding{\n\t\topt: opt,\n\t}\n\tc.SetAddrs(opt.Addrs)\n\n\treturn c\n}\n\nfunc (c *ringSharding) OnNewNode(fn func(rdb *Client)) {\n\tc.mu.Lock()\n\tc.onNewNode = append(c.onNewNode, fn)\n\tc.mu.Unlock()\n}\n\n// SetAddrs replaces the shards in use, such that you can increase and\n// decrease number of shards, that you use. It will reuse shards that\n// existed before and close the ones that will not be used anymore.\nfunc (c *ringSharding) SetAddrs(addrs map[string]string) {\n\tc.setAddrsMu.Lock()\n\tdefer c.setAddrsMu.Unlock()\n\n\tcleanup := func(shards map[string]*ringShard) {\n\t\tfor addr, shard := range shards {\n\t\t\tif err := shard.Client.Close(); err != nil {\n\t\t\t\tinternal.Logger.Printf(context.Background(), \"shard.Close %s failed: %s\", addr, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tc.mu.RLock()\n\tif c.closed {\n\t\tc.mu.RUnlock()\n\t\treturn\n\t}\n\texisting := c.shards\n\tc.mu.RUnlock()\n\n\tshards, created, unused := c.newRingShards(addrs, existing)\n\n\tc.mu.Lock()\n\tif c.closed {\n\t\tcleanup(created)\n\t\tc.mu.Unlock()\n\t\treturn\n\t}\n\tc.shards = shards\n\tc.rebalanceLocked()\n\tc.mu.Unlock()\n\n\tcleanup(unused)\n}\n\nfunc (c *ringSharding) newRingShards(\n\taddrs map[string]string, existing *ringShards,\n) (shards *ringShards, created, unused map[string]*ringShard) {\n\tshards = &ringShards{m: make(map[string]*ringShard, len(addrs))}\n\tcreated = make(map[string]*ringShard) // indexed by addr\n\tunused = make(map[string]*ringShard)  // indexed by addr\n\n\tif existing != nil {\n\t\tfor _, shard := range existing.list {\n\t\t\tunused[shard.addr] = shard\n\t\t}\n\t}\n\n\tfor name, addr := range addrs {\n\t\tif shard, ok := unused[addr]; ok {\n\t\t\tshards.m[name] = shard\n\t\t\tdelete(unused, addr)\n\t\t} else {\n\t\t\tshard := newRingShard(c.opt, addr)\n\t\t\tshards.m[name] = shard\n\t\t\tcreated[addr] = shard\n\n\t\t\tfor _, fn := range c.onNewNode {\n\t\t\t\tfn(shard.Client)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, shard := range shards.m {\n\t\tshards.list = append(shards.list, shard)\n\t}\n\n\treturn\n}\n\n// Warning: External exposure of `c.shards.list` may cause data races.\n// So keep internal or implement deep copy if exposed.\nfunc (c *ringSharding) List() []*ringShard {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\tif c.closed {\n\t\treturn nil\n\t}\n\treturn c.shards.list\n}\n\nfunc (c *ringSharding) Hash(key string) string {\n\tkey = hashtag.Key(key)\n\n\tvar hash string\n\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\tif c.numShard > 0 {\n\t\thash = c.hash.Get(key)\n\t}\n\n\treturn hash\n}\n\nfunc (c *ringSharding) GetByKey(key string) (*ringShard, error) {\n\tkey = hashtag.Key(key)\n\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\tif c.closed {\n\t\treturn nil, pool.ErrClosed\n\t}\n\n\tif c.numShard == 0 {\n\t\treturn nil, errRingShardsDown\n\t}\n\n\tshardName := c.hash.Get(key)\n\tif shardName == \"\" {\n\t\treturn nil, errRingShardsDown\n\t}\n\treturn c.shards.m[shardName], nil\n}\n\nfunc (c *ringSharding) GetByName(shardName string) (*ringShard, error) {\n\tif shardName == \"\" {\n\t\treturn c.Random()\n\t}\n\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\tshard, ok := c.shards.m[shardName]\n\tif !ok {\n\t\treturn nil, errors.New(\"redis: the shard is not in the ring\")\n\t}\n\n\treturn shard, nil\n}\n\nfunc (c *ringSharding) Random() (*ringShard, error) {\n\treturn c.GetByKey(strconv.Itoa(rand.Int()))\n}\n\n// Heartbeat monitors state of each shard in the ring.\nfunc (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) {\n\tticker := time.NewTicker(frequency)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tvar rebalance bool\n\n\t\t\t// note: `c.List()` return a shadow copy of `[]*ringShard`.\n\t\t\tfor _, shard := range c.List() {\n\t\t\t\tisUp := c.opt.HeartbeatFn(ctx, shard.Client)\n\t\t\t\tif shard.Vote(isUp) {\n\t\t\t\t\tinternal.Logger.Printf(ctx, \"ring shard state changed: %s\", shard)\n\t\t\t\t\trebalance = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif rebalance {\n\t\t\t\tc.mu.Lock()\n\t\t\t\tc.rebalanceLocked()\n\t\t\t\tc.mu.Unlock()\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// rebalanceLocked removes dead shards from the Ring.\n// Requires c.mu locked.\nfunc (c *ringSharding) rebalanceLocked() {\n\tif c.closed {\n\t\treturn\n\t}\n\tif c.shards == nil {\n\t\treturn\n\t}\n\n\tliveShards := make([]string, 0, len(c.shards.m))\n\n\tfor name, shard := range c.shards.m {\n\t\tif shard.IsUp() {\n\t\t\tliveShards = append(liveShards, name)\n\t\t}\n\t}\n\n\tc.hash = c.opt.NewConsistentHash(liveShards)\n\tc.numShard = len(liveShards)\n}\n\nfunc (c *ringSharding) Len() int {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\treturn c.numShard\n}\n\nfunc (c *ringSharding) Close() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.closed {\n\t\treturn nil\n\t}\n\tc.closed = true\n\n\tvar firstErr error\n\n\tfor _, shard := range c.shards.list {\n\t\tif err := shard.Client.Close(); err != nil && firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\n\tc.hash = nil\n\tc.shards = nil\n\tc.numShard = 0\n\n\treturn firstErr\n}\n\n//------------------------------------------------------------------------------\n\n// Ring is a Redis client that uses consistent hashing to distribute\n// keys across multiple Redis servers (shards). It's safe for\n// concurrent use by multiple goroutines.\n//\n// Ring monitors the state of each shard and removes dead shards from\n// the ring. When a shard comes online it is added back to the ring. This\n// gives you maximum availability and partition tolerance, but no\n// consistency between different shards or even clients. Each client\n// uses shards that are available to the client and does not do any\n// coordination when shard state is changed.\n//\n// Ring should be used when you need multiple Redis servers for caching\n// and can tolerate losing data when one of the servers dies.\n// Otherwise you should use Redis Cluster.\ntype Ring struct {\n\tcmdable\n\thooksMixin\n\n\topt               *RingOptions\n\tsharding          *ringSharding\n\tcmdsInfoCache     *cmdsInfoCache\n\theartbeatCancelFn context.CancelFunc\n}\n\nfunc NewRing(opt *RingOptions) *Ring {\n\tif opt == nil {\n\t\tpanic(\"redis: NewRing nil options\")\n\t}\n\topt.init()\n\n\thbCtx, hbCancel := context.WithCancel(context.Background())\n\n\tring := Ring{\n\t\topt:               opt,\n\t\tsharding:          newRingSharding(opt),\n\t\theartbeatCancelFn: hbCancel,\n\t}\n\n\tring.cmdsInfoCache = newCmdsInfoCache(ring.cmdsInfo)\n\tring.cmdable = ring.Process\n\n\tring.initHooks(hooks{\n\t\tprocess: ring.process,\n\t\tpipeline: func(ctx context.Context, cmds []Cmder) error {\n\t\t\treturn ring.generalProcessPipeline(ctx, cmds, false)\n\t\t},\n\t\ttxPipeline: func(ctx context.Context, cmds []Cmder) error {\n\t\t\treturn ring.generalProcessPipeline(ctx, cmds, true)\n\t\t},\n\t})\n\n\tgo ring.sharding.Heartbeat(hbCtx, opt.HeartbeatFrequency)\n\n\treturn &ring\n}\n\nfunc (c *Ring) SetAddrs(addrs map[string]string) {\n\tc.sharding.SetAddrs(addrs)\n}\n\nfunc (c *Ring) Process(ctx context.Context, cmd Cmder) error {\n\terr := c.processHook(ctx, cmd)\n\tcmd.SetErr(err)\n\treturn err\n}\n\n// Options returns read-only Options that were used to create the client.\nfunc (c *Ring) Options() *RingOptions {\n\treturn c.opt\n}\n\nfunc (c *Ring) retryBackoff(attempt int) time.Duration {\n\treturn internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)\n}\n\n// PoolStats returns accumulated connection pool stats.\nfunc (c *Ring) PoolStats() *PoolStats {\n\t// note: `c.List()` return a shadow copy of `[]*ringShard`.\n\tshards := c.sharding.List()\n\tvar acc PoolStats\n\tfor _, shard := range shards {\n\t\ts := shard.Client.connPool.Stats()\n\t\tacc.Hits += s.Hits\n\t\tacc.Misses += s.Misses\n\t\tacc.Timeouts += s.Timeouts\n\t\tacc.TotalConns += s.TotalConns\n\t\tacc.IdleConns += s.IdleConns\n\t}\n\treturn &acc\n}\n\n// Len returns the current number of shards in the ring.\nfunc (c *Ring) Len() int {\n\treturn c.sharding.Len()\n}\n\n// Subscribe subscribes the client to the specified channels.\nfunc (c *Ring) Subscribe(ctx context.Context, channels ...string) *PubSub {\n\tif len(channels) == 0 {\n\t\tpanic(\"at least one channel is required\")\n\t}\n\n\tshard, err := c.sharding.GetByKey(channels[0])\n\tif err != nil {\n\t\t// TODO: return PubSub with sticky error\n\t\tpanic(err)\n\t}\n\treturn shard.Client.Subscribe(ctx, channels...)\n}\n\n// PSubscribe subscribes the client to the given patterns.\nfunc (c *Ring) PSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tif len(channels) == 0 {\n\t\tpanic(\"at least one channel is required\")\n\t}\n\n\tshard, err := c.sharding.GetByKey(channels[0])\n\tif err != nil {\n\t\t// TODO: return PubSub with sticky error\n\t\tpanic(err)\n\t}\n\treturn shard.Client.PSubscribe(ctx, channels...)\n}\n\n// SSubscribe Subscribes the client to the specified shard channels.\nfunc (c *Ring) SSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tif len(channels) == 0 {\n\t\tpanic(\"at least one channel is required\")\n\t}\n\tshard, err := c.sharding.GetByKey(channels[0])\n\tif err != nil {\n\t\t// TODO: return PubSub with sticky error\n\t\tpanic(err)\n\t}\n\treturn shard.Client.SSubscribe(ctx, channels...)\n}\n\nfunc (c *Ring) OnNewNode(fn func(rdb *Client)) {\n\tc.sharding.OnNewNode(fn)\n}\n\n// ForEachShard concurrently calls the fn on each live shard in the ring.\n// It returns the first error if any.\nfunc (c *Ring) ForEachShard(\n\tctx context.Context,\n\tfn func(ctx context.Context, client *Client) error,\n) error {\n\t// note: `c.List()` return a shadow copy of `[]*ringShard`.\n\tshards := c.sharding.List()\n\tvar wg sync.WaitGroup\n\terrCh := make(chan error, 1)\n\tfor _, shard := range shards {\n\t\tif shard.IsDown() {\n\t\t\tcontinue\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo func(shard *ringShard) {\n\t\t\tdefer wg.Done()\n\t\t\terr := fn(ctx, shard.Client)\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase errCh <- err:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t}(shard)\n\t}\n\twg.Wait()\n\n\tselect {\n\tcase err := <-errCh:\n\t\treturn err\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) {\n\t// note: `c.List()` return a shadow copy of `[]*ringShard`.\n\tshards := c.sharding.List()\n\tvar firstErr error\n\tfor _, shard := range shards {\n\t\tcmdsInfo, err := shard.Client.Command(ctx).Result()\n\t\tif err == nil {\n\t\t\treturn cmdsInfo, nil\n\t\t}\n\t\tif firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t}\n\tif firstErr == nil {\n\t\treturn nil, errRingShardsDown\n\t}\n\treturn nil, firstErr\n}\n\nfunc (c *Ring) cmdShard(cmd Cmder) (*ringShard, error) {\n\tpos := cmdFirstKeyPos(cmd)\n\tif pos == 0 {\n\t\treturn c.sharding.Random()\n\t}\n\tfirstKey := cmd.stringArg(pos)\n\treturn c.sharding.GetByKey(firstKey)\n}\n\nfunc (c *Ring) process(ctx context.Context, cmd Cmder) error {\n\tvar lastErr error\n\tfor attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {\n\t\tif attempt > 0 {\n\t\t\tif err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tshard, err := c.cmdShard(cmd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlastErr = shard.Client.Process(ctx, cmd)\n\t\tif lastErr == nil || !shouldRetry(lastErr, cmd.readTimeout() == nil) {\n\t\t\treturn lastErr\n\t\t}\n\t}\n\treturn lastErr\n}\n\nfunc (c *Ring) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.Pipeline().Pipelined(ctx, fn)\n}\n\nfunc (c *Ring) Pipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: pipelineExecer(c.processPipelineHook),\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc (c *Ring) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.TxPipeline().Pipelined(ctx, fn)\n}\n\nfunc (c *Ring) TxPipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: func(ctx context.Context, cmds []Cmder) error {\n\t\t\tcmds = wrapMultiExec(ctx, cmds)\n\t\t\treturn c.processTxPipelineHook(ctx, cmds)\n\t\t},\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc (c *Ring) generalProcessPipeline(\n\tctx context.Context, cmds []Cmder, tx bool,\n) error {\n\tif tx {\n\t\t// Trim multi .. exec.\n\t\tcmds = cmds[1 : len(cmds)-1]\n\t}\n\n\tcmdsMap := make(map[string][]Cmder)\n\n\tfor _, cmd := range cmds {\n\t\thash := cmd.stringArg(cmdFirstKeyPos(cmd))\n\t\tif hash != \"\" {\n\t\t\thash = c.sharding.Hash(hash)\n\t\t}\n\t\tcmdsMap[hash] = append(cmdsMap[hash], cmd)\n\t}\n\n\tvar wg sync.WaitGroup\n\terrs := make(chan error, len(cmdsMap))\n\n\tfor hash, cmds := range cmdsMap {\n\t\twg.Add(1)\n\t\tgo func(hash string, cmds []Cmder) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// TODO: retry?\n\t\t\tshard, err := c.sharding.GetByName(hash)\n\t\t\tif err != nil {\n\t\t\t\tsetCmdsErr(cmds, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thook := shard.Client.processPipelineHook\n\t\t\tif tx {\n\t\t\t\tcmds = wrapMultiExec(ctx, cmds)\n\t\t\t\thook = shard.Client.processTxPipelineHook\n\t\t\t}\n\n\t\t\tif err = hook(ctx, cmds); err != nil {\n\t\t\t\terrs <- err\n\t\t\t}\n\t\t}(hash, cmds)\n\t}\n\n\twg.Wait()\n\tclose(errs)\n\n\tif err := <-errs; err != nil {\n\t\treturn err\n\t}\n\treturn cmdsFirstErr(cmds)\n}\n\nfunc (c *Ring) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {\n\tif len(keys) == 0 {\n\t\treturn fmt.Errorf(\"redis: Watch requires at least one key\")\n\t}\n\n\tvar shards []*ringShard\n\n\tfor _, key := range keys {\n\t\tif key != \"\" {\n\t\t\tshard, err := c.sharding.GetByKey(key)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tshards = append(shards, shard)\n\t\t}\n\t}\n\n\tif len(shards) == 0 {\n\t\treturn fmt.Errorf(\"redis: Watch requires at least one shard\")\n\t}\n\n\tif len(shards) > 1 {\n\t\tfor _, shard := range shards[1:] {\n\t\t\tif shard.Client != shards[0].Client {\n\t\t\t\terr := fmt.Errorf(\"redis: Watch requires all keys to be in the same shard\")\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn shards[0].Client.Watch(ctx, fn, keys...)\n}\n\n// Close closes the ring client, releasing any open resources.\n//\n// It is rare to Close a Ring, as the Ring is meant to be long-lived\n// and shared between many goroutines.\nfunc (c *Ring) Close() error {\n\tc.heartbeatCancelFn()\n\n\treturn c.sharding.Close()\n}\n\n// GetShardClients returns a list of all shard clients in the ring.\n// This can be used to create dedicated connections (e.g., PubSub) for each shard.\nfunc (c *Ring) GetShardClients() []*Client {\n\tshards := c.sharding.List()\n\tclients := make([]*Client, 0, len(shards))\n\tfor _, shard := range shards {\n\t\tif shard.IsUp() {\n\t\t\tclients = append(clients, shard.Client)\n\t\t}\n\t}\n\treturn clients\n}\n\n// GetShardClientForKey returns the shard client that would handle the given key.\n// This can be used to determine which shard a particular key/channel would be routed to.\nfunc (c *Ring) GetShardClientForKey(key string) (*Client, error) {\n\tshard, err := c.sharding.GetByKey(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn shard.Client, nil\n}\n"
  },
  {
    "path": "ring_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"Redis Ring PROTO 2\", func() {\n\tconst heartbeat = 100 * time.Millisecond\n\n\tvar ring *redis.Ring\n\n\tBeforeEach(func() {\n\t\topt := redisRingOptions()\n\t\topt.Protocol = 2\n\t\topt.HeartbeatFrequency = heartbeat\n\t\tring = redis.NewRing(opt)\n\n\t\terr := ring.ForEachShard(ctx, func(ctx context.Context, cl *redis.Client) error {\n\t\t\treturn cl.FlushDB(ctx).Err()\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(ring.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should ring PROTO 2\", func() {\n\t\t_ = ring.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\tval, err := c.Do(ctx, \"HELLO\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).Should(ContainElements(\"proto\", int64(2)))\n\t\t\treturn nil\n\t\t})\n\t})\n})\n\nvar _ = Describe(\"Redis Ring\", func() {\n\tconst heartbeat = 100 * time.Millisecond\n\n\tvar ring *redis.Ring\n\n\tsetRingKeys := func() {\n\t\tfor i := 0; i < 100; i++ {\n\t\t\terr := ring.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\t}\n\n\tBeforeEach(func() {\n\t\topt := redisRingOptions()\n\t\topt.ClientName = \"ring_hi\"\n\t\topt.HeartbeatFrequency = heartbeat\n\t\tring = redis.NewRing(opt)\n\n\t\terr := ring.ForEachShard(ctx, func(ctx context.Context, cl *redis.Client) error {\n\t\t\treturn cl.FlushDB(ctx).Err()\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(ring.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"do\", func() {\n\t\tval, err := ring.Do(ctx, \"ping\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"PONG\"))\n\t})\n\n\tIt(\"supports context\", func() {\n\t\tctx, cancel := context.WithCancel(ctx)\n\t\tcancel()\n\n\t\terr := ring.Ping(ctx).Err()\n\t\tExpect(err).To(MatchError(\"context canceled\"))\n\t})\n\n\tIt(\"should ring client setname\", func() {\n\t\terr := ring.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\treturn c.Ping(ctx).Err()\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t_ = ring.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\tval, err := c.ClientList(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).Should(ContainSubstring(\"name=ring_hi\"))\n\t\t\treturn nil\n\t\t})\n\t})\n\n\tIt(\"should ring PROTO 3\", func() {\n\t\t_ = ring.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\tval, err := c.Do(ctx, \"HELLO\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).Should(HaveKeyWithValue(\"proto\", int64(3)))\n\t\t\treturn nil\n\t\t})\n\t})\n\n\tIt(\"distributes keys\", func() {\n\t\tsetRingKeys()\n\n\t\t// Both shards should have some keys now.\n\t\tExpect(ringShard1.Info(ctx, \"keyspace\").Val()).To(ContainSubstring(\"keys=56\"))\n\t\tExpect(ringShard2.Info(ctx, \"keyspace\").Val()).To(ContainSubstring(\"keys=44\"))\n\t})\n\n\tIt(\"distributes keys when using EVAL\", func() {\n\t\tscript := redis.NewScript(`\n\t\t\tlocal r = redis.call('SET', KEYS[1], ARGV[1])\n\t\t\treturn r\n\t\t`)\n\n\t\tvar key string\n\t\tfor i := 0; i < 100; i++ {\n\t\t\tkey = fmt.Sprintf(\"key%d\", i)\n\t\t\terr := script.Run(ctx, ring, []string{key}, \"value\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\n\t\tExpect(ringShard1.Info(ctx, \"keyspace\").Val()).To(ContainSubstring(\"keys=56\"))\n\t\tExpect(ringShard2.Info(ctx, \"keyspace\").Val()).To(ContainSubstring(\"keys=44\"))\n\t})\n\n\tIt(\"supports hash tags\", func() {\n\t\tfor i := 0; i < 100; i++ {\n\t\t\terr := ring.Set(ctx, fmt.Sprintf(\"key%d{tag}\", i), \"value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\n\t\tExpect(ringShard1.Info(ctx, \"keyspace\").Val()).ToNot(ContainSubstring(\"keys=\"))\n\t\tExpect(ringShard2.Info(ctx, \"keyspace\").Val()).To(ContainSubstring(\"keys=100\"))\n\t})\n\n\tDescribe(\"[new] dynamic setting ring shards\", func() {\n\t\tIt(\"downscale shard and check reuse shard, upscale shard and check reuse\", func() {\n\t\t\tExpect(ring.Len(), 2)\n\n\t\t\twantShard := ring.ShardByName(\"ringShardOne\")\n\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\t\"ringShardOne\": \":\" + ringShard1Port,\n\t\t\t})\n\t\t\tExpect(ring.Len(), 1)\n\t\t\tgotShard := ring.ShardByName(\"ringShardOne\")\n\t\t\tExpect(gotShard).To(BeIdenticalTo(wantShard))\n\n\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\t\"ringShardOne\": \":\" + ringShard1Port,\n\t\t\t\t\"ringShardTwo\": \":\" + ringShard2Port,\n\t\t\t})\n\t\t\tExpect(ring.Len(), 2)\n\t\t\tgotShard = ring.ShardByName(\"ringShardOne\")\n\t\t\tExpect(gotShard).To(BeIdenticalTo(wantShard))\n\t\t})\n\n\t\tIt(\"uses 3 shards after setting it to 3 shards\", func() {\n\t\t\tExpect(ring.Len(), 2)\n\n\t\t\tshardName1 := \"ringShardOne\"\n\t\t\tshardAddr1 := \":\" + ringShard1Port\n\t\t\twantShard1 := ring.ShardByName(shardName1)\n\t\t\tshardName2 := \"ringShardTwo\"\n\t\t\tshardAddr2 := \":\" + ringShard2Port\n\t\t\twantShard2 := ring.ShardByName(shardName2)\n\t\t\tshardName3 := \"ringShardThree\"\n\t\t\tshardAddr3 := \":\" + ringShard3Port\n\n\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\tshardName1: shardAddr1,\n\t\t\t\tshardName2: shardAddr2,\n\t\t\t\tshardName3: shardAddr3,\n\t\t\t})\n\t\t\tExpect(ring.Len(), 3)\n\t\t\tgotShard1 := ring.ShardByName(shardName1)\n\t\t\tgotShard2 := ring.ShardByName(shardName2)\n\t\t\tgotShard3 := ring.ShardByName(shardName3)\n\t\t\tExpect(gotShard1).To(BeIdenticalTo(wantShard1))\n\t\t\tExpect(gotShard2).To(BeIdenticalTo(wantShard2))\n\t\t\tExpect(gotShard3).ToNot(BeNil())\n\n\t\t\tring.SetAddrs(map[string]string{\n\t\t\t\tshardName1: shardAddr1,\n\t\t\t\tshardName2: shardAddr2,\n\t\t\t})\n\t\t\tExpect(ring.Len(), 2)\n\t\t\tgotShard1 = ring.ShardByName(shardName1)\n\t\t\tgotShard2 = ring.ShardByName(shardName2)\n\t\t\tgotShard3 = ring.ShardByName(shardName3)\n\t\t\tExpect(gotShard1).To(BeIdenticalTo(wantShard1))\n\t\t\tExpect(gotShard2).To(BeIdenticalTo(wantShard2))\n\t\t\tExpect(gotShard3).To(BeNil())\n\t\t})\n\t})\n\tDescribe(\"pipeline\", func() {\n\t\tIt(\"doesn't panic closed ring, returns error\", func() {\n\t\t\tpipe := ring.Pipeline()\n\t\t\tfor i := 0; i < 3; i++ {\n\t\t\t\terr := pipe.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tExpect(ring.Close()).NotTo(HaveOccurred())\n\n\t\t\tExpect(func() {\n\t\t\t\t_, execErr := pipe.Exec(ctx)\n\t\t\t\tExpect(execErr).To(HaveOccurred())\n\t\t\t}).NotTo(Panic())\n\t\t})\n\n\t\tIt(\"distributes keys\", func() {\n\t\t\tpipe := ring.Pipeline()\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\terr := pipe.Set(ctx, fmt.Sprintf(\"key%d\", i), \"value\", 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t\tcmds, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cmds).To(HaveLen(100))\n\n\t\t\tfor _, cmd := range cmds {\n\t\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmd.(*redis.StatusCmd).Val()).To(Equal(\"OK\"))\n\t\t\t}\n\n\t\t\t// Both shards should have some keys now.\n\t\t\tExpect(ringShard1.Info(ctx).Val()).To(ContainSubstring(\"keys=56\"))\n\t\t\tExpect(ringShard2.Info(ctx).Val()).To(ContainSubstring(\"keys=44\"))\n\t\t})\n\n\t\tIt(\"is consistent with ring\", func() {\n\t\t\tvar keys []string\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tkey := make([]byte, 64)\n\t\t\t\t_, err := rand.Read(key)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tkeys = append(keys, string(key))\n\t\t\t}\n\n\t\t\t_, err := ring.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tpipe.Set(ctx, key, \"value\", 0).Err()\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tfor _, key := range keys {\n\t\t\t\tval, err := ring.Get(ctx, key).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(Equal(\"value\"))\n\t\t\t}\n\t\t})\n\n\t\tIt(\"supports hash tags\", func() {\n\t\t\t_, err := ring.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\tpipe.Set(ctx, fmt.Sprintf(\"key%d{tag}\", i), \"value\", 0).Err()\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(ringShard1.Info(ctx).Val()).ToNot(ContainSubstring(\"keys=\"))\n\t\t\tExpect(ringShard2.Info(ctx).Val()).To(ContainSubstring(\"keys=100\"))\n\t\t})\n\n\t\tIt(\"return dial timeout error\", func() {\n\t\t\topt := redisRingOptions()\n\t\t\topt.DialTimeout = 250 * time.Millisecond\n\t\t\topt.Addrs = map[string]string{\"ringShardNotExist\": \":1997\"}\n\t\t\tring = redis.NewRing(opt)\n\n\t\t\t_, err := ring.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.HSet(ctx, \"key\", \"value\")\n\t\t\t\tpipe.Expire(ctx, \"key\", time.Minute)\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\t})\n\n\tDescribe(\"new client callback\", func() {\n\t\tIt(\"can be initialized with a new client callback\", func() {\n\t\t\topts := redisRingOptions()\n\t\t\topts.NewClient = func(opt *redis.Options) *redis.Client {\n\t\t\t\topt.Username = \"username1\"\n\t\t\t\topt.Password = \"password1\"\n\t\t\t\treturn redis.NewClient(opt)\n\t\t\t}\n\t\t\tring = redis.NewRing(opts)\n\n\t\t\terr := ring.Ping(ctx).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.Error()).To(ContainSubstring(\"WRONGPASS\"))\n\t\t})\n\t})\n\n\tDescribe(\"Process hook\", func() {\n\t\tBeforeEach(func() {\n\t\t\t// the health check leads to data race for variable \"stack []string\".\n\t\t\t// here, the health check time is set to 72 hours to avoid health check\n\t\t\topt := redisRingOptions()\n\t\t\topt.HeartbeatFrequency = 72 * time.Hour\n\t\t\tring = redis.NewRing(opt)\n\t\t})\n\t\tIt(\"supports Process hook\", func() {\n\t\t\terr := ring.Set(ctx, \"key\", \"test\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvar stack []string\n\n\t\t\tring.AddHook(&hook{\n\t\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"get key: \"))\n\t\t\t\t\t\tstack = append(stack, \"ring.BeforeProcess\")\n\n\t\t\t\t\t\terr := hook(ctx, cmd)\n\n\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"get key: test\"))\n\t\t\t\t\t\tstack = append(stack, \"ring.AfterProcess\")\n\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tring.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {\n\t\t\t\tshard.AddHook(&hook{\n\t\t\t\t\tprocessHook: func(hook redis.ProcessHook) redis.ProcessHook {\n\t\t\t\t\t\treturn func(ctx context.Context, cmd redis.Cmder) error {\n\t\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"get key: \"))\n\t\t\t\t\t\t\tstack = append(stack, \"shard.BeforeProcess\")\n\n\t\t\t\t\t\t\terr := hook(ctx, cmd)\n\n\t\t\t\t\t\t\tExpect(cmd.String()).To(Equal(\"get key: test\"))\n\t\t\t\t\t\t\tstack = append(stack, \"shard.AfterProcess\")\n\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\terr = ring.Get(ctx, \"key\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(stack).To(Equal([]string{\n\t\t\t\t\"ring.BeforeProcess\",\n\t\t\t\t\"shard.BeforeProcess\",\n\t\t\t\t\"shard.AfterProcess\",\n\t\t\t\t\"ring.AfterProcess\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"supports Pipeline hook\", func() {\n\t\t\terr := ring.Ping(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvar stack []string\n\n\t\t\tring.AddHook(&hook{\n\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\t// skip the connection initialization\n\t\t\t\t\t\tif cmds[0].Name() == \"hello\" || cmds[0].Name() == \"client\" {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">\", 0))\n\t\t\t\t\t\tExpect(cmds[0].String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\tstack = append(stack, \"ring.BeforeProcessPipeline\")\n\n\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">\", 0))\n\t\t\t\t\t\tExpect(cmds[0].String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\tstack = append(stack, \"ring.AfterProcessPipeline\")\n\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tring.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {\n\t\t\t\tshard.AddHook(&hook{\n\t\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\t\t// skip the connection initialization\n\t\t\t\t\t\t\tif cmds[0].Name() == \"hello\" || cmds[0].Name() == \"client\" {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">\", 0))\n\t\t\t\t\t\t\tExpect(cmds[0].String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\t\tstack = append(stack, \"shard.BeforeProcessPipeline\")\n\n\t\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">\", 0))\n\t\t\t\t\t\t\tExpect(cmds[0].String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\t\tstack = append(stack, \"shard.AfterProcessPipeline\")\n\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\t_, err = ring.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(stack).To(Equal([]string{\n\t\t\t\t\"ring.BeforeProcessPipeline\",\n\t\t\t\t\"shard.BeforeProcessPipeline\",\n\t\t\t\t\"shard.AfterProcessPipeline\",\n\t\t\t\t\"ring.AfterProcessPipeline\",\n\t\t\t}))\n\t\t})\n\n\t\tIt(\"supports TxPipeline hook\", func() {\n\t\t\terr := ring.Ping(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tvar stack []string\n\n\t\t\tring.AddHook(&hook{\n\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\tdefer GinkgoRecover()\n\t\t\t\t\t\t// skip the connection initialization\n\t\t\t\t\t\tif cmds[0].Name() == \"hello\" || cmds[0].Name() == \"client\" {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">=\", 3))\n\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\tstack = append(stack, \"ring.BeforeProcessPipeline\")\n\n\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">=\", 3))\n\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\tstack = append(stack, \"ring.AfterProcessPipeline\")\n\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tring.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {\n\t\t\t\tshard.AddHook(&hook{\n\t\t\t\t\tprocessPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {\n\t\t\t\t\t\treturn func(ctx context.Context, cmds []redis.Cmder) error {\n\t\t\t\t\t\t\tdefer GinkgoRecover()\n\t\t\t\t\t\t\t// skip the connection initialization\n\t\t\t\t\t\t\tif cmds[0].Name() == \"hello\" || cmds[0].Name() == \"client\" {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">=\", 3))\n\t\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: \"))\n\t\t\t\t\t\t\tstack = append(stack, \"shard.BeforeProcessPipeline\")\n\n\t\t\t\t\t\t\terr := hook(ctx, cmds)\n\n\t\t\t\t\t\t\tExpect(len(cmds)).To(BeNumerically(\">=\", 3))\n\t\t\t\t\t\t\tExpect(cmds[1].String()).To(Equal(\"ping: PONG\"))\n\t\t\t\t\t\t\tstack = append(stack, \"shard.AfterProcessPipeline\")\n\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\t_, err = ring.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(stack).To(Equal([]string{\n\t\t\t\t\"ring.BeforeProcessPipeline\",\n\t\t\t\t\"shard.BeforeProcessPipeline\",\n\t\t\t\t\"shard.AfterProcessPipeline\",\n\t\t\t\t\"ring.AfterProcessPipeline\",\n\t\t\t}))\n\t\t})\n\t})\n})\n\nvar _ = Describe(\"empty Redis Ring\", func() {\n\tvar ring *redis.Ring\n\n\tBeforeEach(func() {\n\t\tring = redis.NewRing(&redis.RingOptions{})\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(ring.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"returns an error\", func() {\n\t\terr := ring.Ping(ctx).Err()\n\t\tExpect(err).To(MatchError(\"redis: all ring shards are down\"))\n\t})\n\n\tIt(\"pipeline returns an error\", func() {\n\t\t_, err := ring.Pipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\tpipe.Ping(ctx)\n\t\t\treturn nil\n\t\t})\n\t\tExpect(err).To(MatchError(\"redis: all ring shards are down\"))\n\t})\n})\n\nvar _ = Describe(\"Ring watch\", func() {\n\tconst heartbeat = 100 * time.Millisecond\n\n\tvar ring *redis.Ring\n\n\tBeforeEach(func() {\n\t\topt := redisRingOptions()\n\t\topt.HeartbeatFrequency = heartbeat\n\t\tring = redis.NewRing(opt)\n\n\t\terr := ring.ForEachShard(ctx, func(ctx context.Context, cl *redis.Client) error {\n\t\t\treturn cl.FlushDB(ctx).Err()\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(ring.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should Watch\", func() {\n\t\tvar incr func(string) error\n\n\t\t// Transactionally increments key using GET and SET commands.\n\t\tincr = func(key string) error {\n\t\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\tn, err := tx.Get(ctx, key).Int64()\n\t\t\t\tif err != nil && err != redis.Nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Set(ctx, key, strconv.FormatInt(n+1, 10), 0)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn err\n\t\t\t}, key)\n\t\t\tif err == redis.TxFailedErr {\n\t\t\t\treturn incr(key)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tvar wg sync.WaitGroup\n\t\tfor i := 0; i < 100; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\terr := incr(\"key\")\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\tn, err := ring.Get(ctx, \"key\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(100)))\n\t})\n\n\tIt(\"should discard\", func() {\n\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\tcmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Set(ctx, \"{shard}key1\", \"hello1\", 0)\n\t\t\t\tpipe.Discard()\n\t\t\t\tpipe.Set(ctx, \"{shard}key2\", \"hello2\", 0)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\treturn err\n\t\t}, \"{shard}key1\", \"{shard}key2\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tget := ring.Get(ctx, \"{shard}key1\")\n\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\tExpect(get.Val()).To(Equal(\"\"))\n\n\t\tget = ring.Get(ctx, \"{shard}key2\")\n\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\tExpect(get.Val()).To(Equal(\"hello2\"))\n\t})\n\n\tIt(\"returns no error when there are no commands\", func() {\n\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t_, err := tx.TxPipelined(ctx, func(redis.Pipeliner) error { return nil })\n\t\t\treturn err\n\t\t}, \"key\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tv, err := ring.Ping(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(v).To(Equal(\"PONG\"))\n\t})\n\n\tIt(\"should exec bulks\", func() {\n\t\tconst N = 20000\n\n\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\tcmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\t\tpipe.Incr(ctx, \"key\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(cmds)).To(Equal(N))\n\t\t\tfor _, cmd := range cmds {\n\t\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\t\t\treturn err\n\t\t}, \"key\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tnum, err := ring.Get(ctx, \"key\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(num).To(Equal(int64(N)))\n\t})\n\n\tIt(\"should Watch/Unwatch\", func() {\n\t\tvar C, N int\n\n\t\terr := ring.Set(ctx, \"key\", \"0\", 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tperform(C, func(id int) {\n\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t\tval, err := tx.Get(ctx, \"key\").Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(val).NotTo(Equal(redis.Nil))\n\n\t\t\t\t\tnum, err := strconv.ParseInt(val, 10, 64)\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t\tcmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\t\tpipe.Set(ctx, \"key\", strconv.FormatInt(num+1, 10), 0)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\t\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\t\treturn err\n\t\t\t\t}, \"key\")\n\t\t\t\tif err == redis.TxFailedErr {\n\t\t\t\t\ti--\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\t\t})\n\n\t\tval, err := ring.Get(ctx, \"key\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(int64(C * N)))\n\t})\n\n\tIt(\"should close Tx without closing the client\", func() {\n\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Ping(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\treturn err\n\t\t}, \"key\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(ring.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"respects max size on multi\", func() {\n\t\t//this test checks the number of \"pool.conn\"\n\t\t//if the health check is performed at the same time\n\t\t//conn will be used, resulting in an abnormal number of \"pool.conn\".\n\t\t//\n\t\t//redis.NewRing() does not have an option to prohibit health checks.\n\t\t//set a relatively large time here to avoid health checks.\n\t\topt := redisRingOptions()\n\t\topt.HeartbeatFrequency = 72 * time.Hour\n\t\tring = redis.NewRing(opt)\n\n\t\tperform(1000, func(id int) {\n\t\t\tvar ping *redis.StatusCmd\n\n\t\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\tcmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tping = pipe.Ping(ctx)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\t\treturn err\n\t\t\t}, \"key\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(ping.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(ping.Val()).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tring.ForEachShard(ctx, func(ctx context.Context, cl *redis.Client) error {\n\t\t\tdefer GinkgoRecover()\n\n\t\t\tpool := cl.Pool()\n\t\t\tExpect(pool.Len()).To(BeNumerically(\"<=\", 10))\n\t\t\tExpect(pool.IdleLen()).To(BeNumerically(\"<=\", 10))\n\t\t\tExpect(pool.Len()).To(Equal(pool.IdleLen()))\n\n\t\t\treturn nil\n\t\t})\n\t})\n})\n\nvar _ = Describe(\"Ring Tx timeout\", func() {\n\tconst heartbeat = 100 * time.Millisecond\n\n\tvar ring *redis.Ring\n\n\tAfterEach(func() {\n\t\t_ = ring.Close()\n\t})\n\n\ttestTimeout := func() {\n\t\tIt(\"Tx timeouts\", func() {\n\t\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\treturn tx.Ping(ctx).Err()\n\t\t\t}, \"foo\")\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\n\t\tIt(\"Tx Pipeline timeouts\", func() {\n\t\t\terr := ring.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Ping(ctx)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn err\n\t\t\t}, \"foo\")\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(err.(net.Error).Timeout()).To(BeTrue())\n\t\t})\n\t}\n\n\tconst pause = 5 * time.Second\n\n\tContext(\"read/write timeout\", func() {\n\t\tBeforeEach(func() {\n\t\t\topt := redisRingOptions()\n\t\t\topt.ReadTimeout = 250 * time.Millisecond\n\t\t\topt.WriteTimeout = 250 * time.Millisecond\n\t\t\topt.HeartbeatFrequency = heartbeat\n\t\t\tring = redis.NewRing(opt)\n\n\t\t\terr := ring.ForEachShard(ctx, func(ctx context.Context, client *redis.Client) error {\n\t\t\t\treturn client.ClientPause(ctx, pause).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\n\t\tAfterEach(func() {\n\t\t\t_ = ring.ForEachShard(ctx, func(ctx context.Context, client *redis.Client) error {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t\t}, 2*pause).ShouldNot(HaveOccurred())\n\t\t\t\treturn nil\n\t\t\t})\n\t\t})\n\n\t\ttestTimeout()\n\t})\n})\n\nvar _ = Describe(\"Ring GetShardClients and GetShardClientForKey\", func() {\n\tvar ring *redis.Ring\n\n\tBeforeEach(func() {\n\t\tring = redis.NewRing(&redis.RingOptions{\n\t\t\tAddrs: map[string]string{\n\t\t\t\t\"shard1\": \":6379\",\n\t\t\t\t\"shard2\": \":6380\",\n\t\t\t},\n\t\t})\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(ring.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"GetShardClients returns active shard clients\", func() {\n\t\tshards := ring.GetShardClients()\n\t\t// Note: This test will pass even if Redis servers are not running,\n\t\t// because GetShardClients only returns clients that are marked as \"up\",\n\t\t// and newly created shards start as \"up\" until the first health check fails.\n\n\t\tif len(shards) == 0 {\n\t\t\t// Expected if Redis servers are not running\n\t\t\tSkip(\"No active shards found (Redis servers not running)\")\n\t\t} else {\n\t\t\tExpect(len(shards)).To(BeNumerically(\">\", 0))\n\t\t\tfor _, client := range shards {\n\t\t\t\tExpect(client).NotTo(BeNil())\n\t\t\t}\n\t\t}\n\t})\n\n\tIt(\"GetShardClientForKey returns correct shard for keys\", func() {\n\t\ttestKeys := []string{\"key1\", \"key2\", \"user:123\", \"channel:test\"}\n\n\t\tfor _, key := range testKeys {\n\t\t\tclient, err := ring.GetShardClientForKey(key)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(client).NotTo(BeNil())\n\t\t}\n\t})\n\n\tIt(\"GetShardClientForKey is consistent for same key\", func() {\n\t\tkey := \"test:consistency\"\n\n\t\t// Call GetShardClientForKey multiple times with the same key\n\t\t// Should always return the same shard\n\t\tvar firstClient *redis.Client\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tclient, err := ring.GetShardClientForKey(key)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(client).NotTo(BeNil())\n\n\t\t\tif i == 0 {\n\t\t\t\tfirstClient = client\n\t\t\t} else {\n\t\t\t\tExpect(client.String()).To(Equal(firstClient.String()))\n\t\t\t}\n\t\t}\n\t})\n\n\tIt(\"GetShardClientForKey distributes keys across shards\", func() {\n\t\ttestKeys := []string{\"key1\", \"key2\", \"key3\", \"key4\", \"key5\"}\n\t\tshardMap := make(map[string]int)\n\n\t\tfor _, key := range testKeys {\n\t\t\tclient, err := ring.GetShardClientForKey(key)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tshardMap[client.String()]++\n\t\t}\n\n\t\t// Should have at least 1 shard (could be all keys go to same shard due to hashing)\n\t\tExpect(len(shardMap)).To(BeNumerically(\">=\", 1))\n\t\t// But with multiple keys, we expect some distribution\n\t\tExpect(len(shardMap)).To(BeNumerically(\"<=\", 2)) // At most 2 shards (our setup)\n\t})\n})\n"
  },
  {
    "path": "script.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n)\n\ntype Scripter interface {\n\tEval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd\n\tEvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd\n\tEvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd\n\tEvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd\n\tScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd\n\tScriptLoad(ctx context.Context, script string) *StringCmd\n}\n\nvar (\n\t_ Scripter = (*Client)(nil)\n\t_ Scripter = (*Ring)(nil)\n\t_ Scripter = (*ClusterClient)(nil)\n)\n\ntype Script struct {\n\tsrc, hash string\n}\n\nfunc NewScript(src string) *Script {\n\th := sha1.New()\n\t_, _ = io.WriteString(h, src)\n\treturn &Script{\n\t\tsrc:  src,\n\t\thash: hex.EncodeToString(h.Sum(nil)),\n\t}\n}\n\nfunc (s *Script) Hash() string {\n\treturn s.hash\n}\n\nfunc (s *Script) Load(ctx context.Context, c Scripter) *StringCmd {\n\treturn c.ScriptLoad(ctx, s.src)\n}\n\nfunc (s *Script) Exists(ctx context.Context, c Scripter) *BoolSliceCmd {\n\treturn c.ScriptExists(ctx, s.hash)\n}\n\nfunc (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {\n\treturn c.Eval(ctx, s.src, keys, args...)\n}\n\nfunc (s *Script) EvalRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {\n\treturn c.EvalRO(ctx, s.src, keys, args...)\n}\n\nfunc (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {\n\treturn c.EvalSha(ctx, s.hash, keys, args...)\n}\n\nfunc (s *Script) EvalShaRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {\n\treturn c.EvalShaRO(ctx, s.hash, keys, args...)\n}\n\n// Run optimistically uses EVALSHA to run the script. If script does not exist\n// it is retried using EVAL.\nfunc (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {\n\tr := s.EvalSha(ctx, c, keys, args...)\n\tif errors.Is(r.Err(), ErrNoScript) {\n\t\treturn s.Eval(ctx, c, keys, args...)\n\t}\n\treturn r\n}\n\n// RunRO optimistically uses EVALSHA_RO to run the script. If script does not exist\n// it is retried using EVAL_RO.\nfunc (s *Script) RunRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {\n\tr := s.EvalShaRO(ctx, c, keys, args...)\n\tif errors.Is(r.Err(), ErrNoScript) {\n\t\treturn s.EvalRO(ctx, c, keys, args...)\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "scripting_commands.go",
    "content": "package redis\n\nimport \"context\"\n\ntype ScriptingFunctionsCmdable interface {\n\tEval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd\n\tEvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd\n\tEvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd\n\tEvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd\n\tScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd\n\tScriptFlush(ctx context.Context) *StatusCmd\n\tScriptKill(ctx context.Context) *StatusCmd\n\tScriptLoad(ctx context.Context, script string) *StringCmd\n\n\tFunctionLoad(ctx context.Context, code string) *StringCmd\n\tFunctionLoadReplace(ctx context.Context, code string) *StringCmd\n\tFunctionDelete(ctx context.Context, libName string) *StringCmd\n\tFunctionFlush(ctx context.Context) *StringCmd\n\tFunctionKill(ctx context.Context) *StringCmd\n\tFunctionFlushAsync(ctx context.Context) *StringCmd\n\tFunctionList(ctx context.Context, q FunctionListQuery) *FunctionListCmd\n\tFunctionDump(ctx context.Context) *StringCmd\n\tFunctionRestore(ctx context.Context, libDump string) *StringCmd\n\tFunctionStats(ctx context.Context) *FunctionStatsCmd\n\tFCall(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd\n\tFCallRo(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd\n\tFCallRO(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd\n}\n\nfunc (c cmdable) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd {\n\treturn c.eval(ctx, \"eval\", script, keys, args...)\n}\n\nfunc (c cmdable) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd {\n\treturn c.eval(ctx, \"eval_ro\", script, keys, args...)\n}\n\nfunc (c cmdable) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd {\n\treturn c.eval(ctx, \"evalsha\", sha1, keys, args...)\n}\n\nfunc (c cmdable) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd {\n\treturn c.eval(ctx, \"evalsha_ro\", sha1, keys, args...)\n}\n\nfunc (c cmdable) eval(ctx context.Context, name, payload string, keys []string, args ...interface{}) *Cmd {\n\tcmdArgs := make([]interface{}, 3+len(keys), 3+len(keys)+len(args))\n\tcmdArgs[0] = name\n\tcmdArgs[1] = payload\n\tcmdArgs[2] = len(keys)\n\tfor i, key := range keys {\n\t\tcmdArgs[3+i] = key\n\t}\n\tcmdArgs = appendArgs(cmdArgs, args)\n\tcmd := NewCmd(ctx, cmdArgs...)\n\n\t// it is possible that only args exist without a key.\n\t// rdb.eval(ctx, eval, script, nil, arg1, arg2)\n\tif len(keys) > 0 {\n\t\tcmd.SetFirstKeyPos(3)\n\t}\n\t_ = c(ctx, cmd)\n\tif err := cmd.Err(); err != nil {\n\t\tif HasErrorPrefix(err, \"NOSCRIPT\") {\n\t\t\tcmd.SetErr(ErrNoScript)\n\t\t}\n\t}\n\treturn cmd\n}\n\nfunc (c cmdable) ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd {\n\targs := make([]interface{}, 2+len(hashes))\n\targs[0] = \"script\"\n\targs[1] = \"exists\"\n\tfor i, hash := range hashes {\n\t\targs[2+i] = hash\n\t}\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ScriptFlush(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"script\", \"flush\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ScriptKill(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"script\", \"kill\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ScriptLoad(ctx context.Context, script string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"script\", \"load\", script)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ------------------------------------------------------------------------------\n\n// FunctionListQuery is used with FunctionList to query for Redis libraries\n//\n//\t  \tLibraryNamePattern \t- Use an empty string to get all libraries.\n//\t  \t\t\t\t\t\t- Use a glob-style pattern to match multiple libraries with a matching name\n//\t  \t\t\t\t\t\t- Use a library's full name to match a single library\n//\t\tWithCode\t\t\t- If true, it will return the code of the library\ntype FunctionListQuery struct {\n\tLibraryNamePattern string\n\tWithCode           bool\n}\n\nfunc (c cmdable) FunctionLoad(ctx context.Context, code string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"load\", code)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionLoadReplace(ctx context.Context, code string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"load\", \"replace\", code)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionDelete(ctx context.Context, libName string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"delete\", libName)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionFlush(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"flush\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionKill(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"kill\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionFlushAsync(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"flush\", \"async\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionList(ctx context.Context, q FunctionListQuery) *FunctionListCmd {\n\targs := make([]interface{}, 2, 5)\n\targs[0] = \"function\"\n\targs[1] = \"list\"\n\tif q.LibraryNamePattern != \"\" {\n\t\targs = append(args, \"libraryname\", q.LibraryNamePattern)\n\t}\n\tif q.WithCode {\n\t\targs = append(args, \"withcode\")\n\t}\n\tcmd := NewFunctionListCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionDump(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"dump\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionRestore(ctx context.Context, libDump string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"function\", \"restore\", libDump)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FunctionStats(ctx context.Context) *FunctionStatsCmd {\n\tcmd := NewFunctionStatsCmd(ctx, \"function\", \"stats\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) FCall(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd {\n\tcmdArgs := fcallArgs(\"fcall\", function, keys, args...)\n\tcmd := NewCmd(ctx, cmdArgs...)\n\tif len(keys) > 0 {\n\t\tcmd.SetFirstKeyPos(3)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FCallRo this function simply calls FCallRO,\n// Deprecated: to maintain convention FCallRO.\nfunc (c cmdable) FCallRo(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd {\n\treturn c.FCallRO(ctx, function, keys, args...)\n}\n\nfunc (c cmdable) FCallRO(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd {\n\tcmdArgs := fcallArgs(\"fcall_ro\", function, keys, args...)\n\tcmd := NewCmd(ctx, cmdArgs...)\n\tif len(keys) > 0 {\n\t\tcmd.SetFirstKeyPos(3)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc fcallArgs(command string, function string, keys []string, args ...interface{}) []interface{} {\n\tcmdArgs := make([]interface{}, 3+len(keys), 3+len(keys)+len(args))\n\tcmdArgs[0] = command\n\tcmdArgs[1] = function\n\tcmdArgs[2] = len(keys)\n\tfor i, key := range keys {\n\t\tcmdArgs[3+i] = key\n\t}\n\n\tcmdArgs = append(cmdArgs, args...)\n\treturn cmdArgs\n}\n"
  },
  {
    "path": "scripts/bump_deps.sh",
    "content": "PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \\; \\\n  | sed 's/^\\.\\///' \\\n  | sort)\n\nfor dir in $PACKAGE_DIRS\ndo\n    printf \"${dir}: go get -d && go mod tidy\\n\"\n    (cd ./${dir} && go get -d && go mod tidy)\ndone\n"
  },
  {
    "path": "scripts/release.sh",
    "content": "#!/bin/bash\n\nset -e\n\nhelp() {\n    cat <<- EOF\nUsage: TAG=tag $0\n\nUpdates version in go.mod files and pushes a new brash to GitHub.\n\nVARIABLES:\n  TAG        git tag, for example, v1.0.0\nEOF\n    exit 0\n}\n\nif [ -z \"$TAG\" ]\nthen\n    printf \"TAG is required\\n\\n\"\n    help\nfi\n\nTAG_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-]+)*)?$\"\nif ! [[ \"${TAG}\" =~ ${TAG_REGEX} ]]; then\n    printf \"TAG is not valid: ${TAG}\\n\\n\"\n    exit 1\nfi\n\nTAG_FOUND=`git tag --list ${TAG}`\nif [[ ${TAG_FOUND} = ${TAG} ]] ; then\n    printf \"tag ${TAG} already exists\\n\\n\"\n    exit 1\nfi\n\nif ! git diff --quiet\nthen\n    printf \"working tree is not clean\\n\\n\"\n    git status\n    exit 1\nfi\n\ngit checkout master\n\nPACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \\; \\\n  | sed 's/^\\.\\///' \\\n  | sort)\n\nfor dir in $PACKAGE_DIRS\ndo\n    printf \"${dir}: go get -u && go mod tidy\\n\"\n    #(cd ./${dir} && go get -u && go mod tidy -compat=1.21)\ndone\n\nfor dir in $PACKAGE_DIRS\ndo\n    sed --in-place \\\n        \"s/redis\\/go-redis\\([^ ]*\\) v.*/redis\\/go-redis\\1 ${TAG}/\" \"${dir}/go.mod\"\n    #(cd ./${dir} && go get -u && go mod tidy -compat=1.21)\n    (cd ./${dir} && go mod tidy -compat=1.21)\ndone\n\nsed --in-place \"s/\\(return \\)\\\"[^\\\"]*\\\"/\\1\\\"${TAG#v}\\\"/\" ./version.go\n\ngit checkout -b release/${TAG} master\ngit add -u\ngit commit -m \"chore: release $TAG (release.sh)\"\ngit push origin release/${TAG}\n"
  },
  {
    "path": "scripts/tag.sh",
    "content": "#!/bin/bash\n\nset -e\n\nDRY_RUN=1\n\nhelps() {\n    cat <<- EOF\nUsage: $0 TAGVERSION [-t]\n\nCreates git tags for public Go packages.\n\nARGUMENTS:\n  TAGVERSION    Tag version to create, for example v1.0.0\n\nOPTIONS:\n  -t           Execute git commands (default: dry run)\nEOF\n    exit 0\n}\n\n\nif [ $# -eq 0 ]; then\n    echo \"Error: Tag version is required\"\n    helps\nfi\n\nTAG=$1\nshift\n\nwhile getopts \"t\" opt; do\n    case $opt in\n        t)\n            DRY_RUN=0\n            ;;\n        \\?)\n            echo \"Invalid option: -$OPTARG\" >&2\n            exit 1\n            ;;\n    esac\ndone\n\n\nif [ \"$DRY_RUN\" -eq 1 ]; then\n    echo \"Running in dry-run mode\"\nfi\n\nif ! grep -Fq \"\\\"${TAG#v}\\\"\" version.go\nthen\n    printf \"version.go does not contain ${TAG#v}\\n\"\n    exit 1\nfi\n\nGOMOD_ERRORS=0\n\n# Check go.mod files for correct dependency versions\nwhile read -r mod_file; do\n    # Look for go-redis packages in require statements\n    while read -r pkg version; do\n        if [ \"$version\" != \"${TAG}\" ]; then\n            printf \"Error: %s has incorrect version for package %s: %s (expected %s)\\n\" \"$mod_file\" \"$pkg\" \"$version\" \"${TAG}\"\n            GOMOD_ERRORS=$((GOMOD_ERRORS + 1))\n        fi\n    done < <(awk '/^require|^require \\(/{p=1;next} /^\\)/{p=0} p{if($1 ~ /^github\\.com\\/redis\\/go-redis/){print $1, $2}}' \"$mod_file\")\ndone < <(find . -type f -name 'go.mod')\n\n# Exit if there are gomod errors\nif [ $GOMOD_ERRORS -gt 0 ]; then\n    exit 1\nfi\n\n\nPACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \\; \\\n  | grep -E -v \"example|internal\" \\\n  | sed 's/^\\.\\///' \\\n  | sort)\n\n\nexecute_git_command() {\n    if [ \"$DRY_RUN\" -eq 0 ]; then\n        \"$@\"\n    else\n        echo \"DRY-RUN: Would execute: $@\"\n    fi\n}\n\nexecute_git_command git tag ${TAG}\nexecute_git_command git push origin ${TAG}\n\nfor dir in $PACKAGE_DIRS\ndo\n    printf \"tagging ${dir}/${TAG}\\n\"\n    execute_git_command git tag ${dir}/${TAG}\n    execute_git_command git push origin ${dir}/${TAG}\ndone\n"
  },
  {
    "path": "search_builders.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n)\n\n// ----------------------\n// Search Module Builders\n// ----------------------\n\n// SearchBuilder provides a fluent API for FT.SEARCH\n// (see original FTSearchOptions for all options).\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype SearchBuilder struct {\n\tc       *Client\n\tctx     context.Context\n\tindex   string\n\tquery   string\n\toptions *FTSearchOptions\n}\n\n// NewSearchBuilder creates a new SearchBuilder for FT.SEARCH commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewSearchBuilder(ctx context.Context, index, query string) *SearchBuilder {\n\tb := &SearchBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSearchOptions{LimitOffset: -1}}\n\treturn b\n}\n\n// WithScores includes WITHSCORES.\nfunc (b *SearchBuilder) WithScores() *SearchBuilder {\n\tb.options.WithScores = true\n\treturn b\n}\n\n// NoContent includes NOCONTENT.\nfunc (b *SearchBuilder) NoContent() *SearchBuilder { b.options.NoContent = true; return b }\n\n// Verbatim includes VERBATIM.\nfunc (b *SearchBuilder) Verbatim() *SearchBuilder { b.options.Verbatim = true; return b }\n\n// NoStopWords includes NOSTOPWORDS.\nfunc (b *SearchBuilder) NoStopWords() *SearchBuilder { b.options.NoStopWords = true; return b }\n\n// WithPayloads includes WITHPAYLOADS.\nfunc (b *SearchBuilder) WithPayloads() *SearchBuilder {\n\tb.options.WithPayloads = true\n\treturn b\n}\n\n// WithSortKeys includes WITHSORTKEYS.\nfunc (b *SearchBuilder) WithSortKeys() *SearchBuilder {\n\tb.options.WithSortKeys = true\n\treturn b\n}\n\n// Filter adds a FILTER clause: FILTER <field> <min> <max>.\nfunc (b *SearchBuilder) Filter(field string, min, max interface{}) *SearchBuilder {\n\tb.options.Filters = append(b.options.Filters, FTSearchFilter{\n\t\tFieldName: field,\n\t\tMin:       min,\n\t\tMax:       max,\n\t})\n\treturn b\n}\n\n// GeoFilter adds a GEOFILTER clause: GEOFILTER <field> <lon> <lat> <radius> <unit>.\nfunc (b *SearchBuilder) GeoFilter(field string, lon, lat, radius float64, unit string) *SearchBuilder {\n\tb.options.GeoFilter = append(b.options.GeoFilter, FTSearchGeoFilter{\n\t\tFieldName: field,\n\t\tLongitude: lon,\n\t\tLatitude:  lat,\n\t\tRadius:    radius,\n\t\tUnit:      unit,\n\t})\n\treturn b\n}\n\n// InKeys restricts the search to the given keys.\nfunc (b *SearchBuilder) InKeys(keys ...interface{}) *SearchBuilder {\n\tb.options.InKeys = append(b.options.InKeys, keys...)\n\treturn b\n}\n\n// InFields restricts the search to the given fields.\nfunc (b *SearchBuilder) InFields(fields ...interface{}) *SearchBuilder {\n\tb.options.InFields = append(b.options.InFields, fields...)\n\treturn b\n}\n\n// ReturnFields adds simple RETURN <n> <field>...\nfunc (b *SearchBuilder) ReturnFields(fields ...string) *SearchBuilder {\n\tfor _, f := range fields {\n\t\tb.options.Return = append(b.options.Return, FTSearchReturn{FieldName: f})\n\t}\n\treturn b\n}\n\n// ReturnAs adds RETURN <field> AS <alias>.\nfunc (b *SearchBuilder) ReturnAs(field, alias string) *SearchBuilder {\n\tb.options.Return = append(b.options.Return, FTSearchReturn{FieldName: field, As: alias})\n\treturn b\n}\n\n// Slop adds SLOP <n>.\nfunc (b *SearchBuilder) Slop(slop int) *SearchBuilder {\n\tb.options.Slop = slop\n\treturn b\n}\n\n// Timeout adds TIMEOUT <ms>.\nfunc (b *SearchBuilder) Timeout(timeout int) *SearchBuilder {\n\tb.options.Timeout = timeout\n\treturn b\n}\n\n// InOrder includes INORDER.\nfunc (b *SearchBuilder) InOrder() *SearchBuilder {\n\tb.options.InOrder = true\n\treturn b\n}\n\n// Language sets LANGUAGE <lang>.\nfunc (b *SearchBuilder) Language(lang string) *SearchBuilder {\n\tb.options.Language = lang\n\treturn b\n}\n\n// Expander sets EXPANDER <expander>.\nfunc (b *SearchBuilder) Expander(expander string) *SearchBuilder {\n\tb.options.Expander = expander\n\treturn b\n}\n\n// Scorer sets SCORER <scorer>.\nfunc (b *SearchBuilder) Scorer(scorer string) *SearchBuilder {\n\tb.options.Scorer = scorer\n\treturn b\n}\n\n// ExplainScore includes EXPLAINSCORE.\nfunc (b *SearchBuilder) ExplainScore() *SearchBuilder {\n\tb.options.ExplainScore = true\n\treturn b\n}\n\n// Payload sets PAYLOAD <payload>.\nfunc (b *SearchBuilder) Payload(payload string) *SearchBuilder {\n\tb.options.Payload = payload\n\treturn b\n}\n\n// SortBy adds SORTBY <field> ASC|DESC.\nfunc (b *SearchBuilder) SortBy(field string, asc bool) *SearchBuilder {\n\tb.options.SortBy = append(b.options.SortBy, FTSearchSortBy{\n\t\tFieldName: field,\n\t\tAsc:       asc,\n\t\tDesc:      !asc,\n\t})\n\treturn b\n}\n\n// WithSortByCount includes WITHCOUNT (when used with SortBy).\nfunc (b *SearchBuilder) WithSortByCount() *SearchBuilder {\n\tb.options.SortByWithCount = true\n\treturn b\n}\n\n// Param adds a single PARAMS <k> <v>.\nfunc (b *SearchBuilder) Param(key string, value interface{}) *SearchBuilder {\n\tif b.options.Params == nil {\n\t\tb.options.Params = make(map[string]interface{}, 1)\n\t}\n\tb.options.Params[key] = value\n\treturn b\n}\n\n// ParamsMap adds multiple PARAMS at once.\nfunc (b *SearchBuilder) ParamsMap(p map[string]interface{}) *SearchBuilder {\n\tif b.options.Params == nil {\n\t\tb.options.Params = make(map[string]interface{}, len(p))\n\t}\n\tfor k, v := range p {\n\t\tb.options.Params[k] = v\n\t}\n\treturn b\n}\n\n// Dialect sets DIALECT <version>.\nfunc (b *SearchBuilder) Dialect(version int) *SearchBuilder {\n\tb.options.DialectVersion = version\n\treturn b\n}\n\n// Limit sets OFFSET and COUNT. CountOnly uses LIMIT 0 0.\nfunc (b *SearchBuilder) Limit(offset, count int) *SearchBuilder {\n\tb.options.LimitOffset = offset\n\tb.options.Limit = count\n\treturn b\n}\nfunc (b *SearchBuilder) CountOnly() *SearchBuilder { b.options.CountOnly = true; return b }\n\n// Run executes FT.SEARCH and returns a typed result.\nfunc (b *SearchBuilder) Run() (FTSearchResult, error) {\n\tcmd := b.c.FTSearchWithArgs(b.ctx, b.index, b.query, b.options)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// AggregateBuilder for FT.AGGREGATE\n// ----------------------\n\ntype AggregateBuilder struct {\n\tc       *Client\n\tctx     context.Context\n\tindex   string\n\tquery   string\n\toptions *FTAggregateOptions\n}\n\n// NewAggregateBuilder creates a new AggregateBuilder for FT.AGGREGATE commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewAggregateBuilder(ctx context.Context, index, query string) *AggregateBuilder {\n\treturn &AggregateBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTAggregateOptions{LimitOffset: -1}}\n}\n\n// Verbatim includes VERBATIM.\nfunc (b *AggregateBuilder) Verbatim() *AggregateBuilder { b.options.Verbatim = true; return b }\n\n// AddScores includes ADDSCORES.\nfunc (b *AggregateBuilder) AddScores() *AggregateBuilder { b.options.AddScores = true; return b }\n\n// Scorer sets SCORER <scorer>.\nfunc (b *AggregateBuilder) Scorer(s string) *AggregateBuilder {\n\tb.options.Scorer = s\n\treturn b\n}\n\n// LoadAll includes LOAD * (mutually exclusive with Load).\nfunc (b *AggregateBuilder) LoadAll() *AggregateBuilder {\n\tb.options.LoadAll = true\n\treturn b\n}\n\n// Load adds LOAD <n> <field> [AS alias]...\n// You can call it multiple times for multiple fields.\nfunc (b *AggregateBuilder) Load(field string, alias ...string) *AggregateBuilder {\n\t// each Load entry becomes one element in options.Load\n\tl := FTAggregateLoad{Field: field}\n\tif len(alias) > 0 {\n\t\tl.As = alias[0]\n\t}\n\tb.options.Load = append(b.options.Load, l)\n\treturn b\n}\n\n// Timeout sets TIMEOUT <ms>.\nfunc (b *AggregateBuilder) Timeout(ms int) *AggregateBuilder {\n\tb.options.Timeout = ms\n\treturn b\n}\n\n// Apply adds APPLY <field> [AS alias].\nfunc (b *AggregateBuilder) Apply(field string, alias ...string) *AggregateBuilder {\n\ta := FTAggregateApply{Field: field}\n\tif len(alias) > 0 {\n\t\ta.As = alias[0]\n\t}\n\tb.options.Apply = append(b.options.Apply, a)\n\treturn b\n}\n\n// GroupBy starts a new GROUPBY <fields...> clause.\nfunc (b *AggregateBuilder) GroupBy(fields ...interface{}) *AggregateBuilder {\n\tb.options.GroupBy = append(b.options.GroupBy, FTAggregateGroupBy{\n\t\tFields: fields,\n\t})\n\treturn b\n}\n\n// Reduce adds a REDUCE <fn> [<#args> <args...>] clause to the *last* GROUPBY.\nfunc (b *AggregateBuilder) Reduce(fn SearchAggregator, args ...interface{}) *AggregateBuilder {\n\tif len(b.options.GroupBy) == 0 {\n\t\t// no GROUPBY yet — nothing to attach to\n\t\treturn b\n\t}\n\tidx := len(b.options.GroupBy) - 1\n\tb.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{\n\t\tReducer: fn,\n\t\tArgs:    args,\n\t})\n\treturn b\n}\n\n// ReduceAs does the same but also sets an alias: REDUCE <fn> … AS <alias>\nfunc (b *AggregateBuilder) ReduceAs(fn SearchAggregator, alias string, args ...interface{}) *AggregateBuilder {\n\tif len(b.options.GroupBy) == 0 {\n\t\treturn b\n\t}\n\tidx := len(b.options.GroupBy) - 1\n\tb.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{\n\t\tReducer: fn,\n\t\tArgs:    args,\n\t\tAs:      alias,\n\t})\n\treturn b\n}\n\n// SortBy adds SORTBY <field> ASC|DESC.\nfunc (b *AggregateBuilder) SortBy(field string, asc bool) *AggregateBuilder {\n\tsb := FTAggregateSortBy{FieldName: field, Asc: asc, Desc: !asc}\n\tb.options.SortBy = append(b.options.SortBy, sb)\n\treturn b\n}\n\n// SortByMax sets MAX <n> (only if SortBy was called).\nfunc (b *AggregateBuilder) SortByMax(max int) *AggregateBuilder {\n\tb.options.SortByMax = max\n\treturn b\n}\n\n// Filter sets FILTER <expr>.\nfunc (b *AggregateBuilder) Filter(expr string) *AggregateBuilder {\n\tb.options.Filter = expr\n\treturn b\n}\n\n// WithCursor enables WITHCURSOR [COUNT <n>] [MAXIDLE <ms>].\nfunc (b *AggregateBuilder) WithCursor(count, maxIdle int) *AggregateBuilder {\n\tb.options.WithCursor = true\n\tif b.options.WithCursorOptions == nil {\n\t\tb.options.WithCursorOptions = &FTAggregateWithCursor{}\n\t}\n\tb.options.WithCursorOptions.Count = count\n\tb.options.WithCursorOptions.MaxIdle = maxIdle\n\treturn b\n}\n\n// Params adds PARAMS <k v> pairs.\nfunc (b *AggregateBuilder) Params(p map[string]interface{}) *AggregateBuilder {\n\tif b.options.Params == nil {\n\t\tb.options.Params = make(map[string]interface{}, len(p))\n\t}\n\tfor k, v := range p {\n\t\tb.options.Params[k] = v\n\t}\n\treturn b\n}\n\n// Dialect sets DIALECT <version>.\nfunc (b *AggregateBuilder) Dialect(version int) *AggregateBuilder {\n\tb.options.DialectVersion = version\n\treturn b\n}\n\n// Run executes FT.AGGREGATE and returns a typed result.\nfunc (b *AggregateBuilder) Run() (*FTAggregateResult, error) {\n\tcmd := b.c.FTAggregateWithArgs(b.ctx, b.index, b.query, b.options)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// CreateIndexBuilder for FT.CREATE\n// ----------------------\n// CreateIndexBuilder is builder for FT.CREATE\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype CreateIndexBuilder struct {\n\tc       *Client\n\tctx     context.Context\n\tindex   string\n\toptions *FTCreateOptions\n\tschema  []*FieldSchema\n}\n\n// NewCreateIndexBuilder creates a new CreateIndexBuilder for FT.CREATE commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewCreateIndexBuilder(ctx context.Context, index string) *CreateIndexBuilder {\n\treturn &CreateIndexBuilder{c: c, ctx: ctx, index: index, options: &FTCreateOptions{}}\n}\n\n// OnHash sets ON HASH.\nfunc (b *CreateIndexBuilder) OnHash() *CreateIndexBuilder { b.options.OnHash = true; return b }\n\n// OnJSON sets ON JSON.\nfunc (b *CreateIndexBuilder) OnJSON() *CreateIndexBuilder { b.options.OnJSON = true; return b }\n\n// Prefix sets PREFIX.\nfunc (b *CreateIndexBuilder) Prefix(prefixes ...interface{}) *CreateIndexBuilder {\n\tb.options.Prefix = prefixes\n\treturn b\n}\n\n// Filter sets FILTER.\nfunc (b *CreateIndexBuilder) Filter(filter string) *CreateIndexBuilder {\n\tb.options.Filter = filter\n\treturn b\n}\n\n// DefaultLanguage sets LANGUAGE.\nfunc (b *CreateIndexBuilder) DefaultLanguage(lang string) *CreateIndexBuilder {\n\tb.options.DefaultLanguage = lang\n\treturn b\n}\n\n// LanguageField sets LANGUAGE_FIELD.\nfunc (b *CreateIndexBuilder) LanguageField(field string) *CreateIndexBuilder {\n\tb.options.LanguageField = field\n\treturn b\n}\n\n// Score sets SCORE.\nfunc (b *CreateIndexBuilder) Score(score float64) *CreateIndexBuilder {\n\tb.options.Score = score\n\treturn b\n}\n\n// ScoreField sets SCORE_FIELD.\nfunc (b *CreateIndexBuilder) ScoreField(field string) *CreateIndexBuilder {\n\tb.options.ScoreField = field\n\treturn b\n}\n\n// PayloadField sets PAYLOAD_FIELD.\nfunc (b *CreateIndexBuilder) PayloadField(field string) *CreateIndexBuilder {\n\tb.options.PayloadField = field\n\treturn b\n}\n\n// NoOffsets includes NOOFFSETS.\nfunc (b *CreateIndexBuilder) NoOffsets() *CreateIndexBuilder { b.options.NoOffsets = true; return b }\n\n// Temporary sets TEMPORARY seconds.\nfunc (b *CreateIndexBuilder) Temporary(sec int) *CreateIndexBuilder {\n\tb.options.Temporary = sec\n\treturn b\n}\n\n// NoHL includes NOHL.\nfunc (b *CreateIndexBuilder) NoHL() *CreateIndexBuilder { b.options.NoHL = true; return b }\n\n// NoFields includes NOFIELDS.\nfunc (b *CreateIndexBuilder) NoFields() *CreateIndexBuilder { b.options.NoFields = true; return b }\n\n// NoFreqs includes NOFREQS.\nfunc (b *CreateIndexBuilder) NoFreqs() *CreateIndexBuilder { b.options.NoFreqs = true; return b }\n\n// StopWords sets STOPWORDS.\nfunc (b *CreateIndexBuilder) StopWords(words ...interface{}) *CreateIndexBuilder {\n\tb.options.StopWords = words\n\treturn b\n}\n\n// SkipInitialScan includes SKIPINITIALSCAN.\nfunc (b *CreateIndexBuilder) SkipInitialScan() *CreateIndexBuilder {\n\tb.options.SkipInitialScan = true\n\treturn b\n}\n\n// Schema adds a FieldSchema.\nfunc (b *CreateIndexBuilder) Schema(field *FieldSchema) *CreateIndexBuilder {\n\tb.schema = append(b.schema, field)\n\treturn b\n}\n\n// Run executes FT.CREATE and returns the status.\nfunc (b *CreateIndexBuilder) Run() (string, error) {\n\tcmd := b.c.FTCreate(b.ctx, b.index, b.options, b.schema...)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// DropIndexBuilder for FT.DROPINDEX\n// ----------------------\n// DropIndexBuilder is a builder for FT.DROPINDEX\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype DropIndexBuilder struct {\n\tc       *Client\n\tctx     context.Context\n\tindex   string\n\toptions *FTDropIndexOptions\n}\n\n// NewDropIndexBuilder creates a new DropIndexBuilder for FT.DROPINDEX commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewDropIndexBuilder(ctx context.Context, index string) *DropIndexBuilder {\n\treturn &DropIndexBuilder{c: c, ctx: ctx, index: index}\n}\n\n// DeleteRuncs includes DD.\nfunc (b *DropIndexBuilder) DeleteDocs() *DropIndexBuilder { b.options.DeleteDocs = true; return b }\n\n// Run executes FT.DROPINDEX.\nfunc (b *DropIndexBuilder) Run() (string, error) {\n\tcmd := b.c.FTDropIndexWithArgs(b.ctx, b.index, b.options)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// AliasBuilder for FT.ALIAS* commands\n// ----------------------\n// AliasBuilder is builder for FT.ALIAS* commands\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype AliasBuilder struct {\n\tc      *Client\n\tctx    context.Context\n\talias  string\n\tindex  string\n\taction string // add|del|update\n}\n\n// NewAliasBuilder creates a new AliasBuilder for FT.ALIAS* commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewAliasBuilder(ctx context.Context, alias string) *AliasBuilder {\n\treturn &AliasBuilder{c: c, ctx: ctx, alias: alias}\n}\n\n// Action sets the action for the alias builder.\nfunc (b *AliasBuilder) Action(action string) *AliasBuilder {\n\tb.action = action\n\treturn b\n}\n\n// Add sets the action to \"add\" and requires an index.\nfunc (b *AliasBuilder) Add(index string) *AliasBuilder {\n\tb.action = \"add\"\n\tb.index = index\n\treturn b\n}\n\n// Del sets the action to \"del\".\nfunc (b *AliasBuilder) Del() *AliasBuilder {\n\tb.action = \"del\"\n\treturn b\n}\n\n// Update sets the action to \"update\" and requires an index.\nfunc (b *AliasBuilder) Update(index string) *AliasBuilder {\n\tb.action = \"update\"\n\tb.index = index\n\treturn b\n}\n\n// Run executes the configured alias command.\nfunc (b *AliasBuilder) Run() (string, error) {\n\tswitch b.action {\n\tcase \"add\":\n\t\tcmd := b.c.FTAliasAdd(b.ctx, b.index, b.alias)\n\t\treturn cmd.Result()\n\tcase \"del\":\n\t\tcmd := b.c.FTAliasDel(b.ctx, b.alias)\n\t\treturn cmd.Result()\n\tcase \"update\":\n\t\tcmd := b.c.FTAliasUpdate(b.ctx, b.index, b.alias)\n\t\treturn cmd.Result()\n\t}\n\treturn \"\", nil\n}\n\n// ----------------------\n// ExplainBuilder for FT.EXPLAIN\n// ----------------------\n// ExplainBuilder is builder for FT.EXPLAIN\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype ExplainBuilder struct {\n\tc       *Client\n\tctx     context.Context\n\tindex   string\n\tquery   string\n\toptions *FTExplainOptions\n}\n\n// NewExplainBuilder creates a new ExplainBuilder for FT.EXPLAIN commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewExplainBuilder(ctx context.Context, index, query string) *ExplainBuilder {\n\treturn &ExplainBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTExplainOptions{}}\n}\n\n// Dialect sets dialect for EXPLAINCLI.\nfunc (b *ExplainBuilder) Dialect(d string) *ExplainBuilder { b.options.Dialect = d; return b }\n\n// Run executes FT.EXPLAIN and returns the plan.\nfunc (b *ExplainBuilder) Run() (string, error) {\n\tcmd := b.c.FTExplainWithArgs(b.ctx, b.index, b.query, b.options)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// InfoBuilder for FT.INFO\n// ----------------------\n\ntype FTInfoBuilder struct {\n\tc     *Client\n\tctx   context.Context\n\tindex string\n}\n\n// NewSearchInfoBuilder creates a new FTInfoBuilder for FT.INFO commands.\nfunc (c *Client) NewSearchInfoBuilder(ctx context.Context, index string) *FTInfoBuilder {\n\treturn &FTInfoBuilder{c: c, ctx: ctx, index: index}\n}\n\n// Run executes FT.INFO and returns detailed info.\nfunc (b *FTInfoBuilder) Run() (FTInfoResult, error) {\n\tcmd := b.c.FTInfo(b.ctx, b.index)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// SpellCheckBuilder for FT.SPELLCHECK\n// ----------------------\n// SpellCheckBuilder is builder for FT.SPELLCHECK\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype SpellCheckBuilder struct {\n\tc       *Client\n\tctx     context.Context\n\tindex   string\n\tquery   string\n\toptions *FTSpellCheckOptions\n}\n\n// NewSpellCheckBuilder creates a new SpellCheckBuilder for FT.SPELLCHECK commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewSpellCheckBuilder(ctx context.Context, index, query string) *SpellCheckBuilder {\n\treturn &SpellCheckBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSpellCheckOptions{}}\n}\n\n// Distance sets MAXDISTANCE.\nfunc (b *SpellCheckBuilder) Distance(d int) *SpellCheckBuilder { b.options.Distance = d; return b }\n\n// Terms sets INCLUDE or EXCLUDE terms.\nfunc (b *SpellCheckBuilder) Terms(include bool, dictionary string, terms ...interface{}) *SpellCheckBuilder {\n\tif b.options.Terms == nil {\n\t\tb.options.Terms = &FTSpellCheckTerms{}\n\t}\n\tif include {\n\t\tb.options.Terms.Inclusion = \"INCLUDE\"\n\t} else {\n\t\tb.options.Terms.Inclusion = \"EXCLUDE\"\n\t}\n\tb.options.Terms.Dictionary = dictionary\n\tb.options.Terms.Terms = terms\n\treturn b\n}\n\n// Dialect sets dialect version.\nfunc (b *SpellCheckBuilder) Dialect(d int) *SpellCheckBuilder { b.options.Dialect = d; return b }\n\n// Run executes FT.SPELLCHECK and returns suggestions.\nfunc (b *SpellCheckBuilder) Run() ([]SpellCheckResult, error) {\n\tcmd := b.c.FTSpellCheckWithArgs(b.ctx, b.index, b.query, b.options)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// DictBuilder for FT.DICT* commands\n// ----------------------\n// DictBuilder is builder for FT.DICT* commands\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype DictBuilder struct {\n\tc      *Client\n\tctx    context.Context\n\tdict   string\n\tterms  []interface{}\n\taction string // add|del|dump\n}\n\n// NewDictBuilder creates a new DictBuilder for FT.DICT* commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewDictBuilder(ctx context.Context, dict string) *DictBuilder {\n\treturn &DictBuilder{c: c, ctx: ctx, dict: dict}\n}\n\n// Action sets the action for the dictionary builder.\nfunc (b *DictBuilder) Action(action string) *DictBuilder {\n\tb.action = action\n\treturn b\n}\n\n// Add sets the action to \"add\" and requires terms.\nfunc (b *DictBuilder) Add(terms ...interface{}) *DictBuilder {\n\tb.action = \"add\"\n\tb.terms = terms\n\treturn b\n}\n\n// Del sets the action to \"del\" and requires terms.\nfunc (b *DictBuilder) Del(terms ...interface{}) *DictBuilder {\n\tb.action = \"del\"\n\tb.terms = terms\n\treturn b\n}\n\n// Dump sets the action to \"dump\".\nfunc (b *DictBuilder) Dump() *DictBuilder {\n\tb.action = \"dump\"\n\treturn b\n}\n\n// Run executes the configured dictionary command.\nfunc (b *DictBuilder) Run() (interface{}, error) {\n\tswitch b.action {\n\tcase \"add\":\n\t\tcmd := b.c.FTDictAdd(b.ctx, b.dict, b.terms...)\n\t\treturn cmd.Result()\n\tcase \"del\":\n\t\tcmd := b.c.FTDictDel(b.ctx, b.dict, b.terms...)\n\t\treturn cmd.Result()\n\tcase \"dump\":\n\t\tcmd := b.c.FTDictDump(b.ctx, b.dict)\n\t\treturn cmd.Result()\n\t}\n\treturn nil, nil\n}\n\n// ----------------------\n// TagValsBuilder for FT.TAGVALS\n// ----------------------\n// TagValsBuilder is builder for FT.TAGVALS\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype TagValsBuilder struct {\n\tc     *Client\n\tctx   context.Context\n\tindex string\n\tfield string\n}\n\n// NewTagValsBuilder creates a new TagValsBuilder for FT.TAGVALS commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewTagValsBuilder(ctx context.Context, index, field string) *TagValsBuilder {\n\treturn &TagValsBuilder{c: c, ctx: ctx, index: index, field: field}\n}\n\n// Run executes FT.TAGVALS and returns tag values.\nfunc (b *TagValsBuilder) Run() ([]string, error) {\n\tcmd := b.c.FTTagVals(b.ctx, b.index, b.field)\n\treturn cmd.Result()\n}\n\n// ----------------------\n// CursorBuilder for FT.CURSOR*\n// ----------------------\n// CursorBuilder is builder for FT.CURSOR* commands\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype CursorBuilder struct {\n\tc        *Client\n\tctx      context.Context\n\tindex    string\n\tcursorId int64\n\tcount    int\n\taction   string // read|del\n}\n\n// NewCursorBuilder creates a new CursorBuilder for FT.CURSOR* commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewCursorBuilder(ctx context.Context, index string, cursorId int64) *CursorBuilder {\n\treturn &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId}\n}\n\n// Action sets the action for the cursor builder.\nfunc (b *CursorBuilder) Action(action string) *CursorBuilder {\n\tb.action = action\n\treturn b\n}\n\n// Read sets the action to \"read\".\nfunc (b *CursorBuilder) Read() *CursorBuilder {\n\tb.action = \"read\"\n\treturn b\n}\n\n// Del sets the action to \"del\".\nfunc (b *CursorBuilder) Del() *CursorBuilder {\n\tb.action = \"del\"\n\treturn b\n}\n\n// Count for READ.\nfunc (b *CursorBuilder) Count(count int) *CursorBuilder { b.count = count; return b }\n\n// Run executes the cursor command.\nfunc (b *CursorBuilder) Run() (interface{}, error) {\n\tswitch b.action {\n\tcase \"read\":\n\t\tcmd := b.c.FTCursorRead(b.ctx, b.index, int(b.cursorId), b.count)\n\t\treturn cmd.Result()\n\tcase \"del\":\n\t\tcmd := b.c.FTCursorDel(b.ctx, b.index, int(b.cursorId))\n\t\treturn cmd.Result()\n\t}\n\treturn nil, nil\n}\n\n// ----------------------\n// SynUpdateBuilder for FT.SYNUPDATE\n// ----------------------\n// SyncUpdateBuilder is builder for FT.SYNCUPDATE\n// EXPERIMENTAL: this API is subject to change, use with caution.\ntype SynUpdateBuilder struct {\n\tc       *Client\n\tctx     context.Context\n\tindex   string\n\tgroupId interface{}\n\toptions *FTSynUpdateOptions\n\tterms   []interface{}\n}\n\n// NewSynUpdateBuilder creates a new SynUpdateBuilder for FT.SYNUPDATE commands.\n// EXPERIMENTAL: this API is subject to change, use with caution.\nfunc (c *Client) NewSynUpdateBuilder(ctx context.Context, index string, groupId interface{}) *SynUpdateBuilder {\n\treturn &SynUpdateBuilder{c: c, ctx: ctx, index: index, groupId: groupId, options: &FTSynUpdateOptions{}}\n}\n\n// SkipInitialScan includes SKIPINITIALSCAN.\nfunc (b *SynUpdateBuilder) SkipInitialScan() *SynUpdateBuilder {\n\tb.options.SkipInitialScan = true\n\treturn b\n}\n\n// Terms adds synonyms to the group.\nfunc (b *SynUpdateBuilder) Terms(terms ...interface{}) *SynUpdateBuilder { b.terms = terms; return b }\n\n// Run executes FT.SYNUPDATE.\nfunc (b *SynUpdateBuilder) Run() (string, error) {\n\tcmd := b.c.FTSynUpdateWithArgs(b.ctx, b.index, b.groupId, b.options, b.terms)\n\treturn cmd.Result()\n}\n"
  },
  {
    "path": "search_builders_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"RediSearch Builders\", Label(\"search\", \"builders\"), func() {\n\tctx := context.Background()\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(&redis.Options{Addr: \":6379\", Protocol: 2})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\texpectCloseErr := client.Close()\n\t\tExpect(expectCloseErr).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should create index and search with scores using builders\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx1\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"foo\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"foo\", \"hello world\")\n\t\tclient.HSet(ctx, \"doc2\", \"foo\", \"hello redis\")\n\n\t\tres, err := client.NewSearchBuilder(ctx, \"idx1\", \"hello\").WithScores().Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(Equal(2))\n\t\tfor _, doc := range res.Docs {\n\t\t\tExpect(*doc.Score).To(BeNumerically(\">\", 0))\n\t\t}\n\t})\n\n\tIt(\"should aggregate using builders\", Label(\"search\", \"ftaggregate\"), func() {\n\t\t_, err := client.NewCreateIndexBuilder(ctx, \"idx2\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tWaitForIndexing(client, \"idx2\")\n\n\t\tclient.HSet(ctx, \"d1\", \"n\", 1)\n\t\tclient.HSet(ctx, \"d2\", \"n\", 2)\n\n\t\tagg, err := client.NewAggregateBuilder(ctx, \"idx2\", \"*\").\n\t\t\tGroupBy(\"@n\").\n\t\t\tReduceAs(redis.SearchCount, \"count\").\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(agg.Rows)).To(Equal(2))\n\t})\n\n\tIt(\"should drop index using builder\", Label(\"search\", \"ftdropindex\"), func() {\n\t\tExpect(client.NewCreateIndexBuilder(ctx, \"idx3\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"x\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx3\")\n\n\t\tdropVal, err := client.NewDropIndexBuilder(ctx, \"idx3\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(dropVal).To(Equal(\"OK\"))\n\t})\n\n\tIt(\"should manage aliases using builder\", Label(\"search\", \"ftalias\"), func() {\n\t\tExpect(client.NewCreateIndexBuilder(ctx, \"idx4\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"t\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx4\")\n\n\t\taddVal, err := client.NewAliasBuilder(ctx, \"alias1\").Add(\"idx4\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(addVal).To(Equal(\"OK\"))\n\n\t\t_, err = client.NewSearchBuilder(ctx, \"alias1\", \"*\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tdelVal, err := client.NewAliasBuilder(ctx, \"alias1\").Del().Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(delVal).To(Equal(\"OK\"))\n\t})\n\n\tIt(\"should explain query using ExplainBuilder\", Label(\"search\", \"builders\", \"ftexplain\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_explain\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"foo\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_explain\")\n\n\t\texpl, err := client.NewExplainBuilder(ctx, \"idx_explain\", \"foo\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(expl).To(ContainSubstring(\"UNION\"))\n\t})\n\n\tIt(\"should retrieve info using SearchInfo builder\", Label(\"search\", \"builders\", \"ftinfo\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_info\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"foo\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_info\")\n\n\t\ti, err := client.NewSearchInfoBuilder(ctx, \"idx_info\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(i.IndexName).To(Equal(\"idx_info\"))\n\t})\n\n\tIt(\"should spellcheck using builder\", Label(\"search\", \"builders\", \"ftspellcheck\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_spell\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"foo\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_spell\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"foo\", \"bar\")\n\n\t\t_, err = client.NewSpellCheckBuilder(ctx, \"idx_spell\", \"ba\").Distance(1).Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should manage dictionary using DictBuilder\", Label(\"search\", \"ftdict\"), func() {\n\t\taddCount, err := client.NewDictBuilder(ctx, \"dict1\").Add(\"a\", \"b\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(addCount).To(Equal(int64(2)))\n\n\t\tdump, err := client.NewDictBuilder(ctx, \"dict1\").Dump().Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(dump).To(ContainElements(\"a\", \"b\"))\n\n\t\tdelCount, err := client.NewDictBuilder(ctx, \"dict1\").Del(\"a\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(delCount).To(Equal(int64(1)))\n\t})\n\n\tIt(\"should tag values using TagValsBuilder\", Label(\"search\", \"builders\", \"fttagvals\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_tag\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"tags\", FieldType: redis.SearchFieldTypeTag}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_tag\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"tags\", \"red,blue\")\n\t\tclient.HSet(ctx, \"doc2\", \"tags\", \"green,blue\")\n\n\t\tvals, err := client.NewTagValsBuilder(ctx, \"idx_tag\", \"tags\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(vals).To(BeAssignableToTypeOf([]string{}))\n\t})\n\n\tIt(\"should cursor read and delete using CursorBuilder\", Label(\"search\", \"builders\", \"ftcursor\"), func() {\n\t\tExpect(client.NewCreateIndexBuilder(ctx, \"idx5\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"f\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx5\")\n\t\tclient.HSet(ctx, \"doc1\", \"f\", \"hello\")\n\t\tclient.HSet(ctx, \"doc2\", \"f\", \"world\")\n\n\t\tcursorBuilder := client.NewCursorBuilder(ctx, \"idx5\", 1)\n\t\tExpect(cursorBuilder).NotTo(BeNil())\n\n\t\tcursorBuilder = cursorBuilder.Count(10)\n\t\tExpect(cursorBuilder).NotTo(BeNil())\n\n\t\tdelBuilder := client.NewCursorBuilder(ctx, \"idx5\", 1)\n\t\tExpect(delBuilder).NotTo(BeNil())\n\t})\n\n\tIt(\"should update synonyms using SynUpdateBuilder\", Label(\"search\", \"builders\", \"ftsynupdate\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_syn\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"foo\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_syn\")\n\n\t\tsyn, err := client.NewSynUpdateBuilder(ctx, \"idx_syn\", \"grp1\").Terms(\"a\", \"b\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(syn).To(Equal(\"OK\"))\n\t})\n\n\tIt(\"should test SearchBuilder with NoContent and Verbatim\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_nocontent\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText, Weight: 5}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_nocontent\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"title\", \"RediSearch\", \"body\", \"Redisearch implements a search engine on top of redis\")\n\n\t\tres, err := client.NewSearchBuilder(ctx, \"idx_nocontent\", \"search engine\").\n\t\t\tNoContent().\n\t\t\tVerbatim().\n\t\t\tLimit(0, 5).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(Equal(1))\n\t\tExpect(res.Docs[0].ID).To(Equal(\"doc1\"))\n\t\t// NoContent means no fields should be returned\n\t\tExpect(res.Docs[0].Fields).To(BeEmpty())\n\t})\n\n\tIt(\"should test SearchBuilder with NoStopWords\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_nostop\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_nostop\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"hello world\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"test document\")\n\n\t\t// Test that NoStopWords method can be called and search works\n\t\tres, err := client.NewSearchBuilder(ctx, \"idx_nostop\", \"hello\").NoContent().NoStopWords().Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(Equal(1))\n\t})\n\n\tIt(\"should test SearchBuilder with filters\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_filters\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"num\", FieldType: redis.SearchFieldTypeNumeric}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"loc\", FieldType: redis.SearchFieldTypeGeo}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_filters\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo bar\", \"num\", 3.141, \"loc\", \"-0.441,51.458\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"foo baz\", \"num\", 2, \"loc\", \"-0.1,51.2\")\n\n\t\t// Test numeric filter\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_filters\", \"foo\").\n\t\t\tFilter(\"num\", 2, 4).\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(2))\n\n\t\t// Test geo filter\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_filters\", \"foo\").\n\t\t\tGeoFilter(\"loc\", -0.44, 51.45, 10, \"km\").\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(1))\n\t})\n\n\tIt(\"should test SearchBuilder with sorting\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_sort\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"num\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_sort\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo bar\", \"num\", 1)\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"foo baz\", \"num\", 2)\n\t\tclient.HSet(ctx, \"doc3\", \"txt\", \"foo qux\", \"num\", 3)\n\n\t\t// Test ascending sort\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_sort\", \"foo\").\n\t\t\tSortBy(\"num\", true).\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(3))\n\t\tExpect(res1.Docs[0].ID).To(Equal(\"doc1\"))\n\t\tExpect(res1.Docs[1].ID).To(Equal(\"doc2\"))\n\t\tExpect(res1.Docs[2].ID).To(Equal(\"doc3\"))\n\n\t\t// Test descending sort\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_sort\", \"foo\").\n\t\t\tSortBy(\"num\", false).\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(3))\n\t\tExpect(res2.Docs[0].ID).To(Equal(\"doc3\"))\n\t\tExpect(res2.Docs[1].ID).To(Equal(\"doc2\"))\n\t\tExpect(res2.Docs[2].ID).To(Equal(\"doc1\"))\n\t})\n\n\tIt(\"should test SearchBuilder with InKeys and InFields\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_in\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_in\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"title\", \"hello world\", \"body\", \"lorem ipsum\")\n\t\tclient.HSet(ctx, \"doc2\", \"title\", \"foo bar\", \"body\", \"hello world\")\n\t\tclient.HSet(ctx, \"doc3\", \"title\", \"baz qux\", \"body\", \"dolor sit\")\n\n\t\t// Test InKeys\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_in\", \"hello\").\n\t\t\tInKeys(\"doc1\", \"doc2\").\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(2))\n\n\t\t// Test InFields\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_in\", \"hello\").\n\t\t\tInFields(\"title\").\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(1))\n\t\tExpect(res2.Docs[0].ID).To(Equal(\"doc1\"))\n\t})\n\n\tIt(\"should test SearchBuilder with Return fields\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_return\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"num\", FieldType: redis.SearchFieldTypeNumeric}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_return\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"title\", \"hello\", \"body\", \"world\", \"num\", 42)\n\n\t\t// Test ReturnFields\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_return\", \"hello\").\n\t\t\tReturnFields(\"title\", \"num\").\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(1))\n\t\tExpect(res1.Docs[0].Fields).To(HaveKey(\"title\"))\n\t\tExpect(res1.Docs[0].Fields).To(HaveKey(\"num\"))\n\t\tExpect(res1.Docs[0].Fields).NotTo(HaveKey(\"body\"))\n\n\t\t// Test ReturnAs\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_return\", \"hello\").\n\t\t\tReturnAs(\"title\", \"doc_title\").\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(1))\n\t\tExpect(res2.Docs[0].Fields).To(HaveKey(\"doc_title\"))\n\t\tExpect(res2.Docs[0].Fields).NotTo(HaveKey(\"title\"))\n\t})\n\n\tIt(\"should test SearchBuilder with advanced options\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_advanced\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_advanced\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"description\", \"The quick brown fox jumps over the lazy dog\")\n\t\tclient.HSet(ctx, \"doc2\", \"description\", \"Quick alice was beginning to get very tired of sitting by her quick sister on the bank\")\n\n\t\t// Test with scores and different scorers\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_advanced\", \"quick\").\n\t\t\tWithScores().\n\t\t\tScorer(\"TFIDF\").\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(2))\n\t\tfor _, doc := range res1.Docs {\n\t\t\tExpect(*doc.Score).To(BeNumerically(\">\", 0))\n\t\t}\n\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_advanced\", \"quick\").\n\t\t\tWithScores().\n\t\t\tPayload(\"test_payload\").\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(2))\n\n\t\t// Test with Slop and InOrder\n\t\tres3, err := client.NewSearchBuilder(ctx, \"idx_advanced\", \"quick brown\").\n\t\t\tSlop(1).\n\t\t\tInOrder().\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res3.Total).To(Equal(1))\n\n\t\t// Test with Language and Expander\n\t\tres4, err := client.NewSearchBuilder(ctx, \"idx_advanced\", \"quick\").\n\t\t\tLanguage(\"english\").\n\t\t\tExpander(\"SYNONYM\").\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res4.Total).To(BeNumerically(\">=\", 0))\n\n\t\t// Test with Timeout\n\t\tres5, err := client.NewSearchBuilder(ctx, \"idx_advanced\", \"quick\").\n\t\t\tTimeout(1000).\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res5.Total).To(Equal(2))\n\t})\n\n\tIt(\"should test SearchBuilder with Params and Dialect\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_params\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_params\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"name\", \"Alice\")\n\t\tclient.HSet(ctx, \"doc2\", \"name\", \"Bob\")\n\t\tclient.HSet(ctx, \"doc3\", \"name\", \"Carol\")\n\n\t\t// Test with single param\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_params\", \"@name:$name\").\n\t\t\tParam(\"name\", \"Alice\").\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(1))\n\t\tExpect(res1.Docs[0].ID).To(Equal(\"doc1\"))\n\n\t\t// Test with multiple params using ParamsMap\n\t\tparams := map[string]interface{}{\n\t\t\t\"name1\": \"Bob\",\n\t\t\t\"name2\": \"Carol\",\n\t\t}\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_params\", \"@name:($name1|$name2)\").\n\t\t\tParamsMap(params).\n\t\t\tDialect(2).\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(2))\n\t})\n\n\tIt(\"should test SearchBuilder with Limit and CountOnly\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_limit\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_limit\")\n\n\t\tfor i := 1; i <= 10; i++ {\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"txt\", \"test document\")\n\t\t}\n\n\t\t// Test with Limit\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_limit\", \"test\").\n\t\t\tLimit(2, 3).\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(10))\n\t\tExpect(len(res1.Docs)).To(Equal(3))\n\n\t\t// Test with CountOnly\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_limit\", \"test\").\n\t\t\tCountOnly().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(10))\n\t\tExpect(len(res2.Docs)).To(Equal(0))\n\t})\n\n\tIt(\"should test SearchBuilder with WithSortByCount and SortBy\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_payloads\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"num\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_payloads\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"hello\", \"num\", 1)\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"world\", \"num\", 2)\n\n\t\t// Test WithSortByCount and SortBy\n\t\tres, err := client.NewSearchBuilder(ctx, \"idx_payloads\", \"*\").\n\t\t\tSortBy(\"num\", true).\n\t\t\tWithSortByCount().\n\t\t\tNoContent().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(Equal(2))\n\t})\n\n\tIt(\"should test SearchBuilder with JSON\", Label(\"search\", \"ftsearch\", \"builders\", \"json\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_json\").\n\t\t\tOnJSON().\n\t\t\tPrefix(\"king:\").\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"$.name\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_json\")\n\n\t\tclient.JSONSet(ctx, \"king:1\", \"$\", `{\"name\": \"henry\"}`)\n\t\tclient.JSONSet(ctx, \"king:2\", \"$\", `{\"name\": \"james\"}`)\n\n\t\tres, err := client.NewSearchBuilder(ctx, \"idx_json\", \"henry\").Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(Equal(1))\n\t\tExpect(res.Docs[0].ID).To(Equal(\"king:1\"))\n\t\tExpect(res.Docs[0].Fields[\"$\"]).To(Equal(`{\"name\":\"henry\"}`))\n\t})\n\n\tIt(\"should test SearchBuilder with vector search\", Label(\"search\", \"ftsearch\", \"builders\", \"vector\"), func() {\n\t\thnswOptions := &redis.FTHNSWOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"}\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_vector\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_vector\")\n\n\t\tclient.HSet(ctx, \"a\", \"v\", \"aaaaaaaa\")\n\t\tclient.HSet(ctx, \"b\", \"v\", \"aaaabaaa\")\n\t\tclient.HSet(ctx, \"c\", \"v\", \"aaaaabaa\")\n\n\t\tres, err := client.NewSearchBuilder(ctx, \"idx_vector\", \"*=>[KNN 2 @v $vec]\").\n\t\t\tReturnFields(\"__v_score\").\n\t\t\tSortBy(\"__v_score\", true).\n\t\t\tDialect(2).\n\t\t\tParam(\"vec\", \"aaaaaaaa\").\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(Equal(\"a\"))\n\t\tExpect(res.Docs[0].Fields[\"__v_score\"]).To(Equal(\"0\"))\n\t})\n\n\tIt(\"should test SearchBuilder with complex filtering and aggregation\", Label(\"search\", \"ftsearch\", \"builders\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_complex\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"category\", FieldType: redis.SearchFieldTypeTag}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"price\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"location\", FieldType: redis.SearchFieldTypeGeo}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_complex\")\n\n\t\tclient.HSet(ctx, \"product1\", \"category\", \"electronics\", \"price\", 100, \"location\", \"-0.1,51.5\", \"description\", \"smartphone device\")\n\t\tclient.HSet(ctx, \"product2\", \"category\", \"electronics\", \"price\", 200, \"location\", \"-0.2,51.6\", \"description\", \"laptop computer\")\n\t\tclient.HSet(ctx, \"product3\", \"category\", \"books\", \"price\", 20, \"location\", \"-0.3,51.7\", \"description\", \"programming guide\")\n\n\t\tres, err := client.NewSearchBuilder(ctx, \"idx_complex\", \"@category:{electronics} @description:(device|computer)\").\n\t\t\tFilter(\"price\", 50, 250).\n\t\t\tGeoFilter(\"location\", -0.15, 51.55, 50, \"km\").\n\t\t\tSortBy(\"price\", true).\n\t\t\tReturnFields(\"category\", \"price\", \"description\").\n\t\t\tLimit(0, 10).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeNumerically(\">=\", 1))\n\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_complex\", \"@category:{$cat} @price:[$min $max]\").\n\t\t\tParamsMap(map[string]interface{}{\n\t\t\t\t\"cat\": \"electronics\",\n\t\t\t\t\"min\": 150,\n\t\t\t\t\"max\": 300,\n\t\t\t}).\n\t\t\tDialect(2).\n\t\t\tWithScores().\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(1))\n\t\tExpect(res2.Docs[0].ID).To(Equal(\"product2\"))\n\t})\n\n\tIt(\"should test SearchBuilder error handling and edge cases\", Label(\"search\", \"ftsearch\", \"builders\", \"edge-cases\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_edge\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_edge\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"hello world\")\n\n\t\t// Test empty query\n\t\tres1, err := client.NewSearchBuilder(ctx, \"idx_edge\", \"*\").NoContent().Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(Equal(1))\n\n\t\t// Test query with no results\n\t\tres2, err := client.NewSearchBuilder(ctx, \"idx_edge\", \"nonexistent\").NoContent().Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(Equal(0))\n\n\t\t// Test with multiple chained methods\n\t\tres3, err := client.NewSearchBuilder(ctx, \"idx_edge\", \"hello\").\n\t\t\tWithScores().\n\t\t\tNoContent().\n\t\t\tVerbatim().\n\t\t\tInOrder().\n\t\t\tSlop(0).\n\t\t\tTimeout(5000).\n\t\t\tLanguage(\"english\").\n\t\t\tDialect(2).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res3.Total).To(Equal(1))\n\t})\n\n\tIt(\"should test SearchBuilder method chaining\", Label(\"search\", \"ftsearch\", \"builders\", \"fluent\"), func() {\n\t\tcreateVal, err := client.NewCreateIndexBuilder(ctx, \"idx_fluent\").\n\t\t\tOnHash().\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"tags\", FieldType: redis.SearchFieldTypeTag}).\n\t\t\tSchema(&redis.FieldSchema{FieldName: \"score\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).\n\t\t\tRun()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(createVal).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_fluent\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"title\", \"Redis Search Tutorial\", \"tags\", \"redis,search,tutorial\", \"score\", 95)\n\t\tclient.HSet(ctx, \"doc2\", \"title\", \"Advanced Redis\", \"tags\", \"redis,advanced\", \"score\", 88)\n\n\t\tbuilder := client.NewSearchBuilder(ctx, \"idx_fluent\", \"@title:(redis) @tags:{search}\")\n\t\tresult := builder.\n\t\t\tWithScores().\n\t\t\tFilter(\"score\", 90, 100).\n\t\t\tSortBy(\"score\", false).\n\t\t\tReturnFields(\"title\", \"score\").\n\t\t\tLimit(0, 5).\n\t\t\tDialect(2).\n\t\t\tTimeout(1000).\n\t\t\tLanguage(\"english\")\n\n\t\tres, err := result.Run()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(Equal(1))\n\t\tExpect(res.Docs[0].ID).To(Equal(\"doc1\"))\n\t\tExpect(res.Docs[0].Fields[\"title\"]).To(Equal(\"Redis Search Tutorial\"))\n\t\tExpect(*res.Docs[0].Score).To(BeNumerically(\">\", 0))\n\t})\n})\n"
  },
  {
    "path": "search_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\ntype SearchCmdable interface {\n\tFT_List(ctx context.Context) *StringSliceCmd\n\tFTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd\n\tFTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd\n\tFTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd\n\tFTAliasDel(ctx context.Context, alias string) *StatusCmd\n\tFTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd\n\tFTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd\n\tFTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd\n\tFTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd\n\tFTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd\n\tFTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd\n\tFTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd\n\tFTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd\n\tFTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd\n\tFTDictDump(ctx context.Context, dict string) *StringSliceCmd\n\tFTDropIndex(ctx context.Context, index string) *StatusCmd\n\tFTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd\n\tFTExplain(ctx context.Context, index string, query string) *StringCmd\n\tFTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd\n\tFTHybrid(ctx context.Context, index string, searchExpr string, vectorField string, vectorData Vector) *FTHybridCmd\n\tFTHybridWithArgs(ctx context.Context, index string, options *FTHybridOptions) *FTHybridCmd\n\tFTInfo(ctx context.Context, index string) *FTInfoCmd\n\tFTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd\n\tFTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd\n\tFTSearch(ctx context.Context, index string, query string) *FTSearchCmd\n\tFTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd\n\tFTSynDump(ctx context.Context, index string) *FTSynDumpCmd\n\tFTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd\n\tFTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd\n\tFTTagVals(ctx context.Context, index string, field string) *StringSliceCmd\n}\n\ntype FTCreateOptions struct {\n\tOnHash          bool\n\tOnJSON          bool\n\tPrefix          []interface{}\n\tFilter          string\n\tDefaultLanguage string\n\tLanguageField   string\n\tScore           float64\n\tScoreField      string\n\tPayloadField    string\n\tMaxTextFields   int\n\tNoOffsets       bool\n\tTemporary       int\n\tNoHL            bool\n\tNoFields        bool\n\tNoFreqs         bool\n\tStopWords       []interface{}\n\tSkipInitialScan bool\n}\n\ntype FieldSchema struct {\n\tFieldName         string\n\tAs                string\n\tFieldType         SearchFieldType\n\tSortable          bool\n\tUNF               bool\n\tNoStem            bool\n\tNoIndex           bool\n\tPhoneticMatcher   string\n\tWeight            float64\n\tSeparator         string\n\tCaseSensitive     bool\n\tWithSuffixtrie    bool\n\tVectorArgs        *FTVectorArgs\n\tGeoShapeFieldType string\n\tIndexEmpty        bool\n\tIndexMissing      bool\n}\n\ntype FTVectorArgs struct {\n\tFlatOptions   *FTFlatOptions\n\tHNSWOptions   *FTHNSWOptions\n\tVamanaOptions *FTVamanaOptions\n}\n\ntype FTFlatOptions struct {\n\tType            string\n\tDim             int\n\tDistanceMetric  string\n\tInitialCapacity int\n\tBlockSize       int\n}\n\ntype FTHNSWOptions struct {\n\tType                   string\n\tDim                    int\n\tDistanceMetric         string\n\tInitialCapacity        int\n\tMaxEdgesPerNode        int\n\tMaxAllowedEdgesPerNode int\n\tEFRunTime              int\n\tEpsilon                float64\n}\n\ntype FTVamanaOptions struct {\n\tType                   string\n\tDim                    int\n\tDistanceMetric         string\n\tCompression            string\n\tConstructionWindowSize int\n\tGraphMaxDegree         int\n\tSearchWindowSize       int\n\tEpsilon                float64\n\tTrainingThreshold      int\n\tReduceDim              int\n}\n\ntype FTDropIndexOptions struct {\n\tDeleteDocs bool\n}\n\ntype SpellCheckTerms struct {\n\tInclude    bool\n\tExclude    bool\n\tDictionary string\n}\n\ntype FTExplainOptions struct {\n\t// Dialect 1,3 and 4 are deprecated since redis 8.0\n\tDialect string\n}\n\ntype FTSynUpdateOptions struct {\n\tSkipInitialScan bool\n}\n\ntype SearchAggregator int\n\nconst (\n\tSearchInvalid = SearchAggregator(iota)\n\tSearchAvg\n\tSearchSum\n\tSearchMin\n\tSearchMax\n\tSearchCount\n\tSearchCountDistinct\n\tSearchCountDistinctish\n\tSearchStdDev\n\tSearchQuantile\n\tSearchToList\n\tSearchFirstValue\n\tSearchRandomSample\n)\n\nfunc (a SearchAggregator) String() string {\n\tswitch a {\n\tcase SearchInvalid:\n\t\treturn \"\"\n\tcase SearchAvg:\n\t\treturn \"AVG\"\n\tcase SearchSum:\n\t\treturn \"SUM\"\n\tcase SearchMin:\n\t\treturn \"MIN\"\n\tcase SearchMax:\n\t\treturn \"MAX\"\n\tcase SearchCount:\n\t\treturn \"COUNT\"\n\tcase SearchCountDistinct:\n\t\treturn \"COUNT_DISTINCT\"\n\tcase SearchCountDistinctish:\n\t\treturn \"COUNT_DISTINCTISH\"\n\tcase SearchStdDev:\n\t\treturn \"STDDEV\"\n\tcase SearchQuantile:\n\t\treturn \"QUANTILE\"\n\tcase SearchToList:\n\t\treturn \"TOLIST\"\n\tcase SearchFirstValue:\n\t\treturn \"FIRST_VALUE\"\n\tcase SearchRandomSample:\n\t\treturn \"RANDOM_SAMPLE\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\ntype SearchFieldType int\n\nconst (\n\tSearchFieldTypeInvalid = SearchFieldType(iota)\n\tSearchFieldTypeNumeric\n\tSearchFieldTypeTag\n\tSearchFieldTypeText\n\tSearchFieldTypeGeo\n\tSearchFieldTypeVector\n\tSearchFieldTypeGeoShape\n)\n\nfunc (t SearchFieldType) String() string {\n\tswitch t {\n\tcase SearchFieldTypeInvalid:\n\t\treturn \"\"\n\tcase SearchFieldTypeNumeric:\n\t\treturn \"NUMERIC\"\n\tcase SearchFieldTypeTag:\n\t\treturn \"TAG\"\n\tcase SearchFieldTypeText:\n\t\treturn \"TEXT\"\n\tcase SearchFieldTypeGeo:\n\t\treturn \"GEO\"\n\tcase SearchFieldTypeVector:\n\t\treturn \"VECTOR\"\n\tcase SearchFieldTypeGeoShape:\n\t\treturn \"GEOSHAPE\"\n\tdefault:\n\t\treturn \"TEXT\"\n\t}\n}\n\n// Each AggregateReducer have different args.\n// Please follow https://redis.io/docs/interact/search-and-query/search/aggregations/#supported-groupby-reducers for more information.\ntype FTAggregateReducer struct {\n\tReducer SearchAggregator\n\tArgs    []interface{}\n\tAs      string\n}\n\ntype FTAggregateGroupBy struct {\n\tFields []interface{}\n\tReduce []FTAggregateReducer\n}\n\ntype FTAggregateSortBy struct {\n\tFieldName string\n\tAsc       bool\n\tDesc      bool\n}\n\ntype FTAggregateApply struct {\n\tField string\n\tAs    string\n}\n\ntype FTAggregateLoad struct {\n\tField string\n\tAs    string\n}\n\ntype FTAggregateWithCursor struct {\n\tCount   int\n\tMaxIdle int\n}\n\ntype FTAggregateOptions struct {\n\tVerbatim  bool\n\tLoadAll   bool\n\tLoad      []FTAggregateLoad\n\tTimeout   int\n\tGroupBy   []FTAggregateGroupBy\n\tSortBy    []FTAggregateSortBy\n\tSortByMax int\n\t// Scorer is used to set scoring function, if not set passed, a default will be used.\n\t// The default scorer depends on the Redis version:\n\t// - `BM25` for Redis >= 8\n\t// - `TFIDF` for Redis < 8\n\tScorer string\n\t// AddScores is available in Redis CE 8\n\tAddScores         bool\n\tApply             []FTAggregateApply\n\tLimitOffset       int\n\tLimit             int\n\tFilter            string\n\tWithCursor        bool\n\tWithCursorOptions *FTAggregateWithCursor\n\tParams            map[string]interface{}\n\t// Dialect 1,3 and 4 are deprecated since redis 8.0\n\tDialectVersion int\n}\n\ntype FTSearchFilter struct {\n\tFieldName interface{}\n\tMin       interface{}\n\tMax       interface{}\n}\n\ntype FTSearchGeoFilter struct {\n\tFieldName string\n\tLongitude float64\n\tLatitude  float64\n\tRadius    float64\n\tUnit      string\n}\n\ntype FTSearchReturn struct {\n\tFieldName string\n\tAs        string\n}\n\ntype FTSearchSortBy struct {\n\tFieldName string\n\tAsc       bool\n\tDesc      bool\n}\n\n// FTSearchOptions hold options that can be passed to the FT.SEARCH command.\n// More information about the options can be found\n// in the documentation for FT.SEARCH https://redis.io/docs/latest/commands/ft.search/\ntype FTSearchOptions struct {\n\tNoContent    bool\n\tVerbatim     bool\n\tNoStopWords  bool\n\tWithScores   bool\n\tWithPayloads bool\n\tWithSortKeys bool\n\tFilters      []FTSearchFilter\n\tGeoFilter    []FTSearchGeoFilter\n\tInKeys       []interface{}\n\tInFields     []interface{}\n\tReturn       []FTSearchReturn\n\tSlop         int\n\tTimeout      int\n\tInOrder      bool\n\tLanguage     string\n\tExpander     string\n\t// Scorer is used to set scoring function, if not set passed, a default will be used.\n\t// The default scorer depends on the Redis version:\n\t// - `BM25` for Redis >= 8\n\t// - `TFIDF` for Redis < 8\n\tScorer          string\n\tExplainScore    bool\n\tPayload         string\n\tSortBy          []FTSearchSortBy\n\tSortByWithCount bool\n\tLimitOffset     int\n\tLimit           int\n\t// CountOnly sets LIMIT 0 0 to get the count - number of documents in the result set without actually returning the result set.\n\t// When using this option, the Limit and LimitOffset options are ignored.\n\tCountOnly bool\n\tParams    map[string]interface{}\n\t// Dialect 1,3 and 4 are deprecated since redis 8.0\n\tDialectVersion int\n}\n\n// FTHybridCombineMethod represents the fusion method for combining search and vector results\ntype FTHybridCombineMethod string\n\nconst (\n\tFTHybridCombineRRF      FTHybridCombineMethod = \"RRF\"\n\tFTHybridCombineLinear   FTHybridCombineMethod = \"LINEAR\"\n\tFTHybridCombineFunction FTHybridCombineMethod = \"FUNCTION\"\n)\n\n// FTHybridSearchExpression represents a search expression in hybrid search\ntype FTHybridSearchExpression struct {\n\tQuery        string\n\tScorer       string\n\tScorerParams []interface{}\n\tYieldScoreAs string\n}\n\ntype FTHybridVectorMethod = string\n\nconst (\n\tKNN   FTHybridCombineMethod = \"KNN\"\n\tRANGE FTHybridCombineMethod = \"RANGE\"\n)\n\n// FTHybridVectorExpression represents a vector expression in hybrid search\ntype FTHybridVectorExpression struct {\n\tVectorField string\n\tVectorData  Vector\n\t// VectorParamName specifies the parameter name for passing vector data via PARAMS mechanism.\n\t// REQUIRED for Redis 8.6+ (inline vector blobs are not supported in 8.6+).\n\t// Optional for Redis 8.4-8.5 (both inline and PARAMS are supported).\n\t// When set, the vector blob will be passed as: VSIM @field $VectorParamName PARAMS ... VectorParamName <blob>\n\t// When empty, the vector blob will be inlined: VSIM @field <blob> (fails on Redis 8.6+)\n\tVectorParamName string\n\tMethod          FTHybridVectorMethod\n\tMethodParams    []interface{}\n\tFilter          string\n\tYieldScoreAs    string\n}\n\n// FTHybridCombineOptions represents options for result fusion\ntype FTHybridCombineOptions struct {\n\tMethod       FTHybridCombineMethod\n\tCount        int\n\tWindow       int     // For RRF\n\tConstant     float64 // For RRF\n\tAlpha        float64 // For LINEAR\n\tBeta         float64 // For LINEAR\n\tYieldScoreAs string\n}\n\n// FTHybridGroupBy represents GROUP BY functionality\ntype FTHybridGroupBy struct {\n\tCount        int\n\tFields       []string\n\tReduceFunc   string\n\tReduceCount  int\n\tReduceParams []interface{}\n}\n\n// FTHybridApply represents APPLY functionality\ntype FTHybridApply struct {\n\tExpression string\n\tAsField    string\n}\n\n// FTHybridWithCursor represents cursor configuration for hybrid search\ntype FTHybridWithCursor struct {\n\tCount   int // Number of results to return per cursor read\n\tMaxIdle int // Maximum idle time in milliseconds before cursor is automatically deleted\n}\n\n// FTHybridOptions hold options that can be passed to the FT.HYBRID command\ntype FTHybridOptions struct {\n\tCountExpressions  int                        // Number of search/vector expressions\n\tSearchExpressions []FTHybridSearchExpression // Multiple search expressions\n\tVectorExpressions []FTHybridVectorExpression // Multiple vector expressions\n\tCombine           *FTHybridCombineOptions    // Fusion step options\n\tLoad              []string                   // Projected fields\n\tGroupBy           *FTHybridGroupBy           // Aggregation grouping\n\tApply             []FTHybridApply            // Field transformations\n\tSortBy            []FTSearchSortBy           // Reuse from FTSearch\n\tFilter            string                     // Post-filter expression\n\tLimitOffset       int                        // Result limiting\n\tLimit             int\n\tParams            map[string]interface{} // Parameter substitution\n\tExplainScore      bool                   // Include score explanations\n\tTimeout           int                    // Runtime timeout\n\tWithCursor        bool                   // Enable cursor support for large result sets\n\tWithCursorOptions *FTHybridWithCursor    // Cursor configuration options\n}\n\ntype FTSynDumpResult struct {\n\tTerm     string\n\tSynonyms []string\n}\n\ntype FTSynDumpCmd struct {\n\tbaseCmd\n\tval []FTSynDumpResult\n}\n\ntype FTAggregateResult struct {\n\tTotal int\n\tRows  []AggregateRow\n}\n\ntype AggregateRow struct {\n\tFields map[string]interface{}\n}\n\ntype AggregateCmd struct {\n\tbaseCmd\n\tval *FTAggregateResult\n}\n\ntype FTInfoResult struct {\n\tIndexErrors              IndexErrors\n\tAttributes               []FTAttribute\n\tBytesPerRecordAvg        string\n\tCleaning                 int\n\tCursorStats              CursorStats\n\tDialectStats             map[string]int\n\tDocTableSizeMB           float64\n\tFieldStatistics          []FieldStatistic\n\tGCStats                  GCStats\n\tGeoshapesSzMB            float64\n\tHashIndexingFailures     int\n\tIndexDefinition          IndexDefinition\n\tIndexName                string\n\tIndexOptions             []string\n\tIndexing                 int\n\tInvertedSzMB             float64\n\tKeyTableSizeMB           float64\n\tMaxDocID                 int\n\tNumDocs                  int\n\tNumRecords               int\n\tNumTerms                 int\n\tNumberOfUses             int\n\tOffsetBitsPerRecordAvg   string\n\tOffsetVectorsSzMB        float64\n\tOffsetsPerTermAvg        string\n\tPercentIndexed           float64\n\tRecordsPerDocAvg         string\n\tSortableValuesSizeMB     float64\n\tTagOverheadSzMB          float64\n\tTextOverheadSzMB         float64\n\tTotalIndexMemorySzMB     float64\n\tTotalIndexingTime        int\n\tTotalInvertedIndexBlocks int\n\tVectorIndexSzMB          float64\n}\n\ntype IndexErrors struct {\n\tIndexingFailures     int\n\tLastIndexingError    string\n\tLastIndexingErrorKey string\n}\n\ntype FTAttribute struct {\n\tIdentifier      string\n\tAttribute       string\n\tType            string\n\tWeight          float64\n\tSortable        bool\n\tNoStem          bool\n\tNoIndex         bool\n\tUNF             bool\n\tPhoneticMatcher string\n\tCaseSensitive   bool\n\tWithSuffixtrie  bool\n\n\t// Vector specific attributes\n\tAlgorithm      string\n\tDataType       string\n\tDim            int\n\tDistanceMetric string\n\tM              int\n\tEFConstruction int\n}\n\ntype CursorStats struct {\n\tGlobalIdle    int\n\tGlobalTotal   int\n\tIndexCapacity int\n\tIndexTotal    int\n}\n\ntype FieldStatistic struct {\n\tIdentifier  string\n\tAttribute   string\n\tIndexErrors IndexErrors\n}\n\ntype GCStats struct {\n\tBytesCollected       int\n\tTotalMsRun           int\n\tTotalCycles          int\n\tAverageCycleTimeMs   string\n\tLastRunTimeMs        int\n\tGCNumericTreesMissed int\n\tGCBlocksDenied       int\n}\n\ntype IndexDefinition struct {\n\tKeyType      string\n\tPrefixes     []string\n\tDefaultScore float64\n}\n\ntype FTSpellCheckOptions struct {\n\tDistance int\n\tTerms    *FTSpellCheckTerms\n\t// Dialect 1,3 and 4 are deprecated since redis 8.0\n\tDialect int\n}\n\ntype FTSpellCheckTerms struct {\n\tInclusion  string // Either \"INCLUDE\" or \"EXCLUDE\"\n\tDictionary string\n\tTerms      []interface{}\n}\n\ntype SpellCheckResult struct {\n\tTerm        string\n\tSuggestions []SpellCheckSuggestion\n}\n\ntype SpellCheckSuggestion struct {\n\tScore      float64\n\tSuggestion string\n}\n\ntype FTSearchResult struct {\n\tTotal int\n\tDocs  []Document\n}\n\ntype Document struct {\n\tID      string\n\tScore   *float64\n\tPayload *string\n\tSortKey *string\n\tFields  map[string]string\n\tError   error\n}\n\ntype AggregateQuery []interface{}\n\n// FT_List - Lists all the existing indexes in the database.\n// For more information, please refer to the Redis documentation:\n// [FT._LIST]: (https://redis.io/commands/ft._list/)\nfunc (c cmdable) FT_List(ctx context.Context) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"FT._LIST\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTAggregate - Performs a search query on an index and applies a series of aggregate transformations to the result.\n// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.\n// For more information, please refer to the Redis documentation:\n// [FT.AGGREGATE]: (https://redis.io/commands/ft.aggregate/)\nfunc (c cmdable) FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd {\n\targs := []interface{}{\"FT.AGGREGATE\", index, query}\n\tcmd := NewMapStringInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery, error) {\n\tqueryArgs := []interface{}{query}\n\tif options != nil {\n\t\tif options.Verbatim {\n\t\t\tqueryArgs = append(queryArgs, \"VERBATIM\")\n\t\t}\n\n\t\tif options.Scorer != \"\" {\n\t\t\tqueryArgs = append(queryArgs, \"SCORER\", options.Scorer)\n\t\t}\n\n\t\tif options.AddScores {\n\t\t\tqueryArgs = append(queryArgs, \"ADDSCORES\")\n\t\t}\n\n\t\tif options.LoadAll && options.Load != nil {\n\t\t\treturn nil, fmt.Errorf(\"FT.AGGREGATE: LOADALL and LOAD are mutually exclusive\")\n\t\t}\n\t\tif options.LoadAll {\n\t\t\tqueryArgs = append(queryArgs, \"LOAD\", \"*\")\n\t\t}\n\t\tif options.Load != nil {\n\t\t\tqueryArgs = append(queryArgs, \"LOAD\", len(options.Load))\n\t\t\tindex, count := len(queryArgs)-1, 0\n\t\t\tfor _, load := range options.Load {\n\t\t\t\tqueryArgs = append(queryArgs, load.Field)\n\t\t\t\tcount++\n\t\t\t\tif load.As != \"\" {\n\t\t\t\t\tqueryArgs = append(queryArgs, \"AS\", load.As)\n\t\t\t\t\tcount += 2\n\t\t\t\t}\n\t\t\t}\n\t\t\tqueryArgs[index] = count\n\t\t}\n\n\t\tif options.Timeout > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"TIMEOUT\", options.Timeout)\n\t\t}\n\n\t\tfor _, apply := range options.Apply {\n\t\t\tqueryArgs = append(queryArgs, \"APPLY\", apply.Field)\n\t\t\tif apply.As != \"\" {\n\t\t\t\tqueryArgs = append(queryArgs, \"AS\", apply.As)\n\t\t\t}\n\t\t}\n\n\t\tif options.GroupBy != nil {\n\t\t\tfor _, groupBy := range options.GroupBy {\n\t\t\t\tqueryArgs = append(queryArgs, \"GROUPBY\", len(groupBy.Fields))\n\t\t\t\tqueryArgs = append(queryArgs, groupBy.Fields...)\n\n\t\t\t\tfor _, reducer := range groupBy.Reduce {\n\t\t\t\t\tqueryArgs = append(queryArgs, \"REDUCE\")\n\t\t\t\t\tqueryArgs = append(queryArgs, reducer.Reducer.String())\n\t\t\t\t\tif reducer.Args != nil {\n\t\t\t\t\t\tqueryArgs = append(queryArgs, len(reducer.Args))\n\t\t\t\t\t\tqueryArgs = append(queryArgs, reducer.Args...)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tqueryArgs = append(queryArgs, 0)\n\t\t\t\t\t}\n\t\t\t\t\tif reducer.As != \"\" {\n\t\t\t\t\t\tqueryArgs = append(queryArgs, \"AS\", reducer.As)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif options.SortBy != nil {\n\t\t\tqueryArgs = append(queryArgs, \"SORTBY\")\n\t\t\tsortByOptions := []interface{}{}\n\t\t\tfor _, sortBy := range options.SortBy {\n\t\t\t\tsortByOptions = append(sortByOptions, sortBy.FieldName)\n\t\t\t\tif sortBy.Asc && sortBy.Desc {\n\t\t\t\t\treturn nil, fmt.Errorf(\"FT.AGGREGATE: ASC and DESC are mutually exclusive\")\n\t\t\t\t}\n\t\t\t\tif sortBy.Asc {\n\t\t\t\t\tsortByOptions = append(sortByOptions, \"ASC\")\n\t\t\t\t}\n\t\t\t\tif sortBy.Desc {\n\t\t\t\t\tsortByOptions = append(sortByOptions, \"DESC\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tqueryArgs = append(queryArgs, len(sortByOptions))\n\t\t\tqueryArgs = append(queryArgs, sortByOptions...)\n\t\t}\n\t\tif options.SortByMax > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"MAX\", options.SortByMax)\n\t\t}\n\t\tif options.LimitOffset >= 0 && options.Limit > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"LIMIT\", options.LimitOffset, options.Limit)\n\t\t}\n\t\tif options.Filter != \"\" {\n\t\t\tqueryArgs = append(queryArgs, \"FILTER\", options.Filter)\n\t\t}\n\t\tif options.WithCursor {\n\t\t\tqueryArgs = append(queryArgs, \"WITHCURSOR\")\n\t\t\tif options.WithCursorOptions != nil {\n\t\t\t\tif options.WithCursorOptions.Count > 0 {\n\t\t\t\t\tqueryArgs = append(queryArgs, \"COUNT\", options.WithCursorOptions.Count)\n\t\t\t\t}\n\t\t\t\tif options.WithCursorOptions.MaxIdle > 0 {\n\t\t\t\t\tqueryArgs = append(queryArgs, \"MAXIDLE\", options.WithCursorOptions.MaxIdle)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif options.Params != nil {\n\t\t\tqueryArgs = append(queryArgs, \"PARAMS\", len(options.Params)*2)\n\t\t\tfor key, value := range options.Params {\n\t\t\t\tqueryArgs = append(queryArgs, key, value)\n\t\t\t}\n\t\t}\n\n\t\tif options.DialectVersion > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"DIALECT\", options.DialectVersion)\n\t\t} else {\n\t\t\tqueryArgs = append(queryArgs, \"DIALECT\", 2)\n\t\t}\n\t}\n\treturn queryArgs, nil\n}\n\nfunc ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) {\n\tif len(data) == 0 {\n\t\treturn nil, fmt.Errorf(\"no data returned\")\n\t}\n\n\ttotal, ok := data[0].(int64)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid total format\")\n\t}\n\n\trows := make([]AggregateRow, 0, len(data)-1)\n\tfor _, row := range data[1:] {\n\t\tfields, ok := row.([]interface{})\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"invalid row format\")\n\t\t}\n\n\t\trowMap := make(map[string]interface{})\n\t\tfor i := 0; i < len(fields); i += 2 {\n\t\t\tkey, ok := fields[i].(string)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid field key format\")\n\t\t\t}\n\t\t\tvalue := fields[i+1]\n\t\t\trowMap[key] = value\n\t\t}\n\t\trows = append(rows, AggregateRow{Fields: rowMap})\n\t}\n\n\tresult := &FTAggregateResult{\n\t\tTotal: int(total),\n\t\tRows:  rows,\n\t}\n\treturn result, nil\n}\n\nfunc NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd {\n\treturn &AggregateCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeAggregate,\n\t\t},\n\t}\n}\n\nfunc (cmd *AggregateCmd) SetVal(val *FTAggregateResult) {\n\tcmd.val = val\n}\n\nfunc (cmd *AggregateCmd) Val() *FTAggregateResult {\n\treturn cmd.val\n}\n\nfunc (cmd *AggregateCmd) Result() (*FTAggregateResult, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *AggregateCmd) RawVal() interface{} {\n\treturn cmd.rawVal\n}\n\nfunc (cmd *AggregateCmd) RawResult() (interface{}, error) {\n\treturn cmd.rawVal, cmd.err\n}\n\nfunc (cmd *AggregateCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) {\n\tdata, err := rd.ReadSlice()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val, err = ProcessAggregateResult(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (cmd *AggregateCmd) Clone() Cmder {\n\tvar val *FTAggregateResult\n\tif cmd.val != nil {\n\t\tval = &FTAggregateResult{\n\t\t\tTotal: cmd.val.Total,\n\t\t}\n\t\tif cmd.val.Rows != nil {\n\t\t\tval.Rows = make([]AggregateRow, len(cmd.val.Rows))\n\t\t\tfor i, row := range cmd.val.Rows {\n\t\t\t\tval.Rows[i] = AggregateRow{}\n\t\t\t\tif row.Fields != nil {\n\t\t\t\t\tval.Rows[i].Fields = make(map[string]interface{}, len(row.Fields))\n\t\t\t\t\tfor k, v := range row.Fields {\n\t\t\t\t\t\tval.Rows[i].Fields[k] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn &AggregateCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result.\n// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.\n// This function also allows for specifying additional options such as: Verbatim, LoadAll, Load, Timeout, GroupBy, SortBy, SortByMax, Apply, LimitOffset, Limit, Filter, WithCursor, Params, and DialectVersion.\n// For more information, please refer to the Redis documentation:\n// [FT.AGGREGATE]: (https://redis.io/commands/ft.aggregate/)\nfunc (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd {\n\targs := []interface{}{\"FT.AGGREGATE\", index, query}\n\tif options != nil {\n\t\tif options.Verbatim {\n\t\t\targs = append(args, \"VERBATIM\")\n\t\t}\n\t\tif options.Scorer != \"\" {\n\t\t\targs = append(args, \"SCORER\", options.Scorer)\n\t\t}\n\t\tif options.AddScores {\n\t\t\targs = append(args, \"ADDSCORES\")\n\t\t}\n\t\tif options.LoadAll && options.Load != nil {\n\t\t\tcmd := NewAggregateCmd(ctx, args...)\n\t\t\tcmd.SetErr(fmt.Errorf(\"FT.AGGREGATE: LOADALL and LOAD are mutually exclusive\"))\n\t\t\treturn cmd\n\t\t}\n\t\tif options.LoadAll {\n\t\t\targs = append(args, \"LOAD\", \"*\")\n\t\t}\n\t\tif options.Load != nil {\n\t\t\targs = append(args, \"LOAD\", len(options.Load))\n\t\t\tindex, count := len(args)-1, 0\n\t\t\tfor _, load := range options.Load {\n\t\t\t\targs = append(args, load.Field)\n\t\t\t\tcount++\n\t\t\t\tif load.As != \"\" {\n\t\t\t\t\targs = append(args, \"AS\", load.As)\n\t\t\t\t\tcount += 2\n\t\t\t\t}\n\t\t\t}\n\t\t\targs[index] = count\n\t\t}\n\t\tif options.Timeout > 0 {\n\t\t\targs = append(args, \"TIMEOUT\", options.Timeout)\n\t\t}\n\t\tfor _, apply := range options.Apply {\n\t\t\targs = append(args, \"APPLY\", apply.Field)\n\t\t\tif apply.As != \"\" {\n\t\t\t\targs = append(args, \"AS\", apply.As)\n\t\t\t}\n\t\t}\n\t\tif options.GroupBy != nil {\n\t\t\tfor _, groupBy := range options.GroupBy {\n\t\t\t\targs = append(args, \"GROUPBY\", len(groupBy.Fields))\n\t\t\t\targs = append(args, groupBy.Fields...)\n\n\t\t\t\tfor _, reducer := range groupBy.Reduce {\n\t\t\t\t\targs = append(args, \"REDUCE\")\n\t\t\t\t\targs = append(args, reducer.Reducer.String())\n\t\t\t\t\tif reducer.Args != nil {\n\t\t\t\t\t\targs = append(args, len(reducer.Args))\n\t\t\t\t\t\targs = append(args, reducer.Args...)\n\t\t\t\t\t} else {\n\t\t\t\t\t\targs = append(args, 0)\n\t\t\t\t\t}\n\t\t\t\t\tif reducer.As != \"\" {\n\t\t\t\t\t\targs = append(args, \"AS\", reducer.As)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif options.SortBy != nil {\n\t\t\targs = append(args, \"SORTBY\")\n\t\t\tsortByOptions := []interface{}{}\n\t\t\tfor _, sortBy := range options.SortBy {\n\t\t\t\tsortByOptions = append(sortByOptions, sortBy.FieldName)\n\t\t\t\tif sortBy.Asc && sortBy.Desc {\n\t\t\t\t\tcmd := NewAggregateCmd(ctx, args...)\n\t\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.AGGREGATE: ASC and DESC are mutually exclusive\"))\n\t\t\t\t\treturn cmd\n\t\t\t\t}\n\t\t\t\tif sortBy.Asc {\n\t\t\t\t\tsortByOptions = append(sortByOptions, \"ASC\")\n\t\t\t\t}\n\t\t\t\tif sortBy.Desc {\n\t\t\t\t\tsortByOptions = append(sortByOptions, \"DESC\")\n\t\t\t\t}\n\t\t\t}\n\t\t\targs = append(args, len(sortByOptions))\n\t\t\targs = append(args, sortByOptions...)\n\t\t}\n\t\tif options.SortByMax > 0 {\n\t\t\targs = append(args, \"MAX\", options.SortByMax)\n\t\t}\n\t\tif options.LimitOffset >= 0 && options.Limit > 0 {\n\t\t\targs = append(args, \"LIMIT\", options.LimitOffset, options.Limit)\n\t\t}\n\t\tif options.Filter != \"\" {\n\t\t\targs = append(args, \"FILTER\", options.Filter)\n\t\t}\n\t\tif options.WithCursor {\n\t\t\targs = append(args, \"WITHCURSOR\")\n\t\t\tif options.WithCursorOptions != nil {\n\t\t\t\tif options.WithCursorOptions.Count > 0 {\n\t\t\t\t\targs = append(args, \"COUNT\", options.WithCursorOptions.Count)\n\t\t\t\t}\n\t\t\t\tif options.WithCursorOptions.MaxIdle > 0 {\n\t\t\t\t\targs = append(args, \"MAXIDLE\", options.WithCursorOptions.MaxIdle)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif options.Params != nil {\n\t\t\targs = append(args, \"PARAMS\", len(options.Params)*2)\n\t\t\tfor key, value := range options.Params {\n\t\t\t\targs = append(args, key, value)\n\t\t\t}\n\t\t}\n\t\tif options.DialectVersion > 0 {\n\t\t\targs = append(args, \"DIALECT\", options.DialectVersion)\n\t\t} else {\n\t\t\targs = append(args, \"DIALECT\", 2)\n\t\t}\n\t}\n\n\tcmd := NewAggregateCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTAliasAdd - Adds an alias to an index.\n// The 'index' parameter specifies the index to which the alias is added, and the 'alias' parameter specifies the alias.\n// For more information, please refer to the Redis documentation:\n// [FT.ALIASADD]: (https://redis.io/commands/ft.aliasadd/)\nfunc (c cmdable) FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd {\n\targs := []interface{}{\"FT.ALIASADD\", alias, index}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTAliasDel - Removes an alias from an index.\n// The 'alias' parameter specifies the alias to be removed.\n// For more information, please refer to the Redis documentation:\n// [FT.ALIASDEL]: (https://redis.io/commands/ft.aliasdel/)\nfunc (c cmdable) FTAliasDel(ctx context.Context, alias string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"FT.ALIASDEL\", alias)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTAliasUpdate - Updates an alias to an index.\n// The 'index' parameter specifies the index to which the alias is updated, and the 'alias' parameter specifies the alias.\n// If the alias already exists for a different index, it updates the alias to point to the specified index instead.\n// For more information, please refer to the Redis documentation:\n// [FT.ALIASUPDATE]: (https://redis.io/commands/ft.aliasupdate/)\nfunc (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"FT.ALIASUPDATE\", alias, index)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTAlter - Alters the definition of an existing index.\n// The 'index' parameter specifies the index to alter, and the 'skipInitialScan' parameter specifies whether to skip the initial scan.\n// The 'definition' parameter specifies the new definition for the index.\n// For more information, please refer to the Redis documentation:\n// [FT.ALTER]: (https://redis.io/commands/ft.alter/)\nfunc (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd {\n\targs := []interface{}{\"FT.ALTER\", index}\n\tif skipInitialScan {\n\t\targs = append(args, \"SKIPINITIALSCAN\")\n\t}\n\targs = append(args, \"SCHEMA\", \"ADD\")\n\targs = append(args, definition...)\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Retrieves the value of a RediSearch configuration parameter.\n// The 'option' parameter specifies the configuration parameter to retrieve.\n// For more information, please refer to the Redis [FT.CONFIG GET] documentation.\n//\n// Deprecated: FTConfigGet is deprecated in Redis 8.\n// All configuration will be done with the CONFIG GET command.\n// For more information check [Client.ConfigGet] and [CONFIG GET Documentation]\n//\n// [CONFIG GET Documentation]: https://redis.io/commands/config-get/\n// [FT.CONFIG GET]: https://redis.io/commands/ft.config-get/\nfunc (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd {\n\tcmd := NewMapMapStringInterfaceCmd(ctx, \"FT.CONFIG\", \"GET\", option)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Sets the value of a RediSearch configuration parameter.\n// The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value.\n// For more information, please refer to the Redis [FT.CONFIG SET] documentation.\n//\n// Deprecated: FTConfigSet is deprecated in Redis 8.\n// All configuration will be done with the CONFIG SET command.\n// For more information check [Client.ConfigSet] and [CONFIG SET Documentation]\n//\n// [CONFIG SET Documentation]: https://redis.io/commands/config-set/\n// [FT.CONFIG SET]: https://redis.io/commands/ft.config-set/\nfunc (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"FT.CONFIG\", \"SET\", option, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTCreate - Creates a new index with the given options and schema.\n// The 'index' parameter specifies the name of the index to create.\n// The 'options' parameter specifies various options for the index, such as:\n// whether to index hashes or JSONs, prefixes, filters, default language, score, score field, payload field, etc.\n// The 'schema' parameter specifies the schema for the index, which includes the field name, field type, etc.\n// For more information, please refer to the Redis documentation:\n// [FT.CREATE]: (https://redis.io/commands/ft.create/)\nfunc (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd {\n\targs := []interface{}{\"FT.CREATE\", index}\n\tif options != nil {\n\t\tif options.OnHash && !options.OnJSON {\n\t\t\targs = append(args, \"ON\", \"HASH\")\n\t\t}\n\t\tif options.OnJSON && !options.OnHash {\n\t\t\targs = append(args, \"ON\", \"JSON\")\n\t\t}\n\t\tif options.OnHash && options.OnJSON {\n\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: ON HASH and ON JSON are mutually exclusive\"))\n\t\t\treturn cmd\n\t\t}\n\t\tif options.Prefix != nil {\n\t\t\targs = append(args, \"PREFIX\", len(options.Prefix))\n\t\t\targs = append(args, options.Prefix...)\n\t\t}\n\t\tif options.Filter != \"\" {\n\t\t\targs = append(args, \"FILTER\", options.Filter)\n\t\t}\n\t\tif options.DefaultLanguage != \"\" {\n\t\t\targs = append(args, \"LANGUAGE\", options.DefaultLanguage)\n\t\t}\n\t\tif options.LanguageField != \"\" {\n\t\t\targs = append(args, \"LANGUAGE_FIELD\", options.LanguageField)\n\t\t}\n\t\tif options.Score > 0 {\n\t\t\targs = append(args, \"SCORE\", options.Score)\n\t\t}\n\t\tif options.ScoreField != \"\" {\n\t\t\targs = append(args, \"SCORE_FIELD\", options.ScoreField)\n\t\t}\n\t\tif options.PayloadField != \"\" {\n\t\t\targs = append(args, \"PAYLOAD_FIELD\", options.PayloadField)\n\t\t}\n\t\tif options.MaxTextFields > 0 {\n\t\t\targs = append(args, \"MAXTEXTFIELDS\", options.MaxTextFields)\n\t\t}\n\t\tif options.NoOffsets {\n\t\t\targs = append(args, \"NOOFFSETS\")\n\t\t}\n\t\tif options.Temporary > 0 {\n\t\t\targs = append(args, \"TEMPORARY\", options.Temporary)\n\t\t}\n\t\tif options.NoHL {\n\t\t\targs = append(args, \"NOHL\")\n\t\t}\n\t\tif options.NoFields {\n\t\t\targs = append(args, \"NOFIELDS\")\n\t\t}\n\t\tif options.NoFreqs {\n\t\t\targs = append(args, \"NOFREQS\")\n\t\t}\n\t\tif options.StopWords != nil {\n\t\t\targs = append(args, \"STOPWORDS\", len(options.StopWords))\n\t\t\targs = append(args, options.StopWords...)\n\t\t}\n\t\tif options.SkipInitialScan {\n\t\t\targs = append(args, \"SKIPINITIALSCAN\")\n\t\t}\n\t}\n\tif schema == nil {\n\t\tcmd := NewStatusCmd(ctx, args...)\n\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: SCHEMA is required\"))\n\t\treturn cmd\n\t}\n\targs = append(args, \"SCHEMA\")\n\tfor _, schema := range schema {\n\t\tif schema.FieldName == \"\" || schema.FieldType == SearchFieldTypeInvalid {\n\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: SCHEMA FieldName and FieldType are required\"))\n\t\t\treturn cmd\n\t\t}\n\t\targs = append(args, schema.FieldName)\n\t\tif schema.As != \"\" {\n\t\t\targs = append(args, \"AS\", schema.As)\n\t\t}\n\t\targs = append(args, schema.FieldType.String())\n\t\tif schema.VectorArgs != nil {\n\t\t\tif schema.FieldType != SearchFieldTypeVector {\n\t\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs\"))\n\t\t\t\treturn cmd\n\t\t\t}\n\t\t\t// Check mutual exclusivity of vector options\n\t\t\toptionCount := 0\n\t\t\tif schema.VectorArgs.FlatOptions != nil {\n\t\t\t\toptionCount++\n\t\t\t}\n\t\t\tif schema.VectorArgs.HNSWOptions != nil {\n\t\t\t\toptionCount++\n\t\t\t}\n\t\t\tif schema.VectorArgs.VamanaOptions != nil {\n\t\t\t\toptionCount++\n\t\t\t}\n\t\t\tif optionCount != 1 {\n\t\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: SCHEMA VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions\"))\n\t\t\t\treturn cmd\n\t\t\t}\n\t\t\tif schema.VectorArgs.FlatOptions != nil {\n\t\t\t\targs = append(args, \"FLAT\")\n\t\t\t\tif schema.VectorArgs.FlatOptions.Type == \"\" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == \"\" {\n\t\t\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT\"))\n\t\t\t\t\treturn cmd\n\t\t\t\t}\n\t\t\t\tflatArgs := []interface{}{\n\t\t\t\t\t\"TYPE\", schema.VectorArgs.FlatOptions.Type,\n\t\t\t\t\t\"DIM\", schema.VectorArgs.FlatOptions.Dim,\n\t\t\t\t\t\"DISTANCE_METRIC\", schema.VectorArgs.FlatOptions.DistanceMetric,\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.FlatOptions.InitialCapacity > 0 {\n\t\t\t\t\tflatArgs = append(flatArgs, \"INITIAL_CAP\", schema.VectorArgs.FlatOptions.InitialCapacity)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.FlatOptions.BlockSize > 0 {\n\t\t\t\t\tflatArgs = append(flatArgs, \"BLOCK_SIZE\", schema.VectorArgs.FlatOptions.BlockSize)\n\t\t\t\t}\n\t\t\t\targs = append(args, len(flatArgs))\n\t\t\t\targs = append(args, flatArgs...)\n\t\t\t}\n\t\t\tif schema.VectorArgs.HNSWOptions != nil {\n\t\t\t\targs = append(args, \"HNSW\")\n\t\t\t\tif schema.VectorArgs.HNSWOptions.Type == \"\" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == \"\" {\n\t\t\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW\"))\n\t\t\t\t\treturn cmd\n\t\t\t\t}\n\t\t\t\thnswArgs := []interface{}{\n\t\t\t\t\t\"TYPE\", schema.VectorArgs.HNSWOptions.Type,\n\t\t\t\t\t\"DIM\", schema.VectorArgs.HNSWOptions.Dim,\n\t\t\t\t\t\"DISTANCE_METRIC\", schema.VectorArgs.HNSWOptions.DistanceMetric,\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.HNSWOptions.InitialCapacity > 0 {\n\t\t\t\t\thnswArgs = append(hnswArgs, \"INITIAL_CAP\", schema.VectorArgs.HNSWOptions.InitialCapacity)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.HNSWOptions.MaxEdgesPerNode > 0 {\n\t\t\t\t\thnswArgs = append(hnswArgs, \"M\", schema.VectorArgs.HNSWOptions.MaxEdgesPerNode)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode > 0 {\n\t\t\t\t\thnswArgs = append(hnswArgs, \"EF_CONSTRUCTION\", schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.HNSWOptions.EFRunTime > 0 {\n\t\t\t\t\thnswArgs = append(hnswArgs, \"EF_RUNTIME\", schema.VectorArgs.HNSWOptions.EFRunTime)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.HNSWOptions.Epsilon > 0 {\n\t\t\t\t\thnswArgs = append(hnswArgs, \"EPSILON\", schema.VectorArgs.HNSWOptions.Epsilon)\n\t\t\t\t}\n\t\t\t\targs = append(args, len(hnswArgs))\n\t\t\t\targs = append(args, hnswArgs...)\n\t\t\t}\n\t\t\tif schema.VectorArgs.VamanaOptions != nil {\n\t\t\t\targs = append(args, \"SVS-VAMANA\")\n\t\t\t\tif schema.VectorArgs.VamanaOptions.Type == \"\" || schema.VectorArgs.VamanaOptions.Dim == 0 || schema.VectorArgs.VamanaOptions.DistanceMetric == \"\" {\n\t\t\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR VAMANA\"))\n\t\t\t\t\treturn cmd\n\t\t\t\t}\n\t\t\t\tvamanaArgs := []interface{}{\n\t\t\t\t\t\"TYPE\", schema.VectorArgs.VamanaOptions.Type,\n\t\t\t\t\t\"DIM\", schema.VectorArgs.VamanaOptions.Dim,\n\t\t\t\t\t\"DISTANCE_METRIC\", schema.VectorArgs.VamanaOptions.DistanceMetric,\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.VamanaOptions.Compression != \"\" {\n\t\t\t\t\tvamanaArgs = append(vamanaArgs, \"COMPRESSION\", schema.VectorArgs.VamanaOptions.Compression)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.VamanaOptions.ConstructionWindowSize > 0 {\n\t\t\t\t\tvamanaArgs = append(vamanaArgs, \"CONSTRUCTION_WINDOW_SIZE\", schema.VectorArgs.VamanaOptions.ConstructionWindowSize)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.VamanaOptions.GraphMaxDegree > 0 {\n\t\t\t\t\tvamanaArgs = append(vamanaArgs, \"GRAPH_MAX_DEGREE\", schema.VectorArgs.VamanaOptions.GraphMaxDegree)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.VamanaOptions.SearchWindowSize > 0 {\n\t\t\t\t\tvamanaArgs = append(vamanaArgs, \"SEARCH_WINDOW_SIZE\", schema.VectorArgs.VamanaOptions.SearchWindowSize)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.VamanaOptions.Epsilon > 0 {\n\t\t\t\t\tvamanaArgs = append(vamanaArgs, \"EPSILON\", schema.VectorArgs.VamanaOptions.Epsilon)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.VamanaOptions.TrainingThreshold > 0 {\n\t\t\t\t\tvamanaArgs = append(vamanaArgs, \"TRAINING_THRESHOLD\", schema.VectorArgs.VamanaOptions.TrainingThreshold)\n\t\t\t\t}\n\t\t\t\tif schema.VectorArgs.VamanaOptions.ReduceDim > 0 {\n\t\t\t\t\tvamanaArgs = append(vamanaArgs, \"REDUCE\", schema.VectorArgs.VamanaOptions.ReduceDim)\n\t\t\t\t}\n\t\t\t\targs = append(args, len(vamanaArgs))\n\t\t\t\targs = append(args, vamanaArgs...)\n\t\t\t}\n\t\t}\n\t\tif schema.GeoShapeFieldType != \"\" {\n\t\t\tif schema.FieldType != SearchFieldTypeGeoShape {\n\t\t\t\tcmd := NewStatusCmd(ctx, args...)\n\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType\"))\n\t\t\t\treturn cmd\n\t\t\t}\n\t\t\targs = append(args, schema.GeoShapeFieldType)\n\t\t}\n\t\tif schema.NoStem {\n\t\t\targs = append(args, \"NOSTEM\")\n\t\t}\n\t\tif schema.Sortable {\n\t\t\targs = append(args, \"SORTABLE\")\n\t\t}\n\t\tif schema.UNF {\n\t\t\targs = append(args, \"UNF\")\n\t\t}\n\t\tif schema.NoIndex {\n\t\t\targs = append(args, \"NOINDEX\")\n\t\t}\n\t\tif schema.PhoneticMatcher != \"\" {\n\t\t\targs = append(args, \"PHONETIC\", schema.PhoneticMatcher)\n\t\t}\n\t\tif schema.Weight > 0 {\n\t\t\targs = append(args, \"WEIGHT\", schema.Weight)\n\t\t}\n\t\tif schema.Separator != \"\" {\n\t\t\targs = append(args, \"SEPARATOR\", schema.Separator)\n\t\t}\n\t\tif schema.CaseSensitive {\n\t\t\targs = append(args, \"CASESENSITIVE\")\n\t\t}\n\t\tif schema.WithSuffixtrie {\n\t\t\targs = append(args, \"WITHSUFFIXTRIE\")\n\t\t}\n\t\tif schema.IndexEmpty {\n\t\t\targs = append(args, \"INDEXEMPTY\")\n\t\t}\n\t\tif schema.IndexMissing {\n\t\t\targs = append(args, \"INDEXMISSING\")\n\n\t\t}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTCursorDel - Deletes a cursor from an existing index.\n// The 'index' parameter specifies the index from which to delete the cursor, and the 'cursorId' parameter specifies the ID of the cursor to delete.\n// For more information, please refer to the Redis documentation:\n// [FT.CURSOR DEL]: (https://redis.io/commands/ft.cursor-del/)\nfunc (c cmdable) FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"FT.CURSOR\", \"DEL\", index, cursorId)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTCursorRead - Reads the next results from an existing cursor.\n// The 'index' parameter specifies the index from which to read the cursor, the 'cursorId' parameter specifies the ID of the cursor to read, and the 'count' parameter specifies the number of results to read.\n// For more information, please refer to the Redis documentation:\n// [FT.CURSOR READ]: (https://redis.io/commands/ft.cursor-read/)\nfunc (c cmdable) FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd {\n\targs := []interface{}{\"FT.CURSOR\", \"READ\", index, cursorId}\n\tif count > 0 {\n\t\targs = append(args, \"COUNT\", count)\n\t}\n\tcmd := NewMapStringInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTDictAdd - Adds terms to a dictionary.\n// The 'dict' parameter specifies the dictionary to which to add the terms, and the 'term' parameter specifies the terms to add.\n// For more information, please refer to the Redis documentation:\n// [FT.DICTADD]: (https://redis.io/commands/ft.dictadd/)\nfunc (c cmdable) FTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd {\n\targs := []interface{}{\"FT.DICTADD\", dict}\n\targs = append(args, term...)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTDictDel - Deletes terms from a dictionary.\n// The 'dict' parameter specifies the dictionary from which to delete the terms, and the 'term' parameter specifies the terms to delete.\n// For more information, please refer to the Redis documentation:\n// [FT.DICTDEL]: (https://redis.io/commands/ft.dictdel/)\nfunc (c cmdable) FTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd {\n\targs := []interface{}{\"FT.DICTDEL\", dict}\n\targs = append(args, term...)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTDictDump - Returns all terms in the specified dictionary.\n// The 'dict' parameter specifies the dictionary from which to return the terms.\n// For more information, please refer to the Redis documentation:\n// [FT.DICTDUMP]: (https://redis.io/commands/ft.dictdump/)\nfunc (c cmdable) FTDictDump(ctx context.Context, dict string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"FT.DICTDUMP\", dict)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTDropIndex - Deletes an index.\n// The 'index' parameter specifies the index to delete.\n// For more information, please refer to the Redis documentation:\n// [FT.DROPINDEX]: (https://redis.io/commands/ft.dropindex/)\nfunc (c cmdable) FTDropIndex(ctx context.Context, index string) *StatusCmd {\n\targs := []interface{}{\"FT.DROPINDEX\", index}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTDropIndexWithArgs - Deletes an index with options.\n// The 'index' parameter specifies the index to delete, and the 'options' parameter specifies the DeleteDocs option for docs deletion.\n// For more information, please refer to the Redis documentation:\n// [FT.DROPINDEX]: (https://redis.io/commands/ft.dropindex/)\nfunc (c cmdable) FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd {\n\targs := []interface{}{\"FT.DROPINDEX\", index}\n\tif options != nil {\n\t\tif options.DeleteDocs {\n\t\t\targs = append(args, \"DD\")\n\t\t}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTExplain - Returns the execution plan for a complex query.\n// The 'index' parameter specifies the index to query, and the 'query' parameter specifies the query string.\n// For more information, please refer to the Redis documentation:\n// [FT.EXPLAIN]: (https://redis.io/commands/ft.explain/)\nfunc (c cmdable) FTExplain(ctx context.Context, index string, query string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"FT.EXPLAIN\", index, query)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTExplainWithArgs - Returns the execution plan for a complex query with options.\n// The 'index' parameter specifies the index to query, the 'query' parameter specifies the query string, and the 'options' parameter specifies the Dialect for the query.\n// For more information, please refer to the Redis documentation:\n// [FT.EXPLAIN]: (https://redis.io/commands/ft.explain/)\nfunc (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd {\n\targs := []interface{}{\"FT.EXPLAIN\", index, query}\n\tif options.Dialect != \"\" {\n\t\targs = append(args, \"DIALECT\", options.Dialect)\n\t} else {\n\t\targs = append(args, \"DIALECT\", 2)\n\t}\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTExplainCli - Returns the execution plan for a complex query. [Not Implemented]\n// For more information, see https://redis.io/commands/ft.explaincli/\nfunc (c cmdable) FTExplainCli(ctx context.Context, key, path string) error {\n\treturn fmt.Errorf(\"FTExplainCli is not implemented\")\n}\n\nfunc parseFTInfo(data map[string]interface{}) (FTInfoResult, error) {\n\tvar ftInfo FTInfoResult\n\t// Manually parse each field from the map\n\tif indexErrors, ok := data[\"Index Errors\"].([]interface{}); ok {\n\t\tftInfo.IndexErrors = IndexErrors{\n\t\t\tIndexingFailures:     internal.ToInteger(indexErrors[1]),\n\t\t\tLastIndexingError:    internal.ToString(indexErrors[3]),\n\t\t\tLastIndexingErrorKey: internal.ToString(indexErrors[5]),\n\t\t}\n\t}\n\n\tif attributes, ok := data[\"attributes\"].([]interface{}); ok {\n\t\tfor _, attr := range attributes {\n\t\t\tif attrMap, ok := attr.([]interface{}); ok {\n\t\t\t\tatt := FTAttribute{}\n\t\t\t\tattrLen := len(attrMap)\n\t\t\t\tfor i := 0; i < attrLen; i++ {\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"attribute\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.Attribute = internal.ToString(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"identifier\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.Identifier = internal.ToString(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"type\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.Type = internal.ToString(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"weight\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.Weight = internal.ToFloat(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"nostem\" {\n\t\t\t\t\t\tatt.NoStem = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"sortable\" {\n\t\t\t\t\t\tatt.Sortable = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"noindex\" {\n\t\t\t\t\t\tatt.NoIndex = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"unf\" {\n\t\t\t\t\t\tatt.UNF = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"phonetic\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.PhoneticMatcher = internal.ToString(attrMap[i+1])\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"case_sensitive\" {\n\t\t\t\t\t\tatt.CaseSensitive = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"withsuffixtrie\" {\n\t\t\t\t\t\tatt.WithSuffixtrie = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// vector specific attributes\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"algorithm\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.Algorithm = internal.ToString(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"data_type\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.DataType = internal.ToString(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"dim\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.Dim = internal.ToInteger(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"distance_metric\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.DistanceMetric = internal.ToString(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"m\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.M = internal.ToInteger(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif internal.ToLower(internal.ToString(attrMap[i])) == \"ef_construction\" && i+1 < attrLen {\n\t\t\t\t\t\tatt.EFConstruction = internal.ToInteger(attrMap[i+1])\n\t\t\t\t\t\ti++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t\t\tftInfo.Attributes = append(ftInfo.Attributes, att)\n\t\t\t}\n\t\t}\n\t}\n\n\tftInfo.BytesPerRecordAvg = internal.ToString(data[\"bytes_per_record_avg\"])\n\tftInfo.Cleaning = internal.ToInteger(data[\"cleaning\"])\n\n\tif cursorStats, ok := data[\"cursor_stats\"].([]interface{}); ok {\n\t\tftInfo.CursorStats = CursorStats{\n\t\t\tGlobalIdle:    internal.ToInteger(cursorStats[1]),\n\t\t\tGlobalTotal:   internal.ToInteger(cursorStats[3]),\n\t\t\tIndexCapacity: internal.ToInteger(cursorStats[5]),\n\t\t\tIndexTotal:    internal.ToInteger(cursorStats[7]),\n\t\t}\n\t}\n\n\tif dialectStats, ok := data[\"dialect_stats\"].([]interface{}); ok {\n\t\tftInfo.DialectStats = make(map[string]int)\n\t\tfor i := 0; i < len(dialectStats); i += 2 {\n\t\t\tftInfo.DialectStats[internal.ToString(dialectStats[i])] = internal.ToInteger(dialectStats[i+1])\n\t\t}\n\t}\n\n\tftInfo.DocTableSizeMB = internal.ToFloat(data[\"doc_table_size_mb\"])\n\n\tif fieldStats, ok := data[\"field statistics\"].([]interface{}); ok {\n\t\tfor _, stat := range fieldStats {\n\t\t\tif statMap, ok := stat.([]interface{}); ok {\n\t\t\t\tftInfo.FieldStatistics = append(ftInfo.FieldStatistics, FieldStatistic{\n\t\t\t\t\tIdentifier: internal.ToString(statMap[1]),\n\t\t\t\t\tAttribute:  internal.ToString(statMap[3]),\n\t\t\t\t\tIndexErrors: IndexErrors{\n\t\t\t\t\t\tIndexingFailures:     internal.ToInteger(statMap[5].([]interface{})[1]),\n\t\t\t\t\t\tLastIndexingError:    internal.ToString(statMap[5].([]interface{})[3]),\n\t\t\t\t\t\tLastIndexingErrorKey: internal.ToString(statMap[5].([]interface{})[5]),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif gcStats, ok := data[\"gc_stats\"].([]interface{}); ok {\n\t\tftInfo.GCStats = GCStats{}\n\t\tfor i := 0; i < len(gcStats); i += 2 {\n\t\t\tif internal.ToLower(internal.ToString(gcStats[i])) == \"bytes_collected\" {\n\t\t\t\tftInfo.GCStats.BytesCollected = internal.ToInteger(gcStats[i+1])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif internal.ToLower(internal.ToString(gcStats[i])) == \"total_ms_run\" {\n\t\t\t\tftInfo.GCStats.TotalMsRun = internal.ToInteger(gcStats[i+1])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif internal.ToLower(internal.ToString(gcStats[i])) == \"total_cycles\" {\n\t\t\t\tftInfo.GCStats.TotalCycles = internal.ToInteger(gcStats[i+1])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif internal.ToLower(internal.ToString(gcStats[i])) == \"average_cycle_time_ms\" {\n\t\t\t\tftInfo.GCStats.AverageCycleTimeMs = internal.ToString(gcStats[i+1])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif internal.ToLower(internal.ToString(gcStats[i])) == \"last_run_time_ms\" {\n\t\t\t\tftInfo.GCStats.LastRunTimeMs = internal.ToInteger(gcStats[i+1])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif internal.ToLower(internal.ToString(gcStats[i])) == \"gc_numeric_trees_missed\" {\n\t\t\t\tftInfo.GCStats.GCNumericTreesMissed = internal.ToInteger(gcStats[i+1])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif internal.ToLower(internal.ToString(gcStats[i])) == \"gc_blocks_denied\" {\n\t\t\t\tftInfo.GCStats.GCBlocksDenied = internal.ToInteger(gcStats[i+1])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\n\tftInfo.GeoshapesSzMB = internal.ToFloat(data[\"geoshapes_sz_mb\"])\n\tftInfo.HashIndexingFailures = internal.ToInteger(data[\"hash_indexing_failures\"])\n\n\tif indexDef, ok := data[\"index_definition\"].([]interface{}); ok {\n\t\tftInfo.IndexDefinition = IndexDefinition{\n\t\t\tKeyType:      internal.ToString(indexDef[1]),\n\t\t\tPrefixes:     internal.ToStringSlice(indexDef[3]),\n\t\t\tDefaultScore: internal.ToFloat(indexDef[5]),\n\t\t}\n\t}\n\n\tftInfo.IndexName = internal.ToString(data[\"index_name\"])\n\tftInfo.IndexOptions = internal.ToStringSlice(data[\"index_options\"].([]interface{}))\n\tftInfo.Indexing = internal.ToInteger(data[\"indexing\"])\n\tftInfo.InvertedSzMB = internal.ToFloat(data[\"inverted_sz_mb\"])\n\tftInfo.KeyTableSizeMB = internal.ToFloat(data[\"key_table_size_mb\"])\n\tftInfo.MaxDocID = internal.ToInteger(data[\"max_doc_id\"])\n\tftInfo.NumDocs = internal.ToInteger(data[\"num_docs\"])\n\tftInfo.NumRecords = internal.ToInteger(data[\"num_records\"])\n\tftInfo.NumTerms = internal.ToInteger(data[\"num_terms\"])\n\tftInfo.NumberOfUses = internal.ToInteger(data[\"number_of_uses\"])\n\tftInfo.OffsetBitsPerRecordAvg = internal.ToString(data[\"offset_bits_per_record_avg\"])\n\tftInfo.OffsetVectorsSzMB = internal.ToFloat(data[\"offset_vectors_sz_mb\"])\n\tftInfo.OffsetsPerTermAvg = internal.ToString(data[\"offsets_per_term_avg\"])\n\tftInfo.PercentIndexed = internal.ToFloat(data[\"percent_indexed\"])\n\tftInfo.RecordsPerDocAvg = internal.ToString(data[\"records_per_doc_avg\"])\n\tftInfo.SortableValuesSizeMB = internal.ToFloat(data[\"sortable_values_size_mb\"])\n\tftInfo.TagOverheadSzMB = internal.ToFloat(data[\"tag_overhead_sz_mb\"])\n\tftInfo.TextOverheadSzMB = internal.ToFloat(data[\"text_overhead_sz_mb\"])\n\tftInfo.TotalIndexMemorySzMB = internal.ToFloat(data[\"total_index_memory_sz_mb\"])\n\tftInfo.TotalIndexingTime = internal.ToInteger(data[\"total_indexing_time\"])\n\tftInfo.TotalInvertedIndexBlocks = internal.ToInteger(data[\"total_inverted_index_blocks\"])\n\tftInfo.VectorIndexSzMB = internal.ToFloat(data[\"vector_index_sz_mb\"])\n\n\treturn ftInfo, nil\n}\n\ntype FTInfoCmd struct {\n\tbaseCmd\n\tval FTInfoResult\n}\n\nfunc newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd {\n\treturn &FTInfoCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFTInfo,\n\t\t},\n\t}\n}\n\nfunc (cmd *FTInfoCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FTInfoCmd) SetVal(val FTInfoResult) {\n\tcmd.val = val\n}\n\nfunc (cmd *FTInfoCmd) Result() (FTInfoResult, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FTInfoCmd) Val() FTInfoResult {\n\treturn cmd.val\n}\n\nfunc (cmd *FTInfoCmd) RawVal() interface{} {\n\treturn cmd.rawVal\n}\n\nfunc (cmd *FTInfoCmd) RawResult() (interface{}, error) {\n\treturn cmd.rawVal, cmd.err\n}\nfunc (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata := make(map[string]interface{}, n)\n\tfor i := 0; i < n; i++ {\n\t\tk, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tv, err := rd.ReadReply()\n\t\tif err != nil {\n\t\t\tif err == Nil {\n\t\t\t\tdata[k] = Nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err, ok := err.(proto.RedisError); ok {\n\t\t\t\tdata[k] = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tdata[k] = v\n\t}\n\tcmd.val, err = parseFTInfo(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *FTInfoCmd) Clone() Cmder {\n\tval := FTInfoResult{\n\t\tIndexErrors:              cmd.val.IndexErrors,\n\t\tBytesPerRecordAvg:        cmd.val.BytesPerRecordAvg,\n\t\tCleaning:                 cmd.val.Cleaning,\n\t\tCursorStats:              cmd.val.CursorStats,\n\t\tDocTableSizeMB:           cmd.val.DocTableSizeMB,\n\t\tGCStats:                  cmd.val.GCStats,\n\t\tGeoshapesSzMB:            cmd.val.GeoshapesSzMB,\n\t\tHashIndexingFailures:     cmd.val.HashIndexingFailures,\n\t\tIndexDefinition:          cmd.val.IndexDefinition,\n\t\tIndexName:                cmd.val.IndexName,\n\t\tIndexing:                 cmd.val.Indexing,\n\t\tInvertedSzMB:             cmd.val.InvertedSzMB,\n\t\tKeyTableSizeMB:           cmd.val.KeyTableSizeMB,\n\t\tMaxDocID:                 cmd.val.MaxDocID,\n\t\tNumDocs:                  cmd.val.NumDocs,\n\t\tNumRecords:               cmd.val.NumRecords,\n\t\tNumTerms:                 cmd.val.NumTerms,\n\t\tNumberOfUses:             cmd.val.NumberOfUses,\n\t\tOffsetBitsPerRecordAvg:   cmd.val.OffsetBitsPerRecordAvg,\n\t\tOffsetVectorsSzMB:        cmd.val.OffsetVectorsSzMB,\n\t\tOffsetsPerTermAvg:        cmd.val.OffsetsPerTermAvg,\n\t\tPercentIndexed:           cmd.val.PercentIndexed,\n\t\tRecordsPerDocAvg:         cmd.val.RecordsPerDocAvg,\n\t\tSortableValuesSizeMB:     cmd.val.SortableValuesSizeMB,\n\t\tTagOverheadSzMB:          cmd.val.TagOverheadSzMB,\n\t\tTextOverheadSzMB:         cmd.val.TextOverheadSzMB,\n\t\tTotalIndexMemorySzMB:     cmd.val.TotalIndexMemorySzMB,\n\t\tTotalIndexingTime:        cmd.val.TotalIndexingTime,\n\t\tTotalInvertedIndexBlocks: cmd.val.TotalInvertedIndexBlocks,\n\t\tVectorIndexSzMB:          cmd.val.VectorIndexSzMB,\n\t}\n\t// Clone slices and maps\n\tif cmd.val.Attributes != nil {\n\t\tval.Attributes = make([]FTAttribute, len(cmd.val.Attributes))\n\t\tcopy(val.Attributes, cmd.val.Attributes)\n\t}\n\tif cmd.val.DialectStats != nil {\n\t\tval.DialectStats = make(map[string]int, len(cmd.val.DialectStats))\n\t\tfor k, v := range cmd.val.DialectStats {\n\t\t\tval.DialectStats[k] = v\n\t\t}\n\t}\n\tif cmd.val.FieldStatistics != nil {\n\t\tval.FieldStatistics = make([]FieldStatistic, len(cmd.val.FieldStatistics))\n\t\tcopy(val.FieldStatistics, cmd.val.FieldStatistics)\n\t}\n\tif cmd.val.IndexOptions != nil {\n\t\tval.IndexOptions = make([]string, len(cmd.val.IndexOptions))\n\t\tcopy(val.IndexOptions, cmd.val.IndexOptions)\n\t}\n\tif cmd.val.IndexDefinition.Prefixes != nil {\n\t\tval.IndexDefinition.Prefixes = make([]string, len(cmd.val.IndexDefinition.Prefixes))\n\t\tcopy(val.IndexDefinition.Prefixes, cmd.val.IndexDefinition.Prefixes)\n\t}\n\treturn &FTInfoCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// FTInfo - Retrieves information about an index.\n// The 'index' parameter specifies the index to retrieve information about.\n// For more information, please refer to the Redis documentation:\n// [FT.INFO]: (https://redis.io/commands/ft.info/)\nfunc (c cmdable) FTInfo(ctx context.Context, index string) *FTInfoCmd {\n\tcmd := newFTInfoCmd(ctx, \"FT.INFO\", index)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTSpellCheck - Checks a query string for spelling errors.\n// For more details about spellcheck query please follow:\n// https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/\n// For more information, please refer to the Redis documentation:\n// [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/)\nfunc (c cmdable) FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd {\n\targs := []interface{}{\"FT.SPELLCHECK\", index, query}\n\tcmd := newFTSpellCheckCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTSpellCheckWithArgs - Checks a query string for spelling errors with additional options.\n// For more details about spellcheck query please follow:\n// https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/\n// For more information, please refer to the Redis documentation:\n// [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/)\nfunc (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd {\n\targs := []interface{}{\"FT.SPELLCHECK\", index, query}\n\tif options != nil {\n\t\tif options.Distance > 0 {\n\t\t\targs = append(args, \"DISTANCE\", options.Distance)\n\t\t}\n\t\tif options.Terms != nil {\n\t\t\targs = append(args, \"TERMS\", options.Terms.Inclusion, options.Terms.Dictionary)\n\t\t\targs = append(args, options.Terms.Terms...)\n\t\t}\n\t\tif options.Dialect > 0 {\n\t\t\targs = append(args, \"DIALECT\", options.Dialect)\n\t\t} else {\n\t\t\targs = append(args, \"DIALECT\", 2)\n\t\t}\n\t}\n\tcmd := newFTSpellCheckCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype FTSpellCheckCmd struct {\n\tbaseCmd\n\tval []SpellCheckResult\n}\n\nfunc newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd {\n\treturn &FTSpellCheckCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFTSpellCheck,\n\t\t},\n\t}\n}\n\nfunc (cmd *FTSpellCheckCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FTSpellCheckCmd) SetVal(val []SpellCheckResult) {\n\tcmd.val = val\n}\n\nfunc (cmd *FTSpellCheckCmd) Result() ([]SpellCheckResult, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FTSpellCheckCmd) Val() []SpellCheckResult {\n\treturn cmd.val\n}\n\nfunc (cmd *FTSpellCheckCmd) RawVal() interface{} {\n\treturn cmd.rawVal\n}\n\nfunc (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) {\n\treturn cmd.rawVal, cmd.err\n}\n\nfunc (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) {\n\tdata, err := rd.ReadSlice()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val, err = parseFTSpellCheck(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) {\n\tresults := make([]SpellCheckResult, 0, len(data))\n\n\tfor _, termData := range data {\n\t\ttermInfo, ok := termData.([]interface{})\n\t\tif !ok || len(termInfo) != 3 {\n\t\t\treturn nil, fmt.Errorf(\"invalid term format\")\n\t\t}\n\n\t\tterm, ok := termInfo[1].(string)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"invalid term format\")\n\t\t}\n\n\t\tsuggestionsData, ok := termInfo[2].([]interface{})\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"invalid suggestions format\")\n\t\t}\n\n\t\tsuggestions := make([]SpellCheckSuggestion, 0, len(suggestionsData))\n\t\tfor _, suggestionData := range suggestionsData {\n\t\t\tsuggestionInfo, ok := suggestionData.([]interface{})\n\t\t\tif !ok || len(suggestionInfo) != 2 {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid suggestion format\")\n\t\t\t}\n\n\t\t\tscoreStr, ok := suggestionInfo[0].(string)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid suggestion score format\")\n\t\t\t}\n\t\t\tscore, err := strconv.ParseFloat(scoreStr, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid suggestion score value\")\n\t\t\t}\n\n\t\t\tsuggestion, ok := suggestionInfo[1].(string)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid suggestion format\")\n\t\t\t}\n\n\t\t\tsuggestions = append(suggestions, SpellCheckSuggestion{\n\t\t\t\tScore:      score,\n\t\t\t\tSuggestion: suggestion,\n\t\t\t})\n\t\t}\n\n\t\tresults = append(results, SpellCheckResult{\n\t\t\tTerm:        term,\n\t\t\tSuggestions: suggestions,\n\t\t})\n\t}\n\n\treturn results, nil\n}\n\nfunc (cmd *FTSpellCheckCmd) Clone() Cmder {\n\tvar val []SpellCheckResult\n\tif cmd.val != nil {\n\t\tval = make([]SpellCheckResult, len(cmd.val))\n\t\tfor i, result := range cmd.val {\n\t\t\tval[i] = SpellCheckResult{\n\t\t\t\tTerm: result.Term,\n\t\t\t}\n\t\t\tif result.Suggestions != nil {\n\t\t\t\tval[i].Suggestions = make([]SpellCheckSuggestion, len(result.Suggestions))\n\t\t\t\tcopy(val[i].Suggestions, result.Suggestions)\n\t\t\t}\n\t\t}\n\t}\n\treturn &FTSpellCheckCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\nfunc parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) {\n\tif len(data) < 1 {\n\t\treturn FTSearchResult{}, fmt.Errorf(\"unexpected search result format\")\n\t}\n\n\ttotal, ok := data[0].(int64)\n\tif !ok {\n\t\treturn FTSearchResult{}, fmt.Errorf(\"invalid total results format\")\n\t}\n\n\tvar results []Document\n\tfor i := 1; i < len(data); {\n\t\tdocID, ok := data[i].(string)\n\t\tif !ok {\n\t\t\treturn FTSearchResult{}, fmt.Errorf(\"invalid document ID format\")\n\t\t}\n\n\t\tdoc := Document{\n\t\t\tID:     docID,\n\t\t\tFields: make(map[string]string),\n\t\t}\n\t\ti++\n\n\t\tif noContent {\n\t\t\tresults = append(results, doc)\n\t\t\tcontinue\n\t\t}\n\n\t\tif withScores && i < len(data) {\n\t\t\tif scoreStr, ok := data[i].(string); ok {\n\t\t\t\tscore, err := strconv.ParseFloat(scoreStr, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn FTSearchResult{}, fmt.Errorf(\"invalid score format\")\n\t\t\t\t}\n\t\t\t\tdoc.Score = &score\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\n\t\tif withPayloads && i < len(data) {\n\t\t\tif payload, ok := data[i].(string); ok {\n\t\t\t\tdoc.Payload = &payload\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\n\t\tif withSortKeys && i < len(data) {\n\t\t\tif sortKey, ok := data[i].(string); ok {\n\t\t\t\tdoc.SortKey = &sortKey\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\n\t\tif i < len(data) {\n\t\t\tfields, ok := data[i].([]interface{})\n\t\t\tif !ok {\n\t\t\t\tif data[i] == proto.Nil || data[i] == nil {\n\t\t\t\t\tdoc.Error = proto.Nil\n\t\t\t\t\tdoc.Fields = map[string]string{}\n\t\t\t\t\tfields = []interface{}{}\n\t\t\t\t} else {\n\t\t\t\t\treturn FTSearchResult{}, fmt.Errorf(\"invalid document fields format\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor j := 0; j < len(fields); j += 2 {\n\t\t\t\tkey, ok := fields[j].(string)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn FTSearchResult{}, fmt.Errorf(\"invalid field key format\")\n\t\t\t\t}\n\t\t\t\tvalue, ok := fields[j+1].(string)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn FTSearchResult{}, fmt.Errorf(\"invalid field value format\")\n\t\t\t\t}\n\t\t\t\tdoc.Fields[key] = value\n\t\t\t}\n\t\t\ti++\n\t\t}\n\n\t\tresults = append(results, doc)\n\t}\n\treturn FTSearchResult{\n\t\tTotal: int(total),\n\t\tDocs:  results,\n\t}, nil\n}\n\ntype FTSearchCmd struct {\n\tbaseCmd\n\tval     FTSearchResult\n\toptions *FTSearchOptions\n}\n\nfunc newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd {\n\treturn &FTSearchCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFTSearch,\n\t\t},\n\t\toptions: options,\n\t}\n}\n\nfunc (cmd *FTSearchCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FTSearchCmd) SetVal(val FTSearchResult) {\n\tcmd.val = val\n}\n\nfunc (cmd *FTSearchCmd) Result() (FTSearchResult, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FTSearchCmd) Val() FTSearchResult {\n\treturn cmd.val\n}\n\nfunc (cmd *FTSearchCmd) RawVal() interface{} {\n\treturn cmd.rawVal\n}\n\nfunc (cmd *FTSearchCmd) RawResult() (interface{}, error) {\n\treturn cmd.rawVal, cmd.err\n}\n\nfunc (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {\n\tdata, err := rd.ReadSlice()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (cmd *FTSearchCmd) Clone() Cmder {\n\tval := FTSearchResult{\n\t\tTotal: cmd.val.Total,\n\t}\n\tif cmd.val.Docs != nil {\n\t\tval.Docs = make([]Document, len(cmd.val.Docs))\n\t\tfor i, doc := range cmd.val.Docs {\n\t\t\tval.Docs[i] = Document{\n\t\t\t\tID:      doc.ID,\n\t\t\t\tScore:   doc.Score,\n\t\t\t\tPayload: doc.Payload,\n\t\t\t\tSortKey: doc.SortKey,\n\t\t\t}\n\t\t\tif doc.Fields != nil {\n\t\t\t\tval.Docs[i].Fields = make(map[string]string, len(doc.Fields))\n\t\t\t\tfor k, v := range doc.Fields {\n\t\t\t\t\tval.Docs[i].Fields[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tvar options *FTSearchOptions\n\tif cmd.options != nil {\n\t\toptions = &FTSearchOptions{\n\t\t\tNoContent:       cmd.options.NoContent,\n\t\t\tVerbatim:        cmd.options.Verbatim,\n\t\t\tNoStopWords:     cmd.options.NoStopWords,\n\t\t\tWithScores:      cmd.options.WithScores,\n\t\t\tWithPayloads:    cmd.options.WithPayloads,\n\t\t\tWithSortKeys:    cmd.options.WithSortKeys,\n\t\t\tSlop:            cmd.options.Slop,\n\t\t\tTimeout:         cmd.options.Timeout,\n\t\t\tInOrder:         cmd.options.InOrder,\n\t\t\tLanguage:        cmd.options.Language,\n\t\t\tExpander:        cmd.options.Expander,\n\t\t\tScorer:          cmd.options.Scorer,\n\t\t\tExplainScore:    cmd.options.ExplainScore,\n\t\t\tPayload:         cmd.options.Payload,\n\t\t\tSortByWithCount: cmd.options.SortByWithCount,\n\t\t\tLimitOffset:     cmd.options.LimitOffset,\n\t\t\tLimit:           cmd.options.Limit,\n\t\t\tCountOnly:       cmd.options.CountOnly,\n\t\t\tDialectVersion:  cmd.options.DialectVersion,\n\t\t}\n\t\t// Clone slices and maps\n\t\tif cmd.options.Filters != nil {\n\t\t\toptions.Filters = make([]FTSearchFilter, len(cmd.options.Filters))\n\t\t\tcopy(options.Filters, cmd.options.Filters)\n\t\t}\n\t\tif cmd.options.GeoFilter != nil {\n\t\t\toptions.GeoFilter = make([]FTSearchGeoFilter, len(cmd.options.GeoFilter))\n\t\t\tcopy(options.GeoFilter, cmd.options.GeoFilter)\n\t\t}\n\t\tif cmd.options.InKeys != nil {\n\t\t\toptions.InKeys = make([]interface{}, len(cmd.options.InKeys))\n\t\t\tcopy(options.InKeys, cmd.options.InKeys)\n\t\t}\n\t\tif cmd.options.InFields != nil {\n\t\t\toptions.InFields = make([]interface{}, len(cmd.options.InFields))\n\t\t\tcopy(options.InFields, cmd.options.InFields)\n\t\t}\n\t\tif cmd.options.Return != nil {\n\t\t\toptions.Return = make([]FTSearchReturn, len(cmd.options.Return))\n\t\t\tcopy(options.Return, cmd.options.Return)\n\t\t}\n\t\tif cmd.options.SortBy != nil {\n\t\t\toptions.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy))\n\t\t\tcopy(options.SortBy, cmd.options.SortBy)\n\t\t}\n\t\tif cmd.options.Params != nil {\n\t\t\toptions.Params = make(map[string]interface{}, len(cmd.options.Params))\n\t\t\tfor k, v := range cmd.options.Params {\n\t\t\t\toptions.Params[k] = v\n\t\t\t}\n\t\t}\n\t}\n\treturn &FTSearchCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t\toptions: options,\n\t}\n}\n\n// FTHybridResult represents the result of a hybrid search operation\ntype FTHybridResult struct {\n\tTotalResults  int\n\tResults       []map[string]interface{}\n\tWarnings      []string\n\tExecutionTime float64\n}\n\n// FTHybridCursorResult represents cursor result for hybrid search\ntype FTHybridCursorResult struct {\n\tSearchCursorID int\n\tVsimCursorID   int\n}\n\ntype FTHybridCmd struct {\n\tbaseCmd\n\tval        FTHybridResult\n\tcursorVal  *FTHybridCursorResult\n\toptions    *FTHybridOptions\n\twithCursor bool\n}\n\nfunc newFTHybridCmd(ctx context.Context, options *FTHybridOptions, args ...interface{}) *FTHybridCmd {\n\tvar withCursor bool\n\tif options != nil && options.WithCursor {\n\t\twithCursor = true\n\t}\n\treturn &FTHybridCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:  ctx,\n\t\t\targs: args,\n\t\t},\n\t\toptions:    options,\n\t\twithCursor: withCursor,\n\t}\n}\n\nfunc (cmd *FTHybridCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FTHybridCmd) SetVal(val FTHybridResult) {\n\tcmd.val = val\n}\n\nfunc (cmd *FTHybridCmd) Result() (FTHybridResult, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FTHybridCmd) CursorResult() (*FTHybridCursorResult, error) {\n\treturn cmd.cursorVal, cmd.err\n}\n\nfunc (cmd *FTHybridCmd) Val() FTHybridResult {\n\treturn cmd.val\n}\n\nfunc (cmd *FTHybridCmd) CursorVal() *FTHybridCursorResult {\n\treturn cmd.cursorVal\n}\n\nfunc (cmd *FTHybridCmd) RawVal() interface{} {\n\treturn cmd.rawVal\n}\n\nfunc (cmd *FTHybridCmd) RawResult() (interface{}, error) {\n\treturn cmd.rawVal, cmd.err\n}\n\nfunc parseFTHybrid(data []interface{}, withCursor bool) (FTHybridResult, *FTHybridCursorResult, error) {\n\t// Convert to map\n\tresultMap := make(map[string]interface{})\n\tfor i := 0; i < len(data); i += 2 {\n\t\tif i+1 < len(data) {\n\t\t\tkey, ok := data[i].(string)\n\t\t\tif !ok {\n\t\t\t\treturn FTHybridResult{}, nil, fmt.Errorf(\"invalid key type at index %d\", i)\n\t\t\t}\n\t\t\tresultMap[key] = data[i+1]\n\t\t}\n\t}\n\n\t// Handle cursor result\n\tif withCursor {\n\t\tsearchCursorID, ok1 := resultMap[\"SEARCH\"].(int64)\n\t\tvsimCursorID, ok2 := resultMap[\"VSIM\"].(int64)\n\t\tif !ok1 || !ok2 {\n\t\t\treturn FTHybridResult{}, nil, fmt.Errorf(\"invalid cursor result format\")\n\t\t}\n\t\treturn FTHybridResult{}, &FTHybridCursorResult{\n\t\t\tSearchCursorID: int(searchCursorID),\n\t\t\tVsimCursorID:   int(vsimCursorID),\n\t\t}, nil\n\t}\n\n\t// Parse regular result\n\ttotalResults, ok := resultMap[\"total_results\"].(int64)\n\tif !ok {\n\t\treturn FTHybridResult{}, nil, fmt.Errorf(\"invalid total_results format\")\n\t}\n\n\tresultsData, ok := resultMap[\"results\"].([]interface{})\n\tif !ok {\n\t\treturn FTHybridResult{}, nil, fmt.Errorf(\"invalid results format\")\n\t}\n\n\t// Parse each result item\n\tresults := make([]map[string]interface{}, 0, len(resultsData))\n\tfor _, item := range resultsData {\n\t\t// Try parsing as map[string]interface{} first (RESP3 format)\n\t\tif itemMap, ok := item.(map[string]interface{}); ok {\n\t\t\tresults = append(results, itemMap)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Try parsing as map[interface{}]interface{} (alternative RESP3 format)\n\t\tif rawMap, ok := item.(map[interface{}]interface{}); ok {\n\t\t\titemMap := make(map[string]interface{})\n\t\t\tfor k, v := range rawMap {\n\t\t\t\tif keyStr, ok := k.(string); ok {\n\t\t\t\t\titemMap[keyStr] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\tresults = append(results, itemMap)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Fall back to array format (RESP2 format - key-value pairs)\n\t\titemData, ok := item.([]interface{})\n\t\tif !ok {\n\t\t\treturn FTHybridResult{}, nil, fmt.Errorf(\"invalid result item format\")\n\t\t}\n\n\t\titemMap := make(map[string]interface{})\n\t\tfor i := 0; i < len(itemData); i += 2 {\n\t\t\tif i+1 < len(itemData) {\n\t\t\t\tkey, ok := itemData[i].(string)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn FTHybridResult{}, nil, fmt.Errorf(\"invalid item key format\")\n\t\t\t\t}\n\t\t\t\titemMap[key] = itemData[i+1]\n\t\t\t}\n\t\t}\n\t\tresults = append(results, itemMap)\n\t}\n\n\t// Parse warnings (optional field)\n\tvar warnings []string\n\tif warningsData, ok := resultMap[\"warnings\"].([]interface{}); ok {\n\t\twarnings = make([]string, 0, len(warningsData))\n\t\tfor _, w := range warningsData {\n\t\t\tif ws, ok := w.(string); ok {\n\t\t\t\twarnings = append(warnings, ws)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Parse execution time (optional field)\n\tvar executionTime float64\n\tif execTimeVal, exists := resultMap[\"execution_time\"]; exists {\n\t\tswitch v := execTimeVal.(type) {\n\t\tcase string:\n\t\t\tvar err error\n\t\t\texecutionTime, err = strconv.ParseFloat(v, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn FTHybridResult{}, nil, fmt.Errorf(\"invalid execution_time format: %v\", err)\n\t\t\t}\n\t\tcase float64:\n\t\t\texecutionTime = v\n\t\tcase int64:\n\t\t\texecutionTime = float64(v)\n\t\t}\n\t}\n\n\treturn FTHybridResult{\n\t\tTotalResults:  int(totalResults),\n\t\tResults:       results,\n\t\tWarnings:      warnings,\n\t\tExecutionTime: executionTime,\n\t}, nil, nil\n}\n\nfunc (cmd *FTHybridCmd) readReply(rd *proto.Reader) (err error) {\n\tdata, err := rd.ReadSlice()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresult, cursorResult, err := parseFTHybrid(data, cmd.withCursor)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif cmd.withCursor {\n\t\tcmd.cursorVal = cursorResult\n\t} else {\n\t\tcmd.val = result\n\t}\n\treturn nil\n}\n\nfunc (cmd *FTHybridCmd) Clone() Cmder {\n\tval := FTHybridResult{\n\t\tTotalResults:  cmd.val.TotalResults,\n\t\tExecutionTime: cmd.val.ExecutionTime,\n\t}\n\tif cmd.val.Results != nil {\n\t\tval.Results = make([]map[string]interface{}, len(cmd.val.Results))\n\t\tfor i, result := range cmd.val.Results {\n\t\t\tval.Results[i] = make(map[string]interface{}, len(result))\n\t\t\tfor k, v := range result {\n\t\t\t\tval.Results[i][k] = v\n\t\t\t}\n\t\t}\n\t}\n\tif cmd.val.Warnings != nil {\n\t\tval.Warnings = make([]string, len(cmd.val.Warnings))\n\t\tcopy(val.Warnings, cmd.val.Warnings)\n\t}\n\n\tvar cursorVal *FTHybridCursorResult\n\tif cmd.cursorVal != nil {\n\t\tcursorVal = &FTHybridCursorResult{\n\t\t\tSearchCursorID: cmd.cursorVal.SearchCursorID,\n\t\t\tVsimCursorID:   cmd.cursorVal.VsimCursorID,\n\t\t}\n\t}\n\n\tvar options *FTHybridOptions\n\tif cmd.options != nil {\n\t\toptions = &FTHybridOptions{\n\t\t\tCountExpressions: cmd.options.CountExpressions,\n\t\t\tLoad:             cmd.options.Load,\n\t\t\tFilter:           cmd.options.Filter,\n\t\t\tLimitOffset:      cmd.options.LimitOffset,\n\t\t\tLimit:            cmd.options.Limit,\n\t\t\tExplainScore:     cmd.options.ExplainScore,\n\t\t\tTimeout:          cmd.options.Timeout,\n\t\t\tWithCursor:       cmd.options.WithCursor,\n\t\t}\n\t\t// Clone slices and maps\n\t\tif cmd.options.SearchExpressions != nil {\n\t\t\toptions.SearchExpressions = make([]FTHybridSearchExpression, len(cmd.options.SearchExpressions))\n\t\t\tcopy(options.SearchExpressions, cmd.options.SearchExpressions)\n\t\t}\n\t\tif cmd.options.VectorExpressions != nil {\n\t\t\toptions.VectorExpressions = make([]FTHybridVectorExpression, len(cmd.options.VectorExpressions))\n\t\t\tcopy(options.VectorExpressions, cmd.options.VectorExpressions)\n\t\t}\n\t\tif cmd.options.Combine != nil {\n\t\t\toptions.Combine = &FTHybridCombineOptions{\n\t\t\t\tMethod:       cmd.options.Combine.Method,\n\t\t\t\tCount:        cmd.options.Combine.Count,\n\t\t\t\tWindow:       cmd.options.Combine.Window,\n\t\t\t\tConstant:     cmd.options.Combine.Constant,\n\t\t\t\tAlpha:        cmd.options.Combine.Alpha,\n\t\t\t\tBeta:         cmd.options.Combine.Beta,\n\t\t\t\tYieldScoreAs: cmd.options.Combine.YieldScoreAs,\n\t\t\t}\n\t\t}\n\t\tif cmd.options.GroupBy != nil {\n\t\t\toptions.GroupBy = &FTHybridGroupBy{\n\t\t\t\tCount:       cmd.options.GroupBy.Count,\n\t\t\t\tReduceFunc:  cmd.options.GroupBy.ReduceFunc,\n\t\t\t\tReduceCount: cmd.options.GroupBy.ReduceCount,\n\t\t\t}\n\t\t\tif cmd.options.GroupBy.Fields != nil {\n\t\t\t\toptions.GroupBy.Fields = make([]string, len(cmd.options.GroupBy.Fields))\n\t\t\t\tcopy(options.GroupBy.Fields, cmd.options.GroupBy.Fields)\n\t\t\t}\n\t\t\tif cmd.options.GroupBy.ReduceParams != nil {\n\t\t\t\toptions.GroupBy.ReduceParams = make([]interface{}, len(cmd.options.GroupBy.ReduceParams))\n\t\t\t\tcopy(options.GroupBy.ReduceParams, cmd.options.GroupBy.ReduceParams)\n\t\t\t}\n\t\t}\n\t\tif cmd.options.Apply != nil {\n\t\t\toptions.Apply = make([]FTHybridApply, len(cmd.options.Apply))\n\t\t\tcopy(options.Apply, cmd.options.Apply)\n\t\t}\n\t\tif cmd.options.SortBy != nil {\n\t\t\toptions.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy))\n\t\t\tcopy(options.SortBy, cmd.options.SortBy)\n\t\t}\n\t\tif cmd.options.Params != nil {\n\t\t\toptions.Params = make(map[string]interface{}, len(cmd.options.Params))\n\t\t\tfor k, v := range cmd.options.Params {\n\t\t\t\toptions.Params[k] = v\n\t\t\t}\n\t\t}\n\t\tif cmd.options.WithCursorOptions != nil {\n\t\t\toptions.WithCursorOptions = &FTHybridWithCursor{\n\t\t\t\tMaxIdle: cmd.options.WithCursorOptions.MaxIdle,\n\t\t\t\tCount:   cmd.options.WithCursorOptions.Count,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &FTHybridCmd{\n\t\tbaseCmd:    cmd.cloneBaseCmd(),\n\t\tval:        val,\n\t\tcursorVal:  cursorVal,\n\t\toptions:    options,\n\t\twithCursor: cmd.withCursor,\n\t}\n}\n\n// FTSearch - Executes a search query on an index.\n// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.\n// For more information, please refer to the Redis documentation about [FT.SEARCH].\n//\n// [FT.SEARCH]: (https://redis.io/commands/ft.search/)\nfunc (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd {\n\targs := []interface{}{\"FT.SEARCH\", index, query}\n\tcmd := newFTSearchCmd(ctx, &FTSearchOptions{}, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype SearchQuery []interface{}\n\n// FTSearchQuery - Executes a search query on an index with additional options.\n// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,\n// and the 'options' parameter specifies additional options for the search.\n// For more information, please refer to the Redis documentation about [FT.SEARCH].\n//\n// [FT.SEARCH]: (https://redis.io/commands/ft.search/)\nfunc FTSearchQuery(query string, options *FTSearchOptions) (SearchQuery, error) {\n\tqueryArgs := []interface{}{query}\n\tif options != nil {\n\t\tif options.NoContent {\n\t\t\tqueryArgs = append(queryArgs, \"NOCONTENT\")\n\t\t}\n\t\tif options.Verbatim {\n\t\t\tqueryArgs = append(queryArgs, \"VERBATIM\")\n\t\t}\n\t\tif options.NoStopWords {\n\t\t\tqueryArgs = append(queryArgs, \"NOSTOPWORDS\")\n\t\t}\n\t\tif options.WithScores {\n\t\t\tqueryArgs = append(queryArgs, \"WITHSCORES\")\n\t\t}\n\t\tif options.WithPayloads {\n\t\t\tqueryArgs = append(queryArgs, \"WITHPAYLOADS\")\n\t\t}\n\t\tif options.WithSortKeys {\n\t\t\tqueryArgs = append(queryArgs, \"WITHSORTKEYS\")\n\t\t}\n\t\tif options.Filters != nil {\n\t\t\tfor _, filter := range options.Filters {\n\t\t\t\tqueryArgs = append(queryArgs, \"FILTER\", filter.FieldName, filter.Min, filter.Max)\n\t\t\t}\n\t\t}\n\t\tif options.GeoFilter != nil {\n\t\t\tfor _, geoFilter := range options.GeoFilter {\n\t\t\t\tqueryArgs = append(queryArgs, \"GEOFILTER\", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit)\n\t\t\t}\n\t\t}\n\t\tif options.InKeys != nil {\n\t\t\tqueryArgs = append(queryArgs, \"INKEYS\", len(options.InKeys))\n\t\t\tqueryArgs = append(queryArgs, options.InKeys...)\n\t\t}\n\t\tif options.InFields != nil {\n\t\t\tqueryArgs = append(queryArgs, \"INFIELDS\", len(options.InFields))\n\t\t\tqueryArgs = append(queryArgs, options.InFields...)\n\t\t}\n\t\tif options.Return != nil {\n\t\t\tqueryArgs = append(queryArgs, \"RETURN\")\n\t\t\tqueryArgsReturn := []interface{}{}\n\t\t\tfor _, ret := range options.Return {\n\t\t\t\tqueryArgsReturn = append(queryArgsReturn, ret.FieldName)\n\t\t\t\tif ret.As != \"\" {\n\t\t\t\t\tqueryArgsReturn = append(queryArgsReturn, \"AS\", ret.As)\n\t\t\t\t}\n\t\t\t}\n\t\t\tqueryArgs = append(queryArgs, len(queryArgsReturn))\n\t\t\tqueryArgs = append(queryArgs, queryArgsReturn...)\n\t\t}\n\t\tif options.Slop > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"SLOP\", options.Slop)\n\t\t}\n\t\tif options.Timeout > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"TIMEOUT\", options.Timeout)\n\t\t}\n\t\tif options.InOrder {\n\t\t\tqueryArgs = append(queryArgs, \"INORDER\")\n\t\t}\n\t\tif options.Language != \"\" {\n\t\t\tqueryArgs = append(queryArgs, \"LANGUAGE\", options.Language)\n\t\t}\n\t\tif options.Expander != \"\" {\n\t\t\tqueryArgs = append(queryArgs, \"EXPANDER\", options.Expander)\n\t\t}\n\t\tif options.Scorer != \"\" {\n\t\t\tqueryArgs = append(queryArgs, \"SCORER\", options.Scorer)\n\t\t}\n\t\tif options.ExplainScore {\n\t\t\tqueryArgs = append(queryArgs, \"EXPLAINSCORE\")\n\t\t}\n\t\tif options.Payload != \"\" {\n\t\t\tqueryArgs = append(queryArgs, \"PAYLOAD\", options.Payload)\n\t\t}\n\t\tif options.SortBy != nil {\n\t\t\tqueryArgs = append(queryArgs, \"SORTBY\")\n\t\t\tfor _, sortBy := range options.SortBy {\n\t\t\t\tqueryArgs = append(queryArgs, sortBy.FieldName)\n\t\t\t\tif sortBy.Asc && sortBy.Desc {\n\t\t\t\t\treturn nil, fmt.Errorf(\"FT.SEARCH: ASC and DESC are mutually exclusive\")\n\t\t\t\t}\n\t\t\t\tif sortBy.Asc {\n\t\t\t\t\tqueryArgs = append(queryArgs, \"ASC\")\n\t\t\t\t}\n\t\t\t\tif sortBy.Desc {\n\t\t\t\t\tqueryArgs = append(queryArgs, \"DESC\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tif options.SortByWithCount {\n\t\t\t\tqueryArgs = append(queryArgs, \"WITHCOUNT\")\n\t\t\t}\n\t\t}\n\t\tif options.LimitOffset >= 0 && options.Limit > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"LIMIT\", options.LimitOffset, options.Limit)\n\t\t}\n\t\tif options.Params != nil {\n\t\t\tqueryArgs = append(queryArgs, \"PARAMS\", len(options.Params)*2)\n\t\t\tfor key, value := range options.Params {\n\t\t\t\tqueryArgs = append(queryArgs, key, value)\n\t\t\t}\n\t\t}\n\t\tif options.DialectVersion > 0 {\n\t\t\tqueryArgs = append(queryArgs, \"DIALECT\", options.DialectVersion)\n\t\t} else {\n\t\t\tqueryArgs = append(queryArgs, \"DIALECT\", 2)\n\t\t}\n\t}\n\treturn queryArgs, nil\n}\n\n// FTSearchWithArgs - Executes a search query on an index with additional options.\n// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query,\n// and the 'options' parameter specifies additional options for the search.\n// For more information, please refer to the Redis documentation about [FT.SEARCH].\n//\n// [FT.SEARCH]: (https://redis.io/commands/ft.search/)\nfunc (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd {\n\targs := []interface{}{\"FT.SEARCH\", index, query}\n\tif options != nil {\n\t\tif options.NoContent {\n\t\t\targs = append(args, \"NOCONTENT\")\n\t\t}\n\t\tif options.Verbatim {\n\t\t\targs = append(args, \"VERBATIM\")\n\t\t}\n\t\tif options.NoStopWords {\n\t\t\targs = append(args, \"NOSTOPWORDS\")\n\t\t}\n\t\tif options.WithScores {\n\t\t\targs = append(args, \"WITHSCORES\")\n\t\t}\n\t\tif options.WithPayloads {\n\t\t\targs = append(args, \"WITHPAYLOADS\")\n\t\t}\n\t\tif options.WithSortKeys {\n\t\t\targs = append(args, \"WITHSORTKEYS\")\n\t\t}\n\t\tif options.Filters != nil {\n\t\t\tfor _, filter := range options.Filters {\n\t\t\t\targs = append(args, \"FILTER\", filter.FieldName, filter.Min, filter.Max)\n\t\t\t}\n\t\t}\n\t\tif options.GeoFilter != nil {\n\t\t\tfor _, geoFilter := range options.GeoFilter {\n\t\t\t\targs = append(args, \"GEOFILTER\", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit)\n\t\t\t}\n\t\t}\n\t\tif options.InKeys != nil {\n\t\t\targs = append(args, \"INKEYS\", len(options.InKeys))\n\t\t\targs = append(args, options.InKeys...)\n\t\t}\n\t\tif options.InFields != nil {\n\t\t\targs = append(args, \"INFIELDS\", len(options.InFields))\n\t\t\targs = append(args, options.InFields...)\n\t\t}\n\t\tif options.Return != nil {\n\t\t\targs = append(args, \"RETURN\")\n\t\t\targsReturn := []interface{}{}\n\t\t\tfor _, ret := range options.Return {\n\t\t\t\targsReturn = append(argsReturn, ret.FieldName)\n\t\t\t\tif ret.As != \"\" {\n\t\t\t\t\targsReturn = append(argsReturn, \"AS\", ret.As)\n\t\t\t\t}\n\t\t\t}\n\t\t\targs = append(args, len(argsReturn))\n\t\t\targs = append(args, argsReturn...)\n\t\t}\n\t\tif options.Slop > 0 {\n\t\t\targs = append(args, \"SLOP\", options.Slop)\n\t\t}\n\t\tif options.Timeout > 0 {\n\t\t\targs = append(args, \"TIMEOUT\", options.Timeout)\n\t\t}\n\t\tif options.InOrder {\n\t\t\targs = append(args, \"INORDER\")\n\t\t}\n\t\tif options.Language != \"\" {\n\t\t\targs = append(args, \"LANGUAGE\", options.Language)\n\t\t}\n\t\tif options.Expander != \"\" {\n\t\t\targs = append(args, \"EXPANDER\", options.Expander)\n\t\t}\n\t\tif options.Scorer != \"\" {\n\t\t\targs = append(args, \"SCORER\", options.Scorer)\n\t\t}\n\t\tif options.ExplainScore {\n\t\t\targs = append(args, \"EXPLAINSCORE\")\n\t\t}\n\t\tif options.Payload != \"\" {\n\t\t\targs = append(args, \"PAYLOAD\", options.Payload)\n\t\t}\n\t\tif options.SortBy != nil {\n\t\t\targs = append(args, \"SORTBY\")\n\t\t\tfor _, sortBy := range options.SortBy {\n\t\t\t\targs = append(args, sortBy.FieldName)\n\t\t\t\tif sortBy.Asc && sortBy.Desc {\n\t\t\t\t\tcmd := newFTSearchCmd(ctx, options, args...)\n\t\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.SEARCH: ASC and DESC are mutually exclusive\"))\n\t\t\t\t\treturn cmd\n\t\t\t\t}\n\t\t\t\tif sortBy.Asc {\n\t\t\t\t\targs = append(args, \"ASC\")\n\t\t\t\t}\n\t\t\t\tif sortBy.Desc {\n\t\t\t\t\targs = append(args, \"DESC\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tif options.SortByWithCount {\n\t\t\t\targs = append(args, \"WITHCOUNT\")\n\t\t\t}\n\t\t}\n\t\tif options.CountOnly {\n\t\t\targs = append(args, \"LIMIT\", 0, 0)\n\t\t} else {\n\t\t\tif options.LimitOffset >= 0 && options.Limit > 0 || options.LimitOffset > 0 && options.Limit == 0 {\n\t\t\t\targs = append(args, \"LIMIT\", options.LimitOffset, options.Limit)\n\t\t\t}\n\t\t}\n\t\tif options.Params != nil {\n\t\t\targs = append(args, \"PARAMS\", len(options.Params)*2)\n\t\t\tfor key, value := range options.Params {\n\t\t\t\targs = append(args, key, value)\n\t\t\t}\n\t\t}\n\t\tif options.DialectVersion > 0 {\n\t\t\targs = append(args, \"DIALECT\", options.DialectVersion)\n\t\t} else {\n\t\t\targs = append(args, \"DIALECT\", 2)\n\t\t}\n\t}\n\tcmd := newFTSearchCmd(ctx, options, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd {\n\treturn &FTSynDumpCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeFTSynDump,\n\t\t},\n\t}\n}\n\nfunc (cmd *FTSynDumpCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *FTSynDumpCmd) SetVal(val []FTSynDumpResult) {\n\tcmd.val = val\n}\n\nfunc (cmd *FTSynDumpCmd) Val() []FTSynDumpResult {\n\treturn cmd.val\n}\n\nfunc (cmd *FTSynDumpCmd) Result() ([]FTSynDumpResult, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *FTSynDumpCmd) RawVal() interface{} {\n\treturn cmd.rawVal\n}\n\nfunc (cmd *FTSynDumpCmd) RawResult() (interface{}, error) {\n\treturn cmd.rawVal, cmd.err\n}\n\nfunc (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error {\n\ttermSynonymPairs, err := rd.ReadSlice()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results []FTSynDumpResult\n\tfor i := 0; i < len(termSynonymPairs); i += 2 {\n\t\tterm, ok := termSynonymPairs[i].(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"invalid term format\")\n\t\t}\n\n\t\tsynonyms, ok := termSynonymPairs[i+1].([]interface{})\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"invalid synonyms format\")\n\t\t}\n\n\t\tsynonymList := make([]string, len(synonyms))\n\t\tfor j, syn := range synonyms {\n\t\t\tsynonym, ok := syn.(string)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"invalid synonym format\")\n\t\t\t}\n\t\t\tsynonymList[j] = synonym\n\t\t}\n\n\t\tresults = append(results, FTSynDumpResult{\n\t\t\tTerm:     term,\n\t\t\tSynonyms: synonymList,\n\t\t})\n\t}\n\n\tcmd.val = results\n\treturn nil\n}\n\nfunc (cmd *FTSynDumpCmd) Clone() Cmder {\n\tvar val []FTSynDumpResult\n\tif cmd.val != nil {\n\t\tval = make([]FTSynDumpResult, len(cmd.val))\n\t\tfor i, result := range cmd.val {\n\t\t\tval[i] = FTSynDumpResult{\n\t\t\t\tTerm: result.Term,\n\t\t\t}\n\t\t\tif result.Synonyms != nil {\n\t\t\t\tval[i].Synonyms = make([]string, len(result.Synonyms))\n\t\t\t\tcopy(val[i].Synonyms, result.Synonyms)\n\t\t\t}\n\t\t}\n\t}\n\treturn &FTSynDumpCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// FTSynDump - Dumps the contents of a synonym group.\n// The 'index' parameter specifies the index to dump.\n// For more information, please refer to the Redis documentation:\n// [FT.SYNDUMP]: (https://redis.io/commands/ft.syndump/)\nfunc (c cmdable) FTSynDump(ctx context.Context, index string) *FTSynDumpCmd {\n\tcmd := NewFTSynDumpCmd(ctx, \"FT.SYNDUMP\", index)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTSynUpdate - Creates or updates a synonym group with additional terms.\n// The 'index' parameter specifies the index to update, the 'synGroupId' parameter specifies the synonym group id, and the 'terms' parameter specifies the additional terms.\n// For more information, please refer to the Redis documentation:\n// [FT.SYNUPDATE]: (https://redis.io/commands/ft.synupdate/)\nfunc (c cmdable) FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd {\n\targs := []interface{}{\"FT.SYNUPDATE\", index, synGroupId}\n\targs = append(args, terms...)\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTSynUpdateWithArgs - Creates or updates a synonym group with additional terms and options.\n// The 'index' parameter specifies the index to update, the 'synGroupId' parameter specifies the synonym group id, the 'options' parameter specifies additional options for the update, and the 'terms' parameter specifies the additional terms.\n// For more information, please refer to the Redis documentation:\n// [FT.SYNUPDATE]: (https://redis.io/commands/ft.synupdate/)\nfunc (c cmdable) FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd {\n\targs := []interface{}{\"FT.SYNUPDATE\", index, synGroupId}\n\tif options.SkipInitialScan {\n\t\targs = append(args, \"SKIPINITIALSCAN\")\n\t}\n\targs = append(args, terms...)\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTTagVals - Returns all distinct values indexed in a tag field.\n// The 'index' parameter specifies the index to check, and the 'field' parameter specifies the tag field to retrieve values from.\n// For more information, please refer to the Redis documentation:\n// [FT.TAGVALS]: (https://redis.io/commands/ft.tagvals/)\nfunc (c cmdable) FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"FT.TAGVALS\", index, field)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// FTHybrid - Executes a hybrid search combining full-text search and vector similarity\n// The 'index' parameter specifies the index to search, 'searchExpr' is the search query,\n// 'vectorField' is the name of the vector field, and 'vectorData' is the vector to search with.\n// FTHybrid is still experimental, the command behaviour and signature may change\nfunc (c cmdable) FTHybrid(ctx context.Context, index string, searchExpr string, vectorField string, vectorData Vector) *FTHybridCmd {\n\toptions := &FTHybridOptions{\n\t\tCountExpressions: 2,\n\t\tSearchExpressions: []FTHybridSearchExpression{\n\t\t\t{Query: searchExpr},\n\t\t},\n\t\tVectorExpressions: []FTHybridVectorExpression{\n\t\t\t{VectorField: vectorField, VectorData: vectorData},\n\t\t},\n\t}\n\treturn c.FTHybridWithArgs(ctx, index, options)\n}\n\n// FTHybridWithArgs - Executes a hybrid search with advanced options\n// FTHybridWithArgs is still experimental, the command behaviour and signature may change\nfunc (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FTHybridOptions) *FTHybridCmd {\n\targs := []interface{}{\"FT.HYBRID\", index}\n\n\tif options != nil {\n\t\t// Add search expressions\n\t\tfor _, searchExpr := range options.SearchExpressions {\n\t\t\targs = append(args, \"SEARCH\", searchExpr.Query)\n\n\t\t\tif searchExpr.Scorer != \"\" {\n\t\t\t\targs = append(args, \"SCORER\", searchExpr.Scorer)\n\t\t\t\tif len(searchExpr.ScorerParams) > 0 {\n\t\t\t\t\targs = append(args, searchExpr.ScorerParams...)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif searchExpr.YieldScoreAs != \"\" {\n\t\t\t\targs = append(args, \"YIELD_SCORE_AS\", searchExpr.YieldScoreAs)\n\t\t\t}\n\t\t}\n\n\t\t// Add vector expressions\n\t\tfor _, vectorExpr := range options.VectorExpressions {\n\t\t\targs = append(args, \"VSIM\", \"@\"+vectorExpr.VectorField)\n\n\t\t\t// For FT.HYBRID, we need to send just the raw vector bytes, not the Value() format\n\t\t\t// Value() returns [format, data] but FT.HYBRID expects just the blob\n\t\t\tvectorValue := vectorExpr.VectorData.Value()\n\t\t\tvar vectorBlob interface{}\n\t\t\tif len(vectorValue) >= 2 {\n\t\t\t\t// vectorValue is [format, data, ...] - we only want the data part\n\t\t\t\tvectorBlob = vectorValue[1]\n\t\t\t} else {\n\t\t\t\t// Fallback for unexpected format\n\t\t\t\tvectorBlob = vectorValue\n\t\t\t}\n\n\t\t\t// If VectorParamName is provided, use PARAMS mechanism (required for Redis 8.6+)\n\t\t\t// If not provided, inline the vector blob (works on Redis 8.4/8.5, fails on 8.6+)\n\t\t\tif vectorExpr.VectorParamName != \"\" {\n\t\t\t\t// Use PARAMS mechanism\n\t\t\t\targs = append(args, \"$\"+vectorExpr.VectorParamName)\n\t\t\t\tif options.Params == nil {\n\t\t\t\t\toptions.Params = make(map[string]interface{})\n\t\t\t\t}\n\t\t\t\toptions.Params[vectorExpr.VectorParamName] = vectorBlob\n\t\t\t} else {\n\t\t\t\t// Inline the vector blob (deprecated in Redis 8.6+)\n\t\t\t\targs = append(args, vectorBlob)\n\t\t\t}\n\n\t\t\tif vectorExpr.Method != \"\" {\n\t\t\t\targs = append(args, vectorExpr.Method)\n\t\t\t\tif len(vectorExpr.MethodParams) > 0 {\n\t\t\t\t\t// MethodParams should be key-value pairs, count them\n\t\t\t\t\targs = append(args, len(vectorExpr.MethodParams))\n\t\t\t\t\targs = append(args, vectorExpr.MethodParams...)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif vectorExpr.Filter != \"\" {\n\t\t\t\targs = append(args, \"FILTER\", vectorExpr.Filter)\n\t\t\t}\n\n\t\t\tif vectorExpr.YieldScoreAs != \"\" {\n\t\t\t\targs = append(args, \"YIELD_SCORE_AS\", vectorExpr.YieldScoreAs)\n\t\t\t}\n\t\t}\n\n\t\t// Add combine/fusion options\n\t\tif options.Combine != nil {\n\t\t\t// Build combine parameters\n\t\t\tcombineParams := []interface{}{}\n\n\t\t\tswitch options.Combine.Method {\n\t\t\tcase FTHybridCombineRRF:\n\t\t\t\tif options.Combine.Window > 0 {\n\t\t\t\t\tcombineParams = append(combineParams, \"WINDOW\", options.Combine.Window)\n\t\t\t\t}\n\t\t\t\tif options.Combine.Constant > 0 {\n\t\t\t\t\tcombineParams = append(combineParams, \"CONSTANT\", options.Combine.Constant)\n\t\t\t\t}\n\t\t\tcase FTHybridCombineLinear:\n\t\t\t\tif options.Combine.Alpha > 0 {\n\t\t\t\t\tcombineParams = append(combineParams, \"ALPHA\", options.Combine.Alpha)\n\t\t\t\t}\n\t\t\t\tif options.Combine.Beta > 0 {\n\t\t\t\t\tcombineParams = append(combineParams, \"BETA\", options.Combine.Beta)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif options.Combine.YieldScoreAs != \"\" {\n\t\t\t\tcombineParams = append(combineParams, \"YIELD_SCORE_AS\", options.Combine.YieldScoreAs)\n\t\t\t}\n\n\t\t\t// Add COMBINE with method and parameter count\n\t\t\targs = append(args, \"COMBINE\", string(options.Combine.Method))\n\t\t\tif len(combineParams) > 0 {\n\t\t\t\targs = append(args, len(combineParams))\n\t\t\t\targs = append(args, combineParams...)\n\t\t\t}\n\t\t}\n\n\t\t// Add LOAD (projected fields)\n\t\tif len(options.Load) > 0 {\n\t\t\targs = append(args, \"LOAD\", len(options.Load))\n\t\t\tfor _, field := range options.Load {\n\t\t\t\targs = append(args, field)\n\t\t\t}\n\t\t}\n\n\t\t// Add GROUPBY\n\t\tif options.GroupBy != nil {\n\t\t\targs = append(args, \"GROUPBY\", options.GroupBy.Count)\n\t\t\tfor _, field := range options.GroupBy.Fields {\n\t\t\t\targs = append(args, field)\n\t\t\t}\n\t\t\tif options.GroupBy.ReduceFunc != \"\" {\n\t\t\t\targs = append(args, \"REDUCE\", options.GroupBy.ReduceFunc, options.GroupBy.ReduceCount)\n\t\t\t\targs = append(args, options.GroupBy.ReduceParams...)\n\t\t\t}\n\t\t}\n\n\t\t// Add APPLY transformations\n\t\tfor _, apply := range options.Apply {\n\t\t\targs = append(args, \"APPLY\", apply.Expression, \"AS\", apply.AsField)\n\t\t}\n\n\t\t// Add SORTBY\n\t\tif len(options.SortBy) > 0 {\n\t\t\tsortByOptions := []interface{}{}\n\t\t\tfor _, sortBy := range options.SortBy {\n\t\t\t\tsortByOptions = append(sortByOptions, sortBy.FieldName)\n\t\t\t\tif sortBy.Asc && sortBy.Desc {\n\t\t\t\t\tcmd := newFTHybridCmd(ctx, options, args...)\n\t\t\t\t\tcmd.SetErr(fmt.Errorf(\"FT.HYBRID: ASC and DESC are mutually exclusive\"))\n\t\t\t\t\treturn cmd\n\t\t\t\t}\n\t\t\t\tif sortBy.Asc {\n\t\t\t\t\tsortByOptions = append(sortByOptions, \"ASC\")\n\t\t\t\t}\n\t\t\t\tif sortBy.Desc {\n\t\t\t\t\tsortByOptions = append(sortByOptions, \"DESC\")\n\t\t\t\t}\n\t\t\t}\n\t\t\targs = append(args, \"SORTBY\", len(sortByOptions))\n\t\t\targs = append(args, sortByOptions...)\n\t\t}\n\n\t\t// Add FILTER (post-filter)\n\t\tif options.Filter != \"\" {\n\t\t\targs = append(args, \"FILTER\", options.Filter)\n\t\t}\n\n\t\t// Add LIMIT\n\t\tif options.LimitOffset >= 0 && options.Limit > 0 || options.LimitOffset > 0 && options.Limit == 0 {\n\t\t\targs = append(args, \"LIMIT\", options.LimitOffset, options.Limit)\n\t\t}\n\n\t\t// Add PARAMS\n\t\tif len(options.Params) > 0 {\n\t\t\targs = append(args, \"PARAMS\", len(options.Params)*2)\n\t\t\tfor key, value := range options.Params {\n\t\t\t\t// Parameter keys should already have '$' prefix from the user\n\t\t\t\t// Don't add it again if it's already there\n\t\t\t\targs = append(args, key, value)\n\t\t\t}\n\t\t}\n\n\t\t// Add EXPLAINSCORE\n\t\tif options.ExplainScore {\n\t\t\targs = append(args, \"EXPLAINSCORE\")\n\t\t}\n\n\t\t// Add TIMEOUT\n\t\tif options.Timeout > 0 {\n\t\t\targs = append(args, \"TIMEOUT\", options.Timeout)\n\t\t}\n\n\t\t// Add WITHCURSOR support\n\t\tif options.WithCursor {\n\t\t\targs = append(args, \"WITHCURSOR\")\n\t\t\tif options.WithCursorOptions != nil {\n\t\t\t\tif options.WithCursorOptions.Count > 0 {\n\t\t\t\t\targs = append(args, \"COUNT\", options.WithCursorOptions.Count)\n\t\t\t\t}\n\t\t\t\tif options.WithCursorOptions.MaxIdle > 0 {\n\t\t\t\t\targs = append(args, \"MAXIDLE\", options.WithCursorOptions.MaxIdle)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tcmd := newFTHybridCmd(ctx, options, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "search_commands_parse_test.go",
    "content": "package redis\n\nimport (\n\t\"testing\"\n)\n\n// TestParseFTInfo tests the parseFTInfo function with a comprehensive FT.INFO response\n// This test uses the actual response structure from Redis with vector fields\nfunc TestParseFTInfo(t *testing.T) {\n\t// This is the data structure that would be returned by Redis for FT.INFO\n\t// Based on the redis-cli output provided by the user\n\tdata := map[string]interface{}{\n\t\t\"index_name\":    \"rand:42d1f2820b3048b6bc783d4dcdb9094a\",\n\t\t\"index_options\": []interface{}{},\n\t\t\"index_definition\": []interface{}{\n\t\t\t\"key_type\", \"HASH\",\n\t\t\t\"prefixes\", []interface{}{\"rand:42d1f2820b3048b6bc783d4dcdb9094a\"},\n\t\t\t\"default_score\", \"1\",\n\t\t\t\"indexes_all\", false,\n\t\t},\n\t\t\"attributes\": []interface{}{\n\t\t\t// prompt field (TEXT)\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"prompt\",\n\t\t\t\t\"attribute\", \"prompt\",\n\t\t\t\t\"type\", \"TEXT\",\n\t\t\t\t\"WEIGHT\", \"1\",\n\t\t\t},\n\t\t\t// response field (TEXT)\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"response\",\n\t\t\t\t\"attribute\", \"response\",\n\t\t\t\t\"type\", \"TEXT\",\n\t\t\t\t\"WEIGHT\", \"1\",\n\t\t\t},\n\t\t\t// exact_digest field (TAG)\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"exact_digest\",\n\t\t\t\t\"attribute\", \"exact_digest\",\n\t\t\t\t\"type\", \"TAG\",\n\t\t\t\t\"SEPARATOR\", \",\",\n\t\t\t},\n\t\t\t// prompt_vector field (VECTOR)\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"prompt_vector\",\n\t\t\t\t\"attribute\", \"prompt_vector\",\n\t\t\t\t\"type\", \"VECTOR\",\n\t\t\t\t\"algorithm\", \"HNSW\",\n\t\t\t\t\"data_type\", \"FLOAT32\",\n\t\t\t\t\"dim\", int64(1536),\n\t\t\t\t\"distance_metric\", \"COSINE\",\n\t\t\t\t\"M\", int64(16),\n\t\t\t\t\"ef_construction\", int64(64),\n\t\t\t},\n\t\t},\n\t\t\"num_docs\":                    int64(0),\n\t\t\"max_doc_id\":                  int64(0),\n\t\t\"num_terms\":                   int64(0),\n\t\t\"num_records\":                 int64(0),\n\t\t\"inverted_sz_mb\":              \"0\",\n\t\t\"vector_index_sz_mb\":          \"0\",\n\t\t\"total_inverted_index_blocks\": int64(0),\n\t\t\"offset_vectors_sz_mb\":        \"0\",\n\t\t\"doc_table_size_mb\":           \"0.01532745361328125\",\n\t\t\"sortable_values_size_mb\":     \"0\",\n\t\t\"key_table_size_mb\":           \"2.288818359375e-5\",\n\t\t\"tag_overhead_sz_mb\":          \"0\",\n\t\t\"text_overhead_sz_mb\":         \"0\",\n\t\t\"total_index_memory_sz_mb\":    \"0.015350341796875\",\n\t\t\"geoshapes_sz_mb\":             \"0\",\n\t\t\"records_per_doc_avg\":         \"nan\",\n\t\t\"bytes_per_record_avg\":        \"nan\",\n\t\t\"offsets_per_term_avg\":        \"nan\",\n\t\t\"offset_bits_per_record_avg\":  \"nan\",\n\t\t\"hash_indexing_failures\":      int64(0),\n\t\t\"total_indexing_time\":         \"0\",\n\t\t\"indexing\":                    int64(0),\n\t\t\"percent_indexed\":             \"1\",\n\t\t\"number_of_uses\":              int64(2),\n\t\t\"cleaning\":                    int64(0),\n\t\t\"gc_stats\": []interface{}{\n\t\t\t\"bytes_collected\", \"0\",\n\t\t\t\"total_ms_run\", \"0\",\n\t\t\t\"total_cycles\", \"0\",\n\t\t\t\"average_cycle_time_ms\", \"nan\",\n\t\t\t\"last_run_time_ms\", \"0\",\n\t\t\t\"gc_numeric_trees_missed\", \"0\",\n\t\t\t\"gc_blocks_denied\", \"0\",\n\t\t},\n\t\t\"cursor_stats\": []interface{}{\n\t\t\t\"global_idle\", int64(0),\n\t\t\t\"global_total\", int64(0),\n\t\t\t\"index_capacity\", int64(128),\n\t\t\t\"index_total\", int64(0),\n\t\t},\n\t\t\"dialect_stats\": []interface{}{\n\t\t\t\"dialect_1\", int64(0),\n\t\t\t\"dialect_2\", int64(0),\n\t\t\t\"dialect_3\", int64(0),\n\t\t\t\"dialect_4\", int64(0),\n\t\t},\n\t\t\"Index Errors\": []interface{}{\n\t\t\t\"indexing failures\", int64(0),\n\t\t\t\"last indexing error\", \"N/A\",\n\t\t\t\"last indexing error key\", \"N/A\",\n\t\t\t\"background indexing status\", \"OK\",\n\t\t},\n\t\t\"field statistics\": []interface{}{\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"prompt\",\n\t\t\t\t\"attribute\", \"prompt\",\n\t\t\t\t\"Index Errors\", []interface{}{\n\t\t\t\t\t\"indexing failures\", int64(0),\n\t\t\t\t\t\"last indexing error\", \"N/A\",\n\t\t\t\t\t\"last indexing error key\", \"N/A\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"response\",\n\t\t\t\t\"attribute\", \"response\",\n\t\t\t\t\"Index Errors\", []interface{}{\n\t\t\t\t\t\"indexing failures\", int64(0),\n\t\t\t\t\t\"last indexing error\", \"N/A\",\n\t\t\t\t\t\"last indexing error key\", \"N/A\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"exact_digest\",\n\t\t\t\t\"attribute\", \"exact_digest\",\n\t\t\t\t\"Index Errors\", []interface{}{\n\t\t\t\t\t\"indexing failures\", int64(0),\n\t\t\t\t\t\"last indexing error\", \"N/A\",\n\t\t\t\t\t\"last indexing error key\", \"N/A\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]interface{}{\n\t\t\t\t\"identifier\", \"prompt_vector\",\n\t\t\t\t\"attribute\", \"prompt_vector\",\n\t\t\t\t\"Index Errors\", []interface{}{\n\t\t\t\t\t\"indexing failures\", int64(0),\n\t\t\t\t\t\"last indexing error\", \"N/A\",\n\t\t\t\t\t\"last indexing error key\", \"N/A\",\n\t\t\t\t},\n\t\t\t\t\"memory\", int64(0),\n\t\t\t\t\"marked_deleted\", int64(0),\n\t\t\t},\n\t\t},\n\t}\n\n\t// Parse the data\n\tresult, err := parseFTInfo(data)\n\tif err != nil {\n\t\tt.Fatalf(\"parseFTInfo failed: %v\", err)\n\t}\n\n\t// Validate index name\n\tif result.IndexName != \"rand:42d1f2820b3048b6bc783d4dcdb9094a\" {\n\t\tt.Errorf(\"IndexName = %v, want %v\", result.IndexName, \"rand:42d1f2820b3048b6bc783d4dcdb9094a\")\n\t}\n\n\t// Validate index definition\n\tif result.IndexDefinition.KeyType != \"HASH\" {\n\t\tt.Errorf(\"IndexDefinition.KeyType = %v, want HASH\", result.IndexDefinition.KeyType)\n\t}\n\tif len(result.IndexDefinition.Prefixes) != 1 || result.IndexDefinition.Prefixes[0] != \"rand:42d1f2820b3048b6bc783d4dcdb9094a\" {\n\t\tt.Errorf(\"IndexDefinition.Prefixes = %v, want [rand:42d1f2820b3048b6bc783d4dcdb9094a]\", result.IndexDefinition.Prefixes)\n\t}\n\tif result.IndexDefinition.DefaultScore != 1.0 {\n\t\tt.Errorf(\"IndexDefinition.DefaultScore = %v, want 1.0\", result.IndexDefinition.DefaultScore)\n\t}\n\n\t// Validate attributes\n\tif len(result.Attributes) != 4 {\n\t\tt.Fatalf(\"len(Attributes) = %v, want 4\", len(result.Attributes))\n\t}\n\n\t// Check prompt field (TEXT)\n\tpromptAttr := result.Attributes[0]\n\tif promptAttr.Identifier != \"prompt\" {\n\t\tt.Errorf(\"Attributes[0].Identifier = %v, want prompt\", promptAttr.Identifier)\n\t}\n\tif promptAttr.Attribute != \"prompt\" {\n\t\tt.Errorf(\"Attributes[0].Attribute = %v, want prompt\", promptAttr.Attribute)\n\t}\n\tif promptAttr.Type != \"TEXT\" {\n\t\tt.Errorf(\"Attributes[0].Type = %v, want TEXT\", promptAttr.Type)\n\t}\n\tif promptAttr.Weight != 1.0 {\n\t\tt.Errorf(\"Attributes[0].Weight = %v, want 1.0\", promptAttr.Weight)\n\t}\n\n\t// Check response field (TEXT)\n\tresponseAttr := result.Attributes[1]\n\tif responseAttr.Identifier != \"response\" {\n\t\tt.Errorf(\"Attributes[1].Identifier = %v, want response\", responseAttr.Identifier)\n\t}\n\tif responseAttr.Attribute != \"response\" {\n\t\tt.Errorf(\"Attributes[1].Attribute = %v, want response\", responseAttr.Attribute)\n\t}\n\tif responseAttr.Type != \"TEXT\" {\n\t\tt.Errorf(\"Attributes[1].Type = %v, want TEXT\", responseAttr.Type)\n\t}\n\n\t// Check exact_digest field (TAG)\n\ttagAttr := result.Attributes[2]\n\tif tagAttr.Identifier != \"exact_digest\" {\n\t\tt.Errorf(\"Attributes[2].Identifier = %v, want exact_digest\", tagAttr.Identifier)\n\t}\n\tif tagAttr.Attribute != \"exact_digest\" {\n\t\tt.Errorf(\"Attributes[2].Attribute = %v, want exact_digest\", tagAttr.Attribute)\n\t}\n\tif tagAttr.Type != \"TAG\" {\n\t\tt.Errorf(\"Attributes[2].Type = %v, want TAG\", tagAttr.Type)\n\t}\n\n\t// Check prompt_vector field (VECTOR)\n\tvectorAttr := result.Attributes[3]\n\tif vectorAttr.Identifier != \"prompt_vector\" {\n\t\tt.Errorf(\"Attributes[3].Identifier = %v, want prompt_vector\", vectorAttr.Identifier)\n\t}\n\tif vectorAttr.Attribute != \"prompt_vector\" {\n\t\tt.Errorf(\"Attributes[3].Attribute = %v, want prompt_vector\", vectorAttr.Attribute)\n\t}\n\tif vectorAttr.Type != \"VECTOR\" {\n\t\tt.Errorf(\"Attributes[3].Type = %v, want VECTOR\", vectorAttr.Type)\n\t}\n\tif vectorAttr.Algorithm != \"HNSW\" {\n\t\tt.Errorf(\"Attributes[3].Algorithm = %v, want HNSW\", vectorAttr.Algorithm)\n\t}\n\tif vectorAttr.DataType != \"FLOAT32\" {\n\t\tt.Errorf(\"Attributes[3].DataType = %v, want FLOAT32\", vectorAttr.DataType)\n\t}\n\tif vectorAttr.Dim != 1536 {\n\t\tt.Errorf(\"Attributes[3].Dim = %v, want 1536\", vectorAttr.Dim)\n\t}\n\tif vectorAttr.DistanceMetric != \"COSINE\" {\n\t\tt.Errorf(\"Attributes[3].DistanceMetric = %v, want COSINE\", vectorAttr.DistanceMetric)\n\t}\n\tif vectorAttr.M != 16 {\n\t\tt.Errorf(\"Attributes[3].M = %v, want 16\", vectorAttr.M)\n\t}\n\tif vectorAttr.EFConstruction != 64 {\n\t\tt.Errorf(\"Attributes[3].EFConstruction = %v, want 64\", vectorAttr.EFConstruction)\n\t}\n\n\t// Validate numeric fields\n\tif result.NumDocs != 0 {\n\t\tt.Errorf(\"NumDocs = %v, want 0\", result.NumDocs)\n\t}\n\tif result.MaxDocID != 0 {\n\t\tt.Errorf(\"MaxDocID = %v, want 0\", result.MaxDocID)\n\t}\n\tif result.NumTerms != 0 {\n\t\tt.Errorf(\"NumTerms = %v, want 0\", result.NumTerms)\n\t}\n\tif result.NumRecords != 0 {\n\t\tt.Errorf(\"NumRecords = %v, want 0\", result.NumRecords)\n\t}\n\tif result.Indexing != 0 {\n\t\tt.Errorf(\"Indexing = %v, want 0\", result.Indexing)\n\t}\n\tif result.PercentIndexed != 1.0 {\n\t\tt.Errorf(\"PercentIndexed = %v, want 1.0\", result.PercentIndexed)\n\t}\n\tif result.HashIndexingFailures != 0 {\n\t\tt.Errorf(\"HashIndexingFailures = %v, want 0\", result.HashIndexingFailures)\n\t}\n\tif result.Cleaning != 0 {\n\t\tt.Errorf(\"Cleaning = %v, want 0\", result.Cleaning)\n\t}\n\tif result.NumberOfUses != 2 {\n\t\tt.Errorf(\"NumberOfUses = %v, want 2\", result.NumberOfUses)\n\t}\n\n\t// Validate average stats (should be \"nan\" for empty index)\n\tif result.RecordsPerDocAvg != \"nan\" {\n\t\tt.Errorf(\"RecordsPerDocAvg = %v, want nan\", result.RecordsPerDocAvg)\n\t}\n\tif result.BytesPerRecordAvg != \"nan\" {\n\t\tt.Errorf(\"BytesPerRecordAvg = %v, want nan\", result.BytesPerRecordAvg)\n\t}\n\tif result.OffsetsPerTermAvg != \"nan\" {\n\t\tt.Errorf(\"OffsetsPerTermAvg = %v, want nan\", result.OffsetsPerTermAvg)\n\t}\n\tif result.OffsetBitsPerRecordAvg != \"nan\" {\n\t\tt.Errorf(\"OffsetBitsPerRecordAvg = %v, want nan\", result.OffsetBitsPerRecordAvg)\n\t}\n\n\t// Validate cursor stats\n\tif result.CursorStats.GlobalIdle != 0 {\n\t\tt.Errorf(\"CursorStats.GlobalIdle = %v, want 0\", result.CursorStats.GlobalIdle)\n\t}\n\tif result.CursorStats.GlobalTotal != 0 {\n\t\tt.Errorf(\"CursorStats.GlobalTotal = %v, want 0\", result.CursorStats.GlobalTotal)\n\t}\n\tif result.CursorStats.IndexCapacity != 128 {\n\t\tt.Errorf(\"CursorStats.IndexCapacity = %v, want 128\", result.CursorStats.IndexCapacity)\n\t}\n\tif result.CursorStats.IndexTotal != 0 {\n\t\tt.Errorf(\"CursorStats.IndexTotal = %v, want 0\", result.CursorStats.IndexTotal)\n\t}\n\n\t// Validate dialect stats\n\tif result.DialectStats[\"dialect_1\"] != 0 {\n\t\tt.Errorf(\"DialectStats[dialect_1] = %v, want 0\", result.DialectStats[\"dialect_1\"])\n\t}\n\tif result.DialectStats[\"dialect_2\"] != 0 {\n\t\tt.Errorf(\"DialectStats[dialect_2] = %v, want 0\", result.DialectStats[\"dialect_2\"])\n\t}\n\tif result.DialectStats[\"dialect_3\"] != 0 {\n\t\tt.Errorf(\"DialectStats[dialect_3] = %v, want 0\", result.DialectStats[\"dialect_3\"])\n\t}\n\tif result.DialectStats[\"dialect_4\"] != 0 {\n\t\tt.Errorf(\"DialectStats[dialect_4] = %v, want 0\", result.DialectStats[\"dialect_4\"])\n\t}\n\n\t// Validate GC stats\n\tif result.GCStats.BytesCollected != 0 {\n\t\tt.Errorf(\"GCStats.BytesCollected = %v, want 0\", result.GCStats.BytesCollected)\n\t}\n\tif result.GCStats.TotalMsRun != 0 {\n\t\tt.Errorf(\"GCStats.TotalMsRun = %v, want 0\", result.GCStats.TotalMsRun)\n\t}\n\tif result.GCStats.TotalCycles != 0 {\n\t\tt.Errorf(\"GCStats.TotalCycles = %v, want 0\", result.GCStats.TotalCycles)\n\t}\n\tif result.GCStats.AverageCycleTimeMs != \"nan\" {\n\t\tt.Errorf(\"GCStats.AverageCycleTimeMs = %v, want nan\", result.GCStats.AverageCycleTimeMs)\n\t}\n\n\t// Validate Index Errors\n\tif result.IndexErrors.IndexingFailures != 0 {\n\t\tt.Errorf(\"IndexErrors.IndexingFailures = %v, want 0\", result.IndexErrors.IndexingFailures)\n\t}\n\tif result.IndexErrors.LastIndexingError != \"N/A\" {\n\t\tt.Errorf(\"IndexErrors.LastIndexingError = %v, want N/A\", result.IndexErrors.LastIndexingError)\n\t}\n\tif result.IndexErrors.LastIndexingErrorKey != \"N/A\" {\n\t\tt.Errorf(\"IndexErrors.LastIndexingErrorKey = %v, want N/A\", result.IndexErrors.LastIndexingErrorKey)\n\t}\n\n\t// Validate field statistics\n\tif len(result.FieldStatistics) != 4 {\n\t\tt.Fatalf(\"len(FieldStatistics) = %v, want 4\", len(result.FieldStatistics))\n\t}\n\n\texpectedIdentifiers := map[string]bool{\n\t\t\"prompt\":        true,\n\t\t\"response\":      true,\n\t\t\"exact_digest\":  true,\n\t\t\"prompt_vector\": true,\n\t}\n\n\tfor _, fieldStat := range result.FieldStatistics {\n\t\tif !expectedIdentifiers[fieldStat.Identifier] {\n\t\t\tt.Errorf(\"Unexpected field statistic identifier: %v\", fieldStat.Identifier)\n\t\t}\n\t\tif fieldStat.IndexErrors.IndexingFailures != 0 {\n\t\t\tt.Errorf(\"FieldStatistic[%s].IndexErrors.IndexingFailures = %v, want 0\", fieldStat.Identifier, fieldStat.IndexErrors.IndexingFailures)\n\t\t}\n\t\tif fieldStat.IndexErrors.LastIndexingError != \"N/A\" {\n\t\t\tt.Errorf(\"FieldStatistic[%s].IndexErrors.LastIndexingError = %v, want N/A\", fieldStat.Identifier, fieldStat.IndexErrors.LastIndexingError)\n\t\t}\n\t\tif fieldStat.IndexErrors.LastIndexingErrorKey != \"N/A\" {\n\t\t\tt.Errorf(\"FieldStatistic[%s].IndexErrors.LastIndexingErrorKey = %v, want N/A\", fieldStat.Identifier, fieldStat.IndexErrors.LastIndexingErrorKey)\n\t\t}\n\t}\n\n\tt.Logf(\"Successfully parsed FT.INFO response with %d attributes\", len(result.Attributes))\n}\n"
  },
  {
    "path": "search_test.go",
    "content": "package redis_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/helper\"\n)\n\nfunc WaitForIndexing(c *redis.Client, index string) {\n\tfor {\n\t\tres, err := c.FTInfo(context.Background(), index).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tif c.Options().Protocol == 2 {\n\t\t\tif res.Indexing == 0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t} else {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc encodeFloat32Vector(vec []float32) []byte {\n\tbuf := new(bytes.Buffer)\n\tfor _, v := range vec {\n\t\tbinary.Write(buf, binary.LittleEndian, v)\n\t}\n\treturn buf.Bytes()\n}\n\nfunc encodeFloat16Vector(vec []float32) []byte {\n\tbuf := new(bytes.Buffer)\n\tfor _, v := range vec {\n\t\t// Convert float32 to float16 (16-bit representation)\n\t\t// This is a simplified conversion - in practice you'd use a proper float16 library\n\t\tf16 := uint16(v * 1000) // Simple scaling for test purposes\n\t\tbinary.Write(buf, binary.LittleEndian, f16)\n\t}\n\treturn buf.Bytes()\n}\n\nvar _ = Describe(\"RediSearch commands Resp 2\", Label(\"search\"), func() {\n\tctx := context.TODO()\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(&redis.Options{Addr: \":6379\", Protocol: 2})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should FTCreate and FTSearch WithScores\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo baz\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"foo bar\")\n\t\tres, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo ~bar\", &redis.FTSearchOptions{WithScores: true}).Result()\n\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(int64(2)))\n\t\tfor _, doc := range res.Docs {\n\t\t\tExpect(*doc.Score).To(BeNumerically(\">\", 0))\n\t\t\tExpect(doc.ID).To(Or(Equal(\"doc1\"), Equal(\"doc2\")))\n\t\t}\n\t})\n\n\tIt(\"should FTCreate and FTSearch stopwords\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{StopWords: []interface{}{\"foo\", \"bar\", \"baz\"}}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo baz\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"hello world\")\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo bar\", &redis.FTSearchOptions{NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(0)))\n\t\tres2, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo bar hello world\", &redis.FTSearchOptions{NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(BeEquivalentTo(int64(1)))\n\t})\n\n\tIt(\"should FTCreate and FTSearch filters\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: \"num\", FieldType: redis.SearchFieldTypeNumeric}, &redis.FieldSchema{FieldName: \"loc\", FieldType: redis.SearchFieldTypeGeo}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo bar\", \"num\", 3.141, \"loc\", \"-0.441,51.458\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"foo baz\", \"num\", 2, \"loc\", \"-0.1,51.2\")\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo\", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: \"num\", Min: 0, Max: 2}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(1)))\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"doc2\"))\n\t\tres2, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo\", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: \"num\", Min: 0, Max: \"+inf\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(BeEquivalentTo(int64(2)))\n\t\tExpect(res2.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\t// Test Geo filter\n\t\tgeoFilter1 := redis.FTSearchGeoFilter{FieldName: \"loc\", Longitude: -0.44, Latitude: 51.45, Radius: 10, Unit: \"km\"}\n\t\tgeoFilter2 := redis.FTSearchGeoFilter{FieldName: \"loc\", Longitude: -0.44, Latitude: 51.45, Radius: 100, Unit: \"km\"}\n\t\tres3, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo\", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter1}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res3.Total).To(BeEquivalentTo(int64(1)))\n\t\tExpect(res3.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tres4, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo\", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter2}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res4.Total).To(BeEquivalentTo(int64(2)))\n\t\tdocs := []interface{}{res4.Docs[0].ID, res4.Docs[1].ID}\n\t\tExpect(docs).To(ContainElement(\"doc1\"))\n\t\tExpect(docs).To(ContainElement(\"doc2\"))\n\n\t})\n\n\tIt(\"should FTCreate and FTSearch sortby\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"num\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: \"num\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"num\")\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo bar\", \"num\", 1)\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"foo baz\", \"num\", 2)\n\t\tclient.HSet(ctx, \"doc3\", \"txt\", \"foo qux\", \"num\", 3)\n\n\t\tsortBy1 := redis.FTSearchSortBy{FieldName: \"num\", Asc: true}\n\t\tsortBy2 := redis.FTSearchSortBy{FieldName: \"num\", Desc: true}\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"num\", \"foo\", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy1}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(3)))\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tExpect(res1.Docs[1].ID).To(BeEquivalentTo(\"doc2\"))\n\t\tExpect(res1.Docs[2].ID).To(BeEquivalentTo(\"doc3\"))\n\n\t\tres2, err := client.FTSearchWithArgs(ctx, \"num\", \"foo\", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(BeEquivalentTo(int64(3)))\n\t\tExpect(res2.Docs[2].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tExpect(res2.Docs[1].ID).To(BeEquivalentTo(\"doc2\"))\n\t\tExpect(res2.Docs[0].ID).To(BeEquivalentTo(\"doc3\"))\n\n\t\tres3, err := client.FTSearchWithArgs(ctx, \"num\", \"foo\", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res3.Total).To(BeEquivalentTo(int64(3)))\n\n\t\tres4, err := client.FTSearchWithArgs(ctx, \"num\", \"notpresentf00\", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res4.Total).To(BeEquivalentTo(int64(0)))\n\t})\n\n\tIt(\"should FTCreate and FTSearch example\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText, Weight: 5}, &redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tclient.HSet(ctx, \"doc1\", \"title\", \"RediSearch\", \"body\", \"Redisearch implements a search engine on top of redis\")\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"txt\", \"search engine\", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(1)))\n\n\t})\n\n\tIt(\"should FTCreate NoIndex\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"field\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"text\", FieldType: redis.SearchFieldTypeText, NoIndex: true, Sortable: true}\n\t\tnum := &redis.FieldSchema{FieldName: \"numeric\", FieldType: redis.SearchFieldTypeNumeric, NoIndex: true, Sortable: true}\n\t\tgeo := &redis.FieldSchema{FieldName: \"geo\", FieldType: redis.SearchFieldTypeGeo, NoIndex: true, Sortable: true}\n\t\ttag := &redis.FieldSchema{FieldName: \"tag\", FieldType: redis.SearchFieldTypeTag, NoIndex: true, Sortable: true}\n\t\tval, err := client.FTCreate(ctx, \"idx\", &redis.FTCreateOptions{}, text1, text2, num, geo, tag).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx\")\n\t\tclient.HSet(ctx, \"doc1\", \"field\", \"aaa\", \"text\", \"1\", \"numeric\", 1, \"geo\", \"1,1\", \"tag\", \"1\")\n\t\tclient.HSet(ctx, \"doc2\", \"field\", \"aab\", \"text\", \"2\", \"numeric\", 2, \"geo\", \"2,2\", \"tag\", \"2\")\n\t\tres1, err := client.FTSearch(ctx, \"idx\", \"@text:aa*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(0)))\n\t\tres2, err := client.FTSearch(ctx, \"idx\", \"@field:aa*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(BeEquivalentTo(int64(2)))\n\t\tres3, err := client.FTSearchWithArgs(ctx, \"idx\", \"*\", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: \"text\", Desc: true}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res3.Total).To(BeEquivalentTo(int64(2)))\n\t\tExpect(res3.Docs[0].ID).To(BeEquivalentTo(\"doc2\"))\n\t\tres4, err := client.FTSearchWithArgs(ctx, \"idx\", \"*\", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: \"text\", Asc: true}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res4.Total).To(BeEquivalentTo(int64(2)))\n\t\tExpect(res4.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tres5, err := client.FTSearchWithArgs(ctx, \"idx\", \"*\", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: \"numeric\", Asc: true}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res5.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tres6, err := client.FTSearchWithArgs(ctx, \"idx\", \"*\", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: \"geo\", Asc: true}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res6.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tres7, err := client.FTSearchWithArgs(ctx, \"idx\", \"*\", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: \"tag\", Asc: true}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res7.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\n\t})\n\n\tIt(\"should FTExplain\", Label(\"search\", \"ftexplain\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"f1\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"f2\", FieldType: redis.SearchFieldTypeText}\n\t\ttext3 := &redis.FieldSchema{FieldName: \"f3\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{}, text1, text2, text3).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tres1, err := client.FTExplain(ctx, \"txt\", \"@f3:f3_val @f2:f2_val @f1:f1_val\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1).ToNot(BeEmpty())\n\n\t})\n\n\tIt(\"should FTAlias\", Label(\"search\", \"ftexplain\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText}\n\t\tval1, err := client.FTCreate(ctx, \"testAlias\", &redis.FTCreateOptions{Prefix: []interface{}{\"index1:\"}}, text1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val1).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"testAlias\")\n\t\tval2, err := client.FTCreate(ctx, \"testAlias2\", &redis.FTCreateOptions{Prefix: []interface{}{\"index2:\"}}, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val2).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"testAlias2\")\n\n\t\tclient.HSet(ctx, \"index1:lonestar\", \"name\", \"lonestar\")\n\t\tclient.HSet(ctx, \"index2:yogurt\", \"name\", \"yogurt\")\n\n\t\tres1, err := client.FTSearch(ctx, \"testAlias\", \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"index1:lonestar\"))\n\n\t\taliasAddRes, err := client.FTAliasAdd(ctx, \"testAlias\", \"mj23\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(aliasAddRes).To(BeEquivalentTo(\"OK\"))\n\n\t\tres1, err = client.FTSearch(ctx, \"mj23\", \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"index1:lonestar\"))\n\n\t\taliasUpdateRes, err := client.FTAliasUpdate(ctx, \"testAlias2\", \"kb24\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(aliasUpdateRes).To(BeEquivalentTo(\"OK\"))\n\n\t\tres3, err := client.FTSearch(ctx, \"kb24\", \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res3.Docs[0].ID).To(BeEquivalentTo(\"index2:yogurt\"))\n\n\t\taliasDelRes, err := client.FTAliasDel(ctx, \"mj23\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(aliasDelRes).To(BeEquivalentTo(\"OK\"))\n\n\t})\n\n\tIt(\"should FTCreate and FTSearch textfield, sortable and nostem \", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tresInfo, err := client.FTInfo(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resInfo.Attributes[0].Sortable).To(BeTrue())\n\t\tExpect(resInfo.Attributes[0].NoStem).To(BeTrue())\n\n\t})\n\n\tIt(\"should FTAlter\", Label(\"search\", \"ftcreate\", \"ftsearch\", \"ftalter\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tresAlter, err := client.FTAlter(ctx, \"idx1\", false, []interface{}{\"body\", redis.SearchFieldTypeText.String()}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resAlter).To(BeEquivalentTo(\"OK\"))\n\n\t\tclient.HSet(ctx, \"doc1\", \"title\", \"MyTitle\", \"body\", \"Some content only in the body\")\n\t\tres1, err := client.FTSearch(ctx, \"idx1\", \"only in the body\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(1)))\n\n\t\t_, err = client.FTSearch(ctx, \"idx_not_exist\", \"only in the body\").Result()\n\t\tExpect(err).To(HaveOccurred())\n\t})\n\n\tIt(\"should FTSpellCheck\", Label(\"search\", \"ftcreate\", \"ftsearch\", \"ftspellcheck\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"f1\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"f2\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"f1\", \"some valid content\", \"f2\", \"this is sample text\")\n\t\tclient.HSet(ctx, \"doc2\", \"f1\", \"very important\", \"f2\", \"lorem ipsum\")\n\n\t\tresSpellCheck, err := client.FTSpellCheck(ctx, \"idx1\", \"impornant\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSpellCheck[0].Suggestions[0].Suggestion).To(BeEquivalentTo(\"important\"))\n\n\t\tresSpellCheck2, err := client.FTSpellCheck(ctx, \"idx1\", \"contnt\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSpellCheck2[0].Suggestions[0].Suggestion).To(BeEquivalentTo(\"content\"))\n\n\t\t// test spellcheck with Levenshtein distance\n\t\tresSpellCheck3, err := client.FTSpellCheck(ctx, \"idx1\", \"vlis\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSpellCheck3[0].Term).To(BeEquivalentTo(\"vlis\"))\n\n\t\tresSpellCheck4, err := client.FTSpellCheckWithArgs(ctx, \"idx1\", \"vlis\", &redis.FTSpellCheckOptions{Distance: 2}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSpellCheck4[0].Suggestions[0].Suggestion).To(BeEquivalentTo(\"valid\"))\n\n\t\t// test spellcheck include\n\t\tresDictAdd, err := client.FTDictAdd(ctx, \"dict\", \"lore\", \"lorem\", \"lorm\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDictAdd).To(BeEquivalentTo(3))\n\t\tterms := &redis.FTSpellCheckTerms{Inclusion: \"INCLUDE\", Dictionary: \"dict\"}\n\t\tresSpellCheck5, err := client.FTSpellCheckWithArgs(ctx, \"idx1\", \"lorm\", &redis.FTSpellCheckOptions{Terms: terms}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tlorm := resSpellCheck5[0].Suggestions\n\t\tExpect(len(lorm)).To(BeEquivalentTo(3))\n\t\tExpect(lorm[0].Score).To(BeEquivalentTo(0.5))\n\t\tExpect(lorm[1].Score).To(BeEquivalentTo(0))\n\t\tExpect(lorm[2].Score).To(BeEquivalentTo(0))\n\n\t\tterms2 := &redis.FTSpellCheckTerms{Inclusion: \"EXCLUDE\", Dictionary: \"dict\"}\n\t\tresSpellCheck6, err := client.FTSpellCheckWithArgs(ctx, \"idx1\", \"lorm\", &redis.FTSpellCheckOptions{Terms: terms2}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSpellCheck6).To(BeEmpty())\n\t})\n\n\tIt(\"should FTDict opreations\", Label(\"search\", \"ftdictdump\", \"ftdictdel\", \"ftdictadd\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"f1\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"f2\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tresDictAdd, err := client.FTDictAdd(ctx, \"custom_dict\", \"item1\", \"item2\", \"item3\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDictAdd).To(BeEquivalentTo(3))\n\n\t\tresDictDel, err := client.FTDictDel(ctx, \"custom_dict\", \"item2\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDictDel).To(BeEquivalentTo(1))\n\n\t\tresDictDump, err := client.FTDictDump(ctx, \"custom_dict\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDictDump).To(BeEquivalentTo([]string{\"item1\", \"item3\"}))\n\n\t\tresDictDel2, err := client.FTDictDel(ctx, \"custom_dict\", \"item1\", \"item3\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDictDel2).To(BeEquivalentTo(2))\n\t})\n\n\tIt(\"should FTSearch phonetic matcher\", Label(\"search\", \"ftsearch\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"name\", \"Jon\")\n\t\tclient.HSet(ctx, \"doc2\", \"name\", \"John\")\n\n\t\tres1, err := client.FTSearch(ctx, \"idx1\", \"Jon\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(1)))\n\t\tExpect(res1.Docs[0].Fields[\"name\"]).To(BeEquivalentTo(\"Jon\"))\n\n\t\tclient.FlushDB(ctx)\n\t\ttext2 := &redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText, PhoneticMatcher: \"dm:en\"}\n\t\tval2, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val2).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"name\", \"Jon\")\n\t\tclient.HSet(ctx, \"doc2\", \"name\", \"John\")\n\n\t\tres2, err := client.FTSearch(ctx, \"idx1\", \"Jon\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(BeEquivalentTo(int64(2)))\n\t\tnames := []interface{}{res2.Docs[0].Fields[\"name\"], res2.Docs[1].Fields[\"name\"]}\n\t\tExpect(names).To(ContainElement(\"Jon\"))\n\t\tExpect(names).To(ContainElement(\"John\"))\n\t})\n\n\t// up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25\n\t// this test expect redis major version >= 8\n\tIt(\"should FTSearch WithScores\", Label(\"search\", \"ftsearch\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"default scorer is not BM25STD\")\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"description\", \"The quick brown fox jumps over the lazy dog\")\n\t\tclient.HSet(ctx, \"doc2\", \"description\", \"Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.\")\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeNumerically(\"<=\", 0.236))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"TFIDF\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"TFIDF.DOCNORM\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"BM25\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeNumerically(\"<=\", 0.22471909420069797))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"DISMAX\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2)))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"DOCSCORE\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"HAMMING\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0)))\n\t})\n\n\t// up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25\n\t// this test expect redis version < 8.0\n\tIt(\"should FTSearch WithScores\", Label(\"search\", \"ftsearch\"), func() {\n\t\tSkipAfterRedisVersion(7.9, \"default scorer is not TFIDF\")\n\t\ttext1 := &redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"description\", \"The quick brown fox jumps over the lazy dog\")\n\t\tclient.HSet(ctx, \"doc2\", \"description\", \"Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.\")\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"TFIDF\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"TFIDF.DOCNORM\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"BM25\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeNumerically(\"<=\", 0.22471909420069797))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"DISMAX\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2)))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"DOCSCORE\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1)))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"quick\", &redis.FTSearchOptions{WithScores: true, Scorer: \"HAMMING\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0)))\n\t})\n\n\tIt(\"should FTConfigSet and FTConfigGet \", Label(\"search\", \"ftconfigget\", \"ftconfigset\", \"NonRedisEnterprise\"), func() {\n\t\tval, err := client.FTConfigSet(ctx, \"MINPREFIX\", \"1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\n\t\tres, err := client.FTConfigGet(ctx, \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res[\"MINPREFIX\"]).To(BeEquivalentTo(\"1\"))\n\n\t\tres, err = client.FTConfigGet(ctx, \"MINPREFIX\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(BeEquivalentTo(map[string]interface{}{\"MINPREFIX\": \"1\"}))\n\n\t})\n\n\tIt(\"should FTAggregate GroupBy \", Label(\"search\", \"ftaggregate\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}\n\t\ttext3 := &redis.FieldSchema{FieldName: \"parent\", FieldType: redis.SearchFieldTypeText}\n\t\tnum := &redis.FieldSchema{FieldName: \"random_num\", FieldType: redis.SearchFieldTypeNumeric}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, text2, text3, num).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"search\", \"title\", \"RediSearch\",\n\t\t\t\"body\", \"Redisearch implements a search engine on top of redis\",\n\t\t\t\"parent\", \"redis\",\n\t\t\t\"random_num\", 10)\n\t\tclient.HSet(ctx, \"ai\", \"title\", \"RedisAI\",\n\t\t\t\"body\", \"RedisAI executes Deep Learning/Machine Learning models and managing their data.\",\n\t\t\t\"parent\", \"redis\",\n\t\t\t\"random_num\", 3)\n\t\tclient.HSet(ctx, \"json\", \"title\", \"RedisJson\",\n\t\t\t\"body\", \"RedisJSON implements ECMA-404 The JSON Data Interchange Standard as a native data type.\",\n\t\t\t\"parent\", \"redis\",\n\t\t\t\"random_num\", 8)\n\n\t\treducer := redis.FTAggregateReducer{Reducer: redis.SearchCount}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliascount\"]).To(BeEquivalentTo(\"3\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchCountDistinct, Args: []interface{}{\"@title\"}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliascount_distincttitle\"]).To(BeEquivalentTo(\"3\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchSum, Args: []interface{}{\"@random_num\"}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliassumrandom_num\"]).To(BeEquivalentTo(\"21\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchMin, Args: []interface{}{\"@random_num\"}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliasminrandom_num\"]).To(BeEquivalentTo(\"3\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchMax, Args: []interface{}{\"@random_num\"}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliasmaxrandom_num\"]).To(BeEquivalentTo(\"10\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchAvg, Args: []interface{}{\"@random_num\"}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliasavgrandom_num\"]).To(BeEquivalentTo(\"7\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchStdDev, Args: []interface{}{\"@random_num\"}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliasstddevrandom_num\"]).To(BeEquivalentTo(\"3.60555127546\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchQuantile, Args: []interface{}{\"@random_num\", 0.5}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliasquantilerandom_num,0.5\"]).To(BeEquivalentTo(\"8\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchToList, Args: []interface{}{\"@title\"}}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"__generated_aliastolisttitle\"]).To(ContainElements(\"RediSearch\", \"RedisAI\", \"RedisJson\"))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchFirstValue, Args: []interface{}{\"@title\"}, As: \"first\"}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"first\"]).To(Or(BeEquivalentTo(\"RediSearch\"), BeEquivalentTo(\"RedisAI\"), BeEquivalentTo(\"RedisJson\")))\n\n\t\treducer = redis.FTAggregateReducer{Reducer: redis.SearchRandomSample, Args: []interface{}{\"@title\", 2}, As: \"random\"}\n\t\toptions = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{\"@parent\"}, Reduce: []redis.FTAggregateReducer{reducer}}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"redis\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"parent\"]).To(BeEquivalentTo(\"redis\"))\n\t\tExpect(res.Rows[0].Fields[\"random\"]).To(Or(\n\t\t\tContainElement(\"RediSearch\"),\n\t\t\tContainElement(\"RedisAI\"),\n\t\t\tContainElement(\"RedisJson\"),\n\t\t))\n\n\t})\n\n\tIt(\"should FTAggregate sort and limit\", Label(\"search\", \"ftaggregate\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"t1\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"t2\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"t1\", \"a\", \"t2\", \"b\")\n\t\tclient.HSet(ctx, \"doc2\", \"t1\", \"b\", \"t2\", \"a\")\n\n\t\toptions := &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: \"@t2\", Asc: true}, {FieldName: \"@t1\", Desc: true}}}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"b\"))\n\t\tExpect(res.Rows[1].Fields[\"t1\"]).To(BeEquivalentTo(\"a\"))\n\t\tExpect(res.Rows[0].Fields[\"t2\"]).To(BeEquivalentTo(\"a\"))\n\t\tExpect(res.Rows[1].Fields[\"t2\"]).To(BeEquivalentTo(\"b\"))\n\n\t\toptions = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: \"@t1\"}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"a\"))\n\t\tExpect(res.Rows[1].Fields[\"t1\"]).To(BeEquivalentTo(\"b\"))\n\n\t\toptions = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: \"@t1\"}}, SortByMax: 1}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"a\"))\n\n\t\toptions = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: \"@t1\"}}, Limit: 1, LimitOffset: 1}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"b\"))\n\n\t\toptions = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: \"@t1\"}}, Limit: 1, LimitOffset: 0}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"a\"))\n\t})\n\n\tIt(\"should FTAggregate load \", Label(\"search\", \"ftaggregate\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"t1\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"t2\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"t1\", \"hello\", \"t2\", \"world\")\n\n\t\toptions := &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: \"t1\"}}}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"hello\"))\n\n\t\toptions = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: \"t2\"}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t2\"]).To(BeEquivalentTo(\"world\"))\n\n\t\toptions = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: \"t2\", As: \"t2alias\"}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t2alias\"]).To(BeEquivalentTo(\"world\"))\n\n\t\toptions = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: \"t1\"}, {Field: \"t2\", As: \"t2alias\"}}}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"hello\"))\n\t\tExpect(res.Rows[0].Fields[\"t2alias\"]).To(BeEquivalentTo(\"world\"))\n\n\t\toptions = &redis.FTAggregateOptions{LoadAll: true}\n\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"t1\"]).To(BeEquivalentTo(\"hello\"))\n\t\tExpect(res.Rows[0].Fields[\"t2\"]).To(BeEquivalentTo(\"world\"))\n\n\t\t_, err = client.FTAggregateWithArgs(ctx, \"idx_not_exist\", \"*\", &redis.FTAggregateOptions{}).Result()\n\t\tExpect(err).To(HaveOccurred())\n\t})\n\n\tIt(\"should FTAggregate with scorer and addscores\", Label(\"search\", \"ftaggregate\", \"NonRedisEnterprise\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"no addscores support\")\n\t\ttitle := &redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText, Sortable: false}\n\t\tdescription := &redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText, Sortable: false}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{\"product:\"}}, title, description).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"product:1\", \"title\", \"New Gaming Laptop\", \"description\", \"this is not a desktop\")\n\t\tclient.HSet(ctx, \"product:2\", \"title\", \"Super Old Not Gaming Laptop\", \"description\", \"this laptop is not a new laptop but it is a laptop\")\n\t\tclient.HSet(ctx, \"product:3\", \"title\", \"Office PC\", \"description\", \"office desktop pc\")\n\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tAddScores: true,\n\t\t\tScorer:    \"BM25\",\n\t\t\tSortBy: []redis.FTAggregateSortBy{{\n\t\t\t\tFieldName: \"@__score\",\n\t\t\t\tDesc:      true,\n\t\t\t}},\n\t\t}\n\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"laptop\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).ToNot(BeNil())\n\t\tExpect(len(res.Rows)).To(BeEquivalentTo(2))\n\t\tscore1, err := helper.ParseFloat(fmt.Sprintf(\"%s\", res.Rows[0].Fields[\"__score\"]))\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tscore2, err := helper.ParseFloat(fmt.Sprintf(\"%s\", res.Rows[1].Fields[\"__score\"]))\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(score1).To(BeNumerically(\">\", score2))\n\n\t\toptionsDM := &redis.FTAggregateOptions{\n\t\t\tAddScores: true,\n\t\t\tScorer:    \"DISMAX\",\n\t\t\tSortBy: []redis.FTAggregateSortBy{{\n\t\t\t\tFieldName: \"@__score\",\n\t\t\t\tDesc:      true,\n\t\t\t}},\n\t\t}\n\n\t\tresDM, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"laptop\", optionsDM).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDM).ToNot(BeNil())\n\t\tExpect(len(resDM.Rows)).To(BeEquivalentTo(2))\n\t\tscore1DM, err := helper.ParseFloat(fmt.Sprintf(\"%s\", resDM.Rows[0].Fields[\"__score\"]))\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tscore2DM, err := helper.ParseFloat(fmt.Sprintf(\"%s\", resDM.Rows[1].Fields[\"__score\"]))\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(score1DM).To(BeNumerically(\">\", score2DM))\n\n\t\tExpect(score1DM).To(BeEquivalentTo(float64(4)))\n\t\tExpect(score2DM).To(BeEquivalentTo(float64(1)))\n\t\tExpect(score1).NotTo(BeEquivalentTo(score1DM))\n\t\tExpect(score2).NotTo(BeEquivalentTo(score2DM))\n\t})\n\n\tIt(\"should FTAggregate apply and groupby\", Label(\"search\", \"ftaggregate\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"PrimaryKey\", FieldType: redis.SearchFieldTypeText, Sortable: true}\n\t\tnum1 := &redis.FieldSchema{FieldName: \"CreatedDateTimeUTC\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, num1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\t// 6 feb\n\t\tclient.HSet(ctx, \"doc1\", \"PrimaryKey\", \"9::362330\", \"CreatedDateTimeUTC\", \"1738823999\")\n\n\t\t// 12 feb\n\t\tclient.HSet(ctx, \"doc2\", \"PrimaryKey\", \"9::362329\", \"CreatedDateTimeUTC\", \"1739342399\")\n\t\tclient.HSet(ctx, \"doc3\", \"PrimaryKey\", \"9::362329\", \"CreatedDateTimeUTC\", \"1739353199\")\n\n\t\treducer := redis.FTAggregateReducer{Reducer: redis.SearchCount, As: \"perDay\"}\n\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tApply: []redis.FTAggregateApply{{Field: \"floor(@CreatedDateTimeUTC /(60*60*24))\", As: \"TimestampAsDay\"}},\n\t\t\tGroupBy: []redis.FTAggregateGroupBy{{\n\t\t\t\tFields: []interface{}{\"@TimestampAsDay\"},\n\t\t\t\tReduce: []redis.FTAggregateReducer{reducer},\n\t\t\t}},\n\t\t\tSortBy: []redis.FTAggregateSortBy{{\n\t\t\t\tFieldName: \"@perDay\",\n\t\t\t\tDesc:      true,\n\t\t\t}},\n\t\t}\n\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).ToNot(BeNil())\n\t\tExpect(len(res.Rows)).To(BeEquivalentTo(2))\n\t\tExpect(res.Rows[0].Fields[\"perDay\"]).To(BeEquivalentTo(\"2\"))\n\t\tExpect(res.Rows[1].Fields[\"perDay\"]).To(BeEquivalentTo(\"1\"))\n\t})\n\n\tIt(\"should FTAggregate apply\", Label(\"search\", \"ftaggregate\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"PrimaryKey\", FieldType: redis.SearchFieldTypeText, Sortable: true}\n\t\tnum1 := &redis.FieldSchema{FieldName: \"CreatedDateTimeUTC\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, num1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"PrimaryKey\", \"9::362330\", \"CreatedDateTimeUTC\", \"637387878524969984\")\n\t\tclient.HSet(ctx, \"doc2\", \"PrimaryKey\", \"9::362329\", \"CreatedDateTimeUTC\", \"637387875859270016\")\n\n\t\toptions := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: \"@CreatedDateTimeUTC * 10\", As: \"CreatedDateTimeUTC\"}}}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows[0].Fields[\"CreatedDateTimeUTC\"]).To(Or(BeEquivalentTo(\"6373878785249699840\"), BeEquivalentTo(\"6373878758592700416\")))\n\t\tExpect(res.Rows[1].Fields[\"CreatedDateTimeUTC\"]).To(Or(BeEquivalentTo(\"6373878785249699840\"), BeEquivalentTo(\"6373878758592700416\")))\n\n\t})\n\n\tIt(\"should FTAggregate filter\", Label(\"search\", \"ftaggregate\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText, Sortable: true}\n\t\tnum1 := &redis.FieldSchema{FieldName: \"age\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, num1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"name\", \"bar\", \"age\", \"25\")\n\t\tclient.HSet(ctx, \"doc2\", \"name\", \"foo\", \"age\", \"19\")\n\n\t\tfor _, dlc := range []int{1, 2} {\n\t\t\toptions := &redis.FTAggregateOptions{Filter: \"@name=='foo' && @age < 20\", DialectVersion: dlc}\n\t\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res.Total).To(Or(BeEquivalentTo(2), BeEquivalentTo(1)))\n\t\t\tExpect(res.Rows[0].Fields[\"name\"]).To(BeEquivalentTo(\"foo\"))\n\n\t\t\toptions = &redis.FTAggregateOptions{Filter: \"@age > 15\", DialectVersion: dlc, SortBy: []redis.FTAggregateSortBy{{FieldName: \"@age\"}}}\n\t\t\tres, err = client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res.Total).To(BeEquivalentTo(2))\n\t\t\tExpect(res.Rows[0].Fields[\"age\"]).To(BeEquivalentTo(\"19\"))\n\t\t\tExpect(res.Rows[1].Fields[\"age\"]).To(BeEquivalentTo(\"25\"))\n\t\t}\n\t})\n\n\tIt(\"should return only the base query when options is nil\", Label(\"search\", \"ftaggregate\"), func() {\n\t\targs, err := redis.FTAggregateQuery(\"testQuery\", nil)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(args).To(Equal(redis.AggregateQuery{\"testQuery\"}))\n\t})\n\n\tIt(\"should include VERBATIM and SCORER when options are set\", Label(\"search\", \"ftaggregate\"), func() {\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tVerbatim: true,\n\t\t\tScorer:   \"BM25\",\n\t\t}\n\t\targs, err := redis.FTAggregateQuery(\"testQuery\", options)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(args[0]).To(Equal(\"testQuery\"))\n\t\tExpect(args).To(ContainElement(\"VERBATIM\"))\n\t\tExpect(args).To(ContainElement(\"SCORER\"))\n\t\tExpect(args).To(ContainElement(\"BM25\"))\n\t})\n\n\tIt(\"should include ADDSCORES when AddScores is true\", Label(\"search\", \"ftaggregate\"), func() {\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tAddScores: true,\n\t\t}\n\t\targs, err := redis.FTAggregateQuery(\"q\", options)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(args).To(ContainElement(\"ADDSCORES\"))\n\t})\n\n\tIt(\"should include LOADALL when LoadAll is true\", Label(\"search\", \"ftaggregate\"), func() {\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tLoadAll: true,\n\t\t}\n\t\targs, err := redis.FTAggregateQuery(\"q\", options)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(args).To(ContainElement(\"LOAD\"))\n\t\tExpect(args).To(ContainElement(\"*\"))\n\t})\n\n\tIt(\"should include LOAD when Load is provided\", Label(\"search\", \"ftaggregate\"), func() {\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tLoad: []redis.FTAggregateLoad{\n\t\t\t\t{Field: \"field1\", As: \"alias1\"},\n\t\t\t\t{Field: \"field2\"},\n\t\t\t},\n\t\t}\n\t\targs, err := redis.FTAggregateQuery(\"q\", options)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t// Verify LOAD options related arguments\n\t\tExpect(args).To(ContainElement(\"LOAD\"))\n\t\t// Check that field names and aliases are present\n\t\tExpect(args).To(ContainElement(\"field1\"))\n\t\tExpect(args).To(ContainElement(\"alias1\"))\n\t\tExpect(args).To(ContainElement(\"field2\"))\n\t})\n\n\tIt(\"should include TIMEOUT when Timeout > 0\", Label(\"search\", \"ftaggregate\"), func() {\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tTimeout: 500,\n\t\t}\n\t\targs, err := redis.FTAggregateQuery(\"q\", options)\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(args).To(ContainElement(\"TIMEOUT\"))\n\t\tfound := false\n\t\tfor i, a := range args {\n\t\t\tif fmt.Sprintf(\"%s\", a) == \"TIMEOUT\" {\n\t\t\t\tExpect(fmt.Sprintf(\"%d\", args[i+1])).To(Equal(\"500\"))\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tExpect(found).To(BeTrue())\n\t})\n\n\tIt(\"should FTSearch SkipInitialScan\", Label(\"search\", \"ftsearch\"), func() {\n\t\tclient.HSet(ctx, \"doc1\", \"foo\", \"bar\")\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"foo\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{SkipInitialScan: true}, text1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tres, err := client.FTSearch(ctx, \"idx1\", \"@foo:bar\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(int64(0)))\n\t})\n\n\tIt(\"should FTCreate json\", Label(\"search\", \"ftcreate\"), func() {\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"$.name\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{\"king:\"}}, text1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.JSONSet(ctx, \"king:1\", \"$\", `{\"name\": \"henry\"}`)\n\t\tclient.JSONSet(ctx, \"king:2\", \"$\", `{\"name\": \"james\"}`)\n\n\t\tres, err := client.FTSearch(ctx, \"idx1\", \"henry\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"king:1\"))\n\t\tExpect(res.Docs[0].Fields[\"$\"]).To(BeEquivalentTo(`{\"name\":\"henry\"}`))\n\t})\n\n\tIt(\"should FTCreate json fields as names\", Label(\"search\", \"ftcreate\"), func() {\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"$.name\", FieldType: redis.SearchFieldTypeText, As: \"name\"}\n\t\tnum1 := &redis.FieldSchema{FieldName: \"$.age\", FieldType: redis.SearchFieldTypeNumeric, As: \"just_a_number\"}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.JSONSet(ctx, \"doc:1\", \"$\", `{\"name\": \"Jon\", \"age\": 25}`)\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"Jon\", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: \"name\"}, {FieldName: \"just_a_number\"}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc:1\"))\n\t\tExpect(res.Docs[0].Fields[\"name\"]).To(BeEquivalentTo(\"Jon\"))\n\t\tExpect(res.Docs[0].Fields[\"just_a_number\"]).To(BeEquivalentTo(\"25\"))\n\t})\n\n\tIt(\"should FTCreate CaseSensitive\", Label(\"search\", \"ftcreate\"), func() {\n\n\t\ttag1 := &redis.FieldSchema{FieldName: \"t\", FieldType: redis.SearchFieldTypeTag, CaseSensitive: false}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, tag1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"1\", \"t\", \"HELLO\")\n\t\tclient.HSet(ctx, \"2\", \"t\", \"hello\")\n\n\t\tres, err := client.FTSearch(ctx, \"idx1\", \"@t:{HELLO}\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(2))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"1\"))\n\t\tExpect(res.Docs[1].ID).To(BeEquivalentTo(\"2\"))\n\n\t\tresDrop, err := client.FTDropIndex(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDrop).To(BeEquivalentTo(\"OK\"))\n\n\t\ttag2 := &redis.FieldSchema{FieldName: \"t\", FieldType: redis.SearchFieldTypeTag, CaseSensitive: true}\n\t\tval, err = client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, tag2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tres, err = client.FTSearch(ctx, \"idx1\", \"@t:{HELLO}\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"1\"))\n\n\t})\n\n\tIt(\"should FTSearch ReturnFields\", Label(\"search\", \"ftsearch\"), func() {\n\t\tresJson, err := client.JSONSet(ctx, \"doc:1\", \"$\", `{\"t\": \"riceratops\",\"t2\": \"telmatosaurus\", \"n\": 9072, \"flt\": 97.2}`).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resJson).To(BeEquivalentTo(\"OK\"))\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"$.t\", FieldType: redis.SearchFieldTypeText}\n\t\tnum1 := &redis.FieldSchema{FieldName: \"$.flt\", FieldType: redis.SearchFieldTypeNumeric}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*\", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: \"$.t\", As: \"txt\"}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc:1\"))\n\t\tExpect(res.Docs[0].Fields[\"txt\"]).To(BeEquivalentTo(\"riceratops\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"*\", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: \"$.t2\", As: \"txt\"}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc:1\"))\n\t\tExpect(res.Docs[0].Fields[\"txt\"]).To(BeEquivalentTo(\"telmatosaurus\"))\n\t})\n\n\tIt(\"should FTSynUpdate\", Label(\"search\", \"ftsynupdate\"), func() {\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tresSynUpdate, err := client.FTSynUpdateWithArgs(ctx, \"idx1\", \"id1\", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{\"boy\", \"child\", \"offspring\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\t\tclient.HSet(ctx, \"doc1\", \"title\", \"he is a baby\", \"body\", \"this is a test\")\n\n\t\tresSynUpdate, err = client.FTSynUpdateWithArgs(ctx, \"idx1\", \"id1\", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{\"baby\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\t\tclient.HSet(ctx, \"doc2\", \"title\", \"he is another baby\", \"body\", \"another test\")\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"child\", &redis.FTSearchOptions{Expander: \"SYNONYM\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc2\"))\n\t\tExpect(res.Docs[0].Fields[\"title\"]).To(BeEquivalentTo(\"he is another baby\"))\n\t\tExpect(res.Docs[0].Fields[\"body\"]).To(BeEquivalentTo(\"another test\"))\n\t})\n\n\tIt(\"should FTSynDump\", Label(\"search\", \"ftsyndump\"), func() {\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tresSynUpdate, err := client.FTSynUpdate(ctx, \"idx1\", \"id1\", []interface{}{\"boy\", \"child\", \"offspring\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\n\t\tresSynUpdate, err = client.FTSynUpdate(ctx, \"idx1\", \"id1\", []interface{}{\"baby\", \"child\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\n\t\tresSynUpdate, err = client.FTSynUpdate(ctx, \"idx1\", \"id1\", []interface{}{\"tree\", \"wood\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\n\t\tresSynDump, err := client.FTSynDump(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynDump[0].Term).To(BeEquivalentTo(\"baby\"))\n\t\tExpect(resSynDump[0].Synonyms).To(BeEquivalentTo([]string{\"id1\"}))\n\t\tExpect(resSynDump[1].Term).To(BeEquivalentTo(\"wood\"))\n\t\tExpect(resSynDump[1].Synonyms).To(BeEquivalentTo([]string{\"id1\"}))\n\t\tExpect(resSynDump[2].Term).To(BeEquivalentTo(\"boy\"))\n\t\tExpect(resSynDump[2].Synonyms).To(BeEquivalentTo([]string{\"id1\"}))\n\t\tExpect(resSynDump[3].Term).To(BeEquivalentTo(\"tree\"))\n\t\tExpect(resSynDump[3].Synonyms).To(BeEquivalentTo([]string{\"id1\"}))\n\t\tExpect(resSynDump[4].Term).To(BeEquivalentTo(\"child\"))\n\t\tExpect(resSynDump[4].Synonyms).To(Or(BeEquivalentTo([]string{\"id1\"}), BeEquivalentTo([]string{\"id1\", \"id1\"})))\n\t\tExpect(resSynDump[5].Term).To(BeEquivalentTo(\"offspring\"))\n\t\tExpect(resSynDump[5].Synonyms).To(BeEquivalentTo([]string{\"id1\"}))\n\n\t})\n\n\tIt(\"should FTCreate json with alias\", Label(\"search\", \"ftcreate\"), func() {\n\n\t\ttext1 := &redis.FieldSchema{FieldName: \"$.name\", FieldType: redis.SearchFieldTypeText, As: \"name\"}\n\t\tnum1 := &redis.FieldSchema{FieldName: \"$.num\", FieldType: redis.SearchFieldTypeNumeric, As: \"num\"}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{\"king:\"}}, text1, num1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.JSONSet(ctx, \"king:1\", \"$\", `{\"name\": \"henry\", \"num\": 42}`)\n\t\tclient.JSONSet(ctx, \"king:2\", \"$\", `{\"name\": \"james\", \"num\": 3.14}`)\n\n\t\tres, err := client.FTSearch(ctx, \"idx1\", \"@name:henry\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"king:1\"))\n\t\tExpect(res.Docs[0].Fields[\"$\"]).To(BeEquivalentTo(`{\"name\":\"henry\",\"num\":42}`))\n\n\t\tres, err = client.FTSearch(ctx, \"idx1\", \"@num:[0 10]\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"king:2\"))\n\t\tExpect(res.Docs[0].Fields[\"$\"]).To(BeEquivalentTo(`{\"name\":\"james\",\"num\":3.14}`))\n\t})\n\n\tIt(\"should FTCreate json with multipath\", Label(\"search\", \"ftcreate\"), func() {\n\n\t\ttag1 := &redis.FieldSchema{FieldName: \"$..name\", FieldType: redis.SearchFieldTypeTag, As: \"name\"}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{\"king:\"}}, tag1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.JSONSet(ctx, \"king:1\", \"$\", `{\"name\": \"henry\", \"country\": {\"name\": \"england\"}}`)\n\n\t\tres, err := client.FTSearch(ctx, \"idx1\", \"@name:{england}\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"king:1\"))\n\t\tExpect(res.Docs[0].Fields[\"$\"]).To(BeEquivalentTo(`{\"name\":\"henry\",\"country\":{\"name\":\"england\"}}`))\n\t})\n\n\tIt(\"should FTCreate json with jsonpath\", Label(\"search\", \"ftcreate\"), func() {\n\n\t\ttext1 := &redis.FieldSchema{FieldName: `$[\"prod:name\"]`, FieldType: redis.SearchFieldTypeText, As: \"name\"}\n\t\ttext2 := &redis.FieldSchema{FieldName: `$.prod:name`, FieldType: redis.SearchFieldTypeText, As: \"name_unsupported\"}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnJSON: true}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.JSONSet(ctx, \"doc:1\", \"$\", `{\"prod:name\": \"RediSearch\"}`)\n\n\t\tres, err := client.FTSearch(ctx, \"idx1\", \"@name:RediSearch\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc:1\"))\n\t\tExpect(res.Docs[0].Fields[\"$\"]).To(BeEquivalentTo(`{\"prod:name\":\"RediSearch\"}`))\n\n\t\tres, err = client.FTSearch(ctx, \"idx1\", \"@name_unsupported:RediSearch\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"@name:RediSearch\", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: \"name\"}}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(1))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc:1\"))\n\t\tExpect(res.Docs[0].Fields[\"name\"]).To(BeEquivalentTo(\"RediSearch\"))\n\n\t})\n\n\tIt(\"should FTCreate VECTOR\", Label(\"search\", \"ftcreate\"), func() {\n\t\thnswOptions := &redis.FTHNSWOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"a\", \"v\", \"aaaaaaaa\")\n\t\tclient.HSet(ctx, \"b\", \"v\", \"aaaabaaa\")\n\t\tclient.HSet(ctx, \"c\", \"v\", \"aaaaabaa\")\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"__v_score\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"__v_score\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tParams:         map[string]interface{}{\"vec\": \"aaaaaaaa\"},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 2 @v $vec]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"a\"))\n\t\tExpect(res.Docs[0].Fields[\"__v_score\"]).To(BeEquivalentTo(\"0\"))\n\t})\n\n\tIt(\"should FTCreate VECTOR with dialect 1 \", Label(\"search\", \"ftcreate\"), func() {\n\t\thnswOptions := &redis.FTHNSWOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"a\", \"v\", \"aaaaaaaa\")\n\t\tclient.HSet(ctx, \"b\", \"v\", \"aaaabaaa\")\n\t\tclient.HSet(ctx, \"c\", \"v\", \"aaaaabaa\")\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"v\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"v\", Asc: true}},\n\t\t\tLimit:          10,\n\t\t\tDialectVersion: 1,\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"a\"))\n\t\tExpect(res.Docs[0].Fields[\"v\"]).To(BeEquivalentTo(\"aaaaaaaa\"))\n\t})\n\n\tIt(\"should FTCreate VECTOR with default dialect\", Label(\"search\", \"ftcreate\"), func() {\n\t\thnswOptions := &redis.FTHNSWOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"a\", \"v\", \"aaaaaaaa\")\n\t\tclient.HSet(ctx, \"b\", \"v\", \"aaaabaaa\")\n\t\tclient.HSet(ctx, \"c\", \"v\", \"aaaaabaa\")\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn: []redis.FTSearchReturn{{FieldName: \"__v_score\"}},\n\t\t\tSortBy: []redis.FTSearchSortBy{{FieldName: \"__v_score\", Asc: true}},\n\t\t\tParams: map[string]interface{}{\"vec\": \"aaaaaaaa\"},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 2 @v $vec]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"a\"))\n\t\tExpect(res.Docs[0].Fields[\"__v_score\"]).To(BeEquivalentTo(\"0\"))\n\t})\n\n\tIt(\"should FTCreate and FTSearch text params\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"name\", \"Alice\")\n\t\tclient.HSet(ctx, \"doc2\", \"name\", \"Bob\")\n\t\tclient.HSet(ctx, \"doc3\", \"name\", \"Carol\")\n\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@name:($name1 | $name2 )\", &redis.FTSearchOptions{Params: map[string]interface{}{\"name1\": \"Alice\", \"name2\": \"Bob\"}, DialectVersion: 2}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(2)))\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tExpect(res1.Docs[1].ID).To(BeEquivalentTo(\"doc2\"))\n\n\t})\n\n\tIt(\"should FTCreate and FTSearch numeric params\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"numval\", FieldType: redis.SearchFieldTypeNumeric}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"numval\", 101)\n\t\tclient.HSet(ctx, \"doc2\", \"numval\", 102)\n\t\tclient.HSet(ctx, \"doc3\", \"numval\", 103)\n\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@numval:[$min $max]\", &redis.FTSearchOptions{Params: map[string]interface{}{\"min\": 101, \"max\": 102}, DialectVersion: 2}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(2)))\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tExpect(res1.Docs[1].ID).To(BeEquivalentTo(\"doc2\"))\n\n\t})\n\n\tIt(\"should FTCreate and FTSearch geo params\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"g\", FieldType: redis.SearchFieldTypeGeo}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"g\", \"29.69465, 34.95126\")\n\t\tclient.HSet(ctx, \"doc2\", \"g\", \"29.69350, 34.94737\")\n\t\tclient.HSet(ctx, \"doc3\", \"g\", \"29.68746, 34.94882\")\n\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@g:[$lon $lat $radius $units]\", &redis.FTSearchOptions{Params: map[string]interface{}{\"lat\": \"34.95126\", \"lon\": \"29.69465\", \"radius\": 1000, \"units\": \"km\"}, DialectVersion: 2}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(3)))\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t\tExpect(res1.Docs[1].ID).To(BeEquivalentTo(\"doc2\"))\n\t\tExpect(res1.Docs[2].ID).To(BeEquivalentTo(\"doc3\"))\n\n\t})\n\n\tIt(\"should FTConfigGet return multiple fields\", Label(\"search\", \"NonRedisEnterprise\"), func() {\n\t\tres, err := client.FTConfigSet(ctx, \"DEFAULT_DIALECT\", \"1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\n\t\tdefDialect, err := client.FTConfigGet(ctx, \"DEFAULT_DIALECT\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(defDialect).To(BeEquivalentTo(map[string]interface{}{\"DEFAULT_DIALECT\": \"1\"}))\n\n\t\tres, err = client.FTConfigSet(ctx, \"DEFAULT_DIALECT\", \"2\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\n\t\tdefDialect, err = client.FTConfigGet(ctx, \"DEFAULT_DIALECT\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(defDialect).To(BeEquivalentTo(map[string]interface{}{\"DEFAULT_DIALECT\": \"2\"}))\n\t})\n\n\tIt(\"should FTConfigSet and FTConfigGet dialect\", Label(\"search\", \"ftconfigget\", \"ftconfigset\", \"NonRedisEnterprise\"), func() {\n\t\tres, err := client.FTConfigSet(ctx, \"DEFAULT_DIALECT\", \"1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\n\t\tdefDialect, err := client.FTConfigGet(ctx, \"DEFAULT_DIALECT\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(defDialect).To(BeEquivalentTo(map[string]interface{}{\"DEFAULT_DIALECT\": \"1\"}))\n\n\t\tres, err = client.FTConfigSet(ctx, \"DEFAULT_DIALECT\", \"2\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res).To(BeEquivalentTo(\"OK\"))\n\n\t\tdefDialect, err = client.FTConfigGet(ctx, \"DEFAULT_DIALECT\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(defDialect).To(BeEquivalentTo(map[string]interface{}{\"DEFAULT_DIALECT\": \"2\"}))\n\t})\n\n\tIt(\"should FTCreate WithSuffixtrie\", Label(\"search\", \"ftcreate\", \"ftinfo\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tres, err := client.FTInfo(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Attributes[0].Attribute).To(BeEquivalentTo(\"txt\"))\n\n\t\tresDrop, err := client.FTDropIndex(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDrop).To(BeEquivalentTo(\"OK\"))\n\n\t\t// create withsuffixtrie index - text field\n\t\tval, err = client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText, WithSuffixtrie: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tres, err = client.FTInfo(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Attributes[0].WithSuffixtrie).To(BeTrue())\n\n\t\tresDrop, err = client.FTDropIndex(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDrop).To(BeEquivalentTo(\"OK\"))\n\n\t\t// create withsuffixtrie index - tag field\n\t\tval, err = client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"t\", FieldType: redis.SearchFieldTypeTag, WithSuffixtrie: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tres, err = client.FTInfo(ctx, \"idx1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Attributes[0].WithSuffixtrie).To(BeTrue())\n\t})\n\n\tIt(\"should test dialect 4\", Label(\"search\", \"ftcreate\", \"ftsearch\", \"NonRedisEnterprise\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{\n\t\t\tPrefix: []interface{}{\"resource:\"},\n\t\t}, &redis.FieldSchema{\n\t\t\tFieldName: \"uuid\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t}, &redis.FieldSchema{\n\t\t\tFieldName: \"tags\",\n\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t}, &redis.FieldSchema{\n\t\t\tFieldName: \"description\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t}, &redis.FieldSchema{\n\t\t\tFieldName: \"rating\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\n\t\tclient.HSet(ctx, \"resource:1\", map[string]interface{}{\n\t\t\t\"uuid\":        \"123e4567-e89b-12d3-a456-426614174000\",\n\t\t\t\"tags\":        \"finance|crypto|$btc|blockchain\",\n\t\t\t\"description\": \"Analysis of blockchain technologies & Bitcoin's potential.\",\n\t\t\t\"rating\":      5,\n\t\t})\n\t\tclient.HSet(ctx, \"resource:2\", map[string]interface{}{\n\t\t\t\"uuid\":        \"987e6543-e21c-12d3-a456-426614174999\",\n\t\t\t\"tags\":        \"health|well-being|fitness|new-year's-resolutions\",\n\t\t\t\"description\": \"Health trends for the new year, including fitness regimes.\",\n\t\t\t\"rating\":      4,\n\t\t})\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@uuid:{$uuid}\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 2,\n\t\t\t\tParams:         map[string]interface{}{\"uuid\": \"123e4567-e89b-12d3-a456-426614174000\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(int64(1)))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"resource:1\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"@uuid:{$uuid}\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 4,\n\t\t\t\tParams:         map[string]interface{}{\"uuid\": \"123e4567-e89b-12d3-a456-426614174000\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(int64(1)))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"resource:1\"))\n\n\t\tclient.HSet(ctx, \"test:1\", map[string]interface{}{\n\t\t\t\"uuid\":  \"3d3586fe-0416-4572-8ce\",\n\t\t\t\"email\": \"adriano@acme.com.ie\",\n\t\t\t\"num\":   5,\n\t\t})\n\n\t\t// Create the index\n\t\tftCreateOptions := &redis.FTCreateOptions{\n\t\t\tPrefix: []interface{}{\"test:\"},\n\t\t}\n\t\tschema := []*redis.FieldSchema{\n\t\t\t{\n\t\t\t\tFieldName: \"uuid\",\n\t\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t\t},\n\t\t\t{\n\t\t\t\tFieldName: \"email\",\n\t\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t\t},\n\t\t\t{\n\t\t\t\tFieldName: \"num\",\n\t\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t\t},\n\t\t}\n\n\t\tval, err = client.FTCreate(ctx, \"idx_hash\", ftCreateOptions, schema...).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_hash\")\n\n\t\tftSearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 4,\n\t\t\tParams: map[string]interface{}{\n\t\t\t\t\"uuid\":  \"3d3586fe-0416-4572-8ce\",\n\t\t\t\t\"email\": \"adriano@acme.com.ie\",\n\t\t\t},\n\t\t}\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx_hash\", \"@uuid:{$uuid}\", ftSearchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"test:1\"))\n\t\tExpect(res.Docs[0].Fields[\"uuid\"]).To(BeEquivalentTo(\"3d3586fe-0416-4572-8ce\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx_hash\", \"@email:{$email}\", ftSearchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"test:1\"))\n\t\tExpect(res.Docs[0].Fields[\"email\"]).To(BeEquivalentTo(\"adriano@acme.com.ie\"))\n\n\t\tftSearchOptions.Params = map[string]interface{}{\"num\": 5}\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx_hash\", \"@num:[5]\", ftSearchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"test:1\"))\n\t\tExpect(res.Docs[0].Fields[\"num\"]).To(BeEquivalentTo(\"5\"))\n\t})\n\n\tIt(\"should FTCreate GeoShape\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"geom\", FieldType: redis.SearchFieldTypeGeoShape, GeoShapeFieldType: \"FLAT\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"small\", \"geom\", \"POLYGON((1 1, 1 100, 100 100, 100 1, 1 1))\")\n\t\tclient.HSet(ctx, \"large\", \"geom\", \"POLYGON((1 1, 1 200, 200 200, 200 1, 1 1))\")\n\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@geom:[WITHIN $poly]\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 3,\n\t\t\t\tParams:         map[string]interface{}{\"poly\": \"POLYGON((0 0, 0 150, 150 150, 150 0, 0 0))\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1.Total).To(BeEquivalentTo(int64(1)))\n\t\tExpect(res1.Docs[0].ID).To(BeEquivalentTo(\"small\"))\n\n\t\tres2, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@geom:[CONTAINS $poly]\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 3,\n\t\t\t\tParams:         map[string]interface{}{\"poly\": \"POLYGON((2 2, 2 50, 50 50, 50 2, 2 2))\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2.Total).To(BeEquivalentTo(int64(2)))\n\t})\n\n\tIt(\"should create search index with FLOAT16 and BFLOAT16 vectors\", Label(\"search\", \"ftcreate\", \"NonRedisEnterprise\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\tval, err := client.FTCreate(ctx, \"index\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"float16\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: \"FLOAT16\", Dim: 768, DistanceMetric: \"COSINE\"}}},\n\t\t\t&redis.FieldSchema{FieldName: \"bfloat16\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: \"BFLOAT16\", Dim: 768, DistanceMetric: \"COSINE\"}}},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"index\")\n\t})\n\n\tIt(\"should test geoshapes query intersects and disjoint\", Label(\"NonRedisEnterprise\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\t_, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{\n\t\t\tFieldName:         \"g\",\n\t\t\tFieldType:         redis.SearchFieldTypeGeoShape,\n\t\t\tGeoShapeFieldType: \"FLAT\",\n\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tclient.HSet(ctx, \"doc_point1\", \"g\", \"POINT (10 10)\")\n\t\tclient.HSet(ctx, \"doc_point2\", \"g\", \"POINT (50 50)\")\n\t\tclient.HSet(ctx, \"doc_polygon1\", \"g\", \"POLYGON ((20 20, 25 35, 35 25, 20 20))\")\n\t\tclient.HSet(ctx, \"doc_polygon2\", \"g\", \"POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))\")\n\n\t\tintersection, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@g:[intersects $shape]\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 3,\n\t\t\t\tParams:         map[string]interface{}{\"shape\": \"POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_assert_geosearch_result(&intersection, []string{\"doc_point2\", \"doc_polygon1\"})\n\n\t\tdisjunction, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@g:[disjoint $shape]\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 3,\n\t\t\t\tParams:         map[string]interface{}{\"shape\": \"POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_assert_geosearch_result(&disjunction, []string{\"doc_point1\", \"doc_polygon2\"})\n\t})\n\n\tIt(\"should test geoshapes query contains and within\", func() {\n\t\t_, err := client.FTCreate(ctx, \"idx2\", &redis.FTCreateOptions{}, &redis.FieldSchema{\n\t\t\tFieldName:         \"g\",\n\t\t\tFieldType:         redis.SearchFieldTypeGeoShape,\n\t\t\tGeoShapeFieldType: \"FLAT\",\n\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tclient.HSet(ctx, \"doc_point1\", \"g\", \"POINT (10 10)\")\n\t\tclient.HSet(ctx, \"doc_point2\", \"g\", \"POINT (50 50)\")\n\t\tclient.HSet(ctx, \"doc_polygon1\", \"g\", \"POLYGON ((20 20, 25 35, 35 25, 20 20))\")\n\t\tclient.HSet(ctx, \"doc_polygon2\", \"g\", \"POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))\")\n\n\t\tcontainsA, err := client.FTSearchWithArgs(ctx, \"idx2\", \"@g:[contains $shape]\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 3,\n\t\t\t\tParams:         map[string]interface{}{\"shape\": \"POINT(25 25)\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_assert_geosearch_result(&containsA, []string{\"doc_polygon1\"})\n\n\t\tcontainsB, err := client.FTSearchWithArgs(ctx, \"idx2\", \"@g:[contains $shape]\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 3,\n\t\t\t\tParams:         map[string]interface{}{\"shape\": \"POLYGON((24 24, 24 26, 25 25, 24 24))\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_assert_geosearch_result(&containsB, []string{\"doc_polygon1\"})\n\n\t\twithin, err := client.FTSearchWithArgs(ctx, \"idx2\", \"@g:[within $shape]\",\n\t\t\t&redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 3,\n\t\t\t\tParams:         map[string]interface{}{\"shape\": \"POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))\"},\n\t\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_assert_geosearch_result(&within, []string{\"doc_point2\", \"doc_polygon1\"})\n\t})\n\n\tIt(\"should search missing fields\", Label(\"search\", \"ftcreate\", \"ftsearch\", \"NonRedisEnterprise\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{Prefix: []interface{}{\"property:\"}},\n\t\t\t&redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText, Sortable: true},\n\t\t\t&redis.FieldSchema{FieldName: \"features\", FieldType: redis.SearchFieldTypeTag, IndexMissing: true},\n\t\t\t&redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText, IndexMissing: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"property:1\", map[string]interface{}{\n\t\t\t\"title\":       \"Luxury Villa in Malibu\",\n\t\t\t\"features\":    \"pool,sea view,modern\",\n\t\t\t\"description\": \"A stunning modern villa overlooking the Pacific Ocean.\",\n\t\t})\n\n\t\tclient.HSet(ctx, \"property:2\", map[string]interface{}{\n\t\t\t\"title\":       \"Downtown Flat\",\n\t\t\t\"description\": \"Modern flat in central Paris with easy access to metro.\",\n\t\t})\n\n\t\tclient.HSet(ctx, \"property:3\", map[string]interface{}{\n\t\t\t\"title\":    \"Beachfront Bungalow\",\n\t\t\t\"features\": \"beachfront,sun deck\",\n\t\t})\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"ismissing(@features)\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:2\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"-ismissing(@features)\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:1\"))\n\t\tExpect(res.Docs[1].ID).To(BeEquivalentTo(\"property:3\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"ismissing(@description)\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:3\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"-ismissing(@description)\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:1\"))\n\t\tExpect(res.Docs[1].ID).To(BeEquivalentTo(\"property:2\"))\n\t})\n\n\tIt(\"should search empty fields\", Label(\"search\", \"ftcreate\", \"ftsearch\", \"NonRedisEnterprise\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{Prefix: []interface{}{\"property:\"}},\n\t\t\t&redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText, Sortable: true},\n\t\t\t&redis.FieldSchema{FieldName: \"features\", FieldType: redis.SearchFieldTypeTag, IndexEmpty: true},\n\t\t\t&redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText, IndexEmpty: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"property:1\", map[string]interface{}{\n\t\t\t\"title\":       \"Luxury Villa in Malibu\",\n\t\t\t\"features\":    \"pool,sea view,modern\",\n\t\t\t\"description\": \"A stunning modern villa overlooking the Pacific Ocean.\",\n\t\t})\n\n\t\tclient.HSet(ctx, \"property:2\", map[string]interface{}{\n\t\t\t\"title\":       \"Downtown Flat\",\n\t\t\t\"features\":    \"\",\n\t\t\t\"description\": \"Modern flat in central Paris with easy access to metro.\",\n\t\t})\n\n\t\tclient.HSet(ctx, \"property:3\", map[string]interface{}{\n\t\t\t\"title\":       \"Beachfront Bungalow\",\n\t\t\t\"features\":    \"beachfront,sun deck\",\n\t\t\t\"description\": \"\",\n\t\t})\n\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"@features:{\\\"\\\"}\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:2\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"-@features:{\\\"\\\"}\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:1\"))\n\t\tExpect(res.Docs[1].ID).To(BeEquivalentTo(\"property:3\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"@description:''\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:3\"))\n\n\t\tres, err = client.FTSearchWithArgs(ctx, \"idx1\", \"-@description:''\", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: \"id\"}}, NoContent: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"property:1\"))\n\t\tExpect(res.Docs[1].ID).To(BeEquivalentTo(\"property:2\"))\n\t})\n\n\tIt(\"should FTCreate VECTOR with int8 and uint8 types\", Label(\"search\", \"ftcreate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"doesn't work with older redis\")\n\t\t// Define INT8 vector field\n\t\thnswOptionsInt8 := &redis.FTHNSWOptions{\n\t\t\tType:           \"INT8\",\n\t\t\tDim:            2,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\n\t\t// Define UINT8 vector field\n\t\thnswOptionsUint8 := &redis.FTHNSWOptions{\n\t\t\tType:           \"UINT8\",\n\t\t\tDim:            2,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\n\t\t// Create index with INT8 and UINT8 vector fields\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"int8_vector\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsInt8}},\n\t\t\t&redis.FieldSchema{FieldName: \"uint8_vector\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsUint8}},\n\t\t).Result()\n\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\t// Insert vectors in int8 and uint8 format\n\t\tclient.HSet(ctx, \"doc1\", \"int8_vector\", \"\\x01\\x02\", \"uint8_vector\", \"\\x01\\x02\")\n\t\tclient.HSet(ctx, \"doc2\", \"int8_vector\", \"\\x03\\x04\", \"uint8_vector\", \"\\x03\\x04\")\n\n\t\t// Perform KNN search on INT8 vector\n\t\tsearchOptionsInt8 := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"int8_vector\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"int8_vector\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tParams:         map[string]interface{}{\"vec\": \"\\x01\\x02\"},\n\t\t}\n\n\t\tresInt8, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 1 @int8_vector $vec]\", searchOptionsInt8).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resInt8.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\n\t\t// Perform KNN search on UINT8 vector\n\t\tsearchOptionsUint8 := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"uint8_vector\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"uint8_vector\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tParams:         map[string]interface{}{\"vec\": \"\\x01\\x02\"},\n\t\t}\n\n\t\tresUint8, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 1 @uint8_vector $vec]\", searchOptionsUint8).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resUint8.Docs[0].ID).To(BeEquivalentTo(\"doc1\"))\n\t})\n\n\tIt(\"should return special float scores in FT.SEARCH vecsim\", Label(\"search\", \"ftsearch\", \"vecsim\"), func() {\n\t\tSkipBeforeRedisVersion(7.4, \"doesn't work with older redis stack images\")\n\n\t\tvecField := &redis.FTFlatOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            2,\n\t\t\tDistanceMetric: \"IP\",\n\t\t}\n\t\t_, err := client.FTCreate(ctx, \"idx_vec\",\n\t\t\t&redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{\"doc:\"}},\n\t\t\t&redis.FieldSchema{FieldName: \"vector\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: vecField}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tWaitForIndexing(client, \"idx_vec\")\n\n\t\tbigPos := []float32{1e38, 1e38}\n\t\tbigNeg := []float32{-1e38, -1e38}\n\t\tnanVec := []float32{float32(math.NaN()), 0}\n\t\tnegNanVec := []float32{float32(math.Copysign(math.NaN(), -1)), 0}\n\n\t\tclient.HSet(ctx, \"doc:1\", \"vector\", encodeFloat32Vector(bigPos))\n\t\tclient.HSet(ctx, \"doc:2\", \"vector\", encodeFloat32Vector(bigNeg))\n\t\tclient.HSet(ctx, \"doc:3\", \"vector\", encodeFloat32Vector(nanVec))\n\t\tclient.HSet(ctx, \"doc:4\", \"vector\", encodeFloat32Vector(negNanVec))\n\n\t\tsearchOptions := &redis.FTSearchOptions{WithScores: true, Params: map[string]interface{}{\"vec\": encodeFloat32Vector(bigPos)}}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx_vec\", \"*=>[KNN 4 @vector $vec]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(4))\n\n\t\tvar scores []float64\n\t\tfor _, row := range res.Docs {\n\t\t\traw := fmt.Sprintf(\"%v\", row.Fields[\"__vector_score\"])\n\t\t\tf, err := helper.ParseFloat(raw)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tscores = append(scores, f)\n\t\t}\n\n\t\tExpect(scores).To(ContainElement(BeNumerically(\"==\", math.Inf(1))))\n\t\tExpect(scores).To(ContainElement(BeNumerically(\"==\", math.Inf(-1))))\n\n\t\t// For NaN values, use a custom check since NaN != NaN in floating point math\n\t\tnanCount := 0\n\t\tfor _, score := range scores {\n\t\t\tif math.IsNaN(score) {\n\t\t\t\tnanCount++\n\t\t\t}\n\t\t}\n\t\tExpect(nanCount).To(Equal(2))\n\t})\n\n\tIt(\"should FTCreate VECTOR with VAMANA algorithm - basic\", Label(\"search\", \"ftcreate\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            2,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"a\", \"v\", \"aaaaaaaa\")\n\t\tclient.HSet(ctx, \"b\", \"v\", \"aaaabaaa\")\n\t\tclient.HSet(ctx, \"c\", \"v\", \"aaaaabaa\")\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"__v_score\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"__v_score\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tParams:         map[string]interface{}{\"vec\": \"aaaaaaaa\"},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 2 @v $vec]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"a\"))\n\t\tExpect(res.Docs[0].Fields[\"__v_score\"]).To(BeEquivalentTo(\"0\"))\n\t})\n\n\tIt(\"should FTCreate VECTOR with VAMANA algorithm - with compression\", Label(\"search\", \"ftcreate\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT16\",\n\t\t\tDim:               256,\n\t\t\tDistanceMetric:    \"COSINE\",\n\t\t\tCompression:       \"LVQ8\",\n\t\t\tTrainingThreshold: 10240,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\t})\n\n\tIt(\"should FTCreate VECTOR with VAMANA algorithm - advanced parameters\", Label(\"search\", \"ftcreate\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:                   \"FLOAT32\",\n\t\t\tDim:                    512,\n\t\t\tDistanceMetric:         \"IP\",\n\t\t\tCompression:            \"LVQ8\",\n\t\t\tConstructionWindowSize: 300,\n\t\t\tGraphMaxDegree:         128,\n\t\t\tSearchWindowSize:       20,\n\t\t\tEpsilon:                0.02,\n\t\t\tTrainingThreshold:      20480,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\t})\n\n\tIt(\"should fail FTCreate VECTOR with VAMANA - missing required parameters\", Label(\"search\", \"ftcreate\"), func() {\n\t\t// Test missing Type\n\t\tcmd := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{\n\t\t\t\tDim:            2,\n\t\t\t\tDistanceMetric: \"L2\",\n\t\t\t}}})\n\t\tExpect(cmd.Err()).To(HaveOccurred())\n\t\tExpect(cmd.Err().Error()).To(ContainSubstring(\"Type, Dim and DistanceMetric are required for VECTOR VAMANA\"))\n\n\t\t// Test missing Dim\n\t\tcmd = client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{\n\t\t\t\tType:           \"FLOAT32\",\n\t\t\t\tDistanceMetric: \"L2\",\n\t\t\t}}})\n\t\tExpect(cmd.Err()).To(HaveOccurred())\n\t\tExpect(cmd.Err().Error()).To(ContainSubstring(\"Type, Dim and DistanceMetric are required for VECTOR VAMANA\"))\n\n\t\t// Test missing DistanceMetric\n\t\tcmd = client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{\n\t\t\t\tType: \"FLOAT32\",\n\t\t\t\tDim:  2,\n\t\t\t}}})\n\t\tExpect(cmd.Err()).To(HaveOccurred())\n\t\tExpect(cmd.Err().Error()).To(ContainSubstring(\"Type, Dim and DistanceMetric are required for VECTOR VAMANA\"))\n\t})\n\n\tIt(\"should fail FTCreate VECTOR with multiple vector options\", Label(\"search\", \"ftcreate\"), func() {\n\t\t// Test VAMANA + HNSW\n\t\tcmd := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{\n\t\t\t\tVamanaOptions: &redis.FTVamanaOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"},\n\t\t\t\tHNSWOptions:   &redis.FTHNSWOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"},\n\t\t\t}})\n\t\tExpect(cmd.Err()).To(HaveOccurred())\n\t\tExpect(cmd.Err().Error()).To(ContainSubstring(\"VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions\"))\n\n\t\t// Test VAMANA + FLAT\n\t\tcmd = client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{\n\t\t\t\tVamanaOptions: &redis.FTVamanaOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"},\n\t\t\t\tFlatOptions:   &redis.FTFlatOptions{Type: \"FLOAT32\", Dim: 2, DistanceMetric: \"L2\"},\n\t\t\t}})\n\t\tExpect(cmd.Err()).To(HaveOccurred())\n\t\tExpect(cmd.Err().Error()).To(ContainSubstring(\"VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions\"))\n\t})\n\n\tIt(\"should test VAMANA L2 distance metric\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            3,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\t// L2 distance test vectors\n\t\tvectors := [][]float32{\n\t\t\t{1.0, 0.0, 0.0},\n\t\t\t{2.0, 0.0, 0.0},\n\t\t\t{0.0, 1.0, 0.0},\n\t\t\t{5.0, 0.0, 0.0},\n\t\t}\n\n\t\tfor i, vec := range vectors {\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"score\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"score\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 3 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA COSINE distance metric\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            3,\n\t\t\tDistanceMetric: \"COSINE\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := [][]float32{\n\t\t\t{1.0, 0.0, 0.0},\n\t\t\t{0.707, 0.707, 0.0},\n\t\t\t{0.0, 1.0, 0.0},\n\t\t\t{-1.0, 0.0, 0.0},\n\t\t}\n\n\t\tfor i, vec := range vectors {\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"score\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"score\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 3 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA IP distance metric\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            3,\n\t\t\tDistanceMetric: \"IP\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := [][]float32{\n\t\t\t{1.0, 2.0, 3.0},\n\t\t\t{2.0, 1.0, 1.0},\n\t\t\t{3.0, 3.0, 3.0},\n\t\t\t{0.1, 0.1, 0.1},\n\t\t}\n\n\t\tfor i, vec := range vectors {\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"score\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"score\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 3 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc2\"))\n\t})\n\n\tIt(\"should test VAMANA basic functionality\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            4,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := [][]float32{\n\t\t\t{1.0, 2.0, 3.0, 4.0},\n\t\t\t{2.0, 3.0, 4.0, 5.0},\n\t\t\t{3.0, 4.0, 5.0, 6.0},\n\t\t\t{10.0, 11.0, 12.0, 13.0},\n\t\t}\n\n\t\tfor i, vec := range vectors {\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn:         []redis.FTSearchReturn{{FieldName: \"__v_score\"}},\n\t\t\tSortBy:         []redis.FTSearchSortBy{{FieldName: \"__v_score\", Asc: true}},\n\t\t\tDialectVersion: 2,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 3 @v $vec]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\")) // Should be closest to itself\n\t\tExpect(res.Docs[0].Fields[\"__v_score\"]).To(BeEquivalentTo(\"0\"))\n\t})\n\n\tIt(\"should test VAMANA FLOAT16 type\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT16\",\n\t\t\tDim:            4,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := [][]float32{\n\t\t\t{1.5, 2.5, 3.5, 4.5},\n\t\t\t{2.5, 3.5, 4.5, 5.5},\n\t\t\t{3.5, 4.5, 5.5, 6.5},\n\t\t}\n\n\t\tfor i, vec := range vectors {\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat16Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat16Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 2 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(2))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA FLOAT32 type\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            4,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := [][]float32{\n\t\t\t{1.0, 2.0, 3.0, 4.0},\n\t\t\t{2.0, 3.0, 4.0, 5.0},\n\t\t\t{3.0, 4.0, 5.0, 6.0},\n\t\t}\n\n\t\tfor i, vec := range vectors {\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 2 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(2))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA with default dialect\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            2,\n\t\t\tDistanceMetric: \"L2\",\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"a\", \"v\", \"aaaaaaaa\")\n\t\tclient.HSet(ctx, \"b\", \"v\", \"aaaabaaa\")\n\t\tclient.HSet(ctx, \"c\", \"v\", \"aaaaabaa\")\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tReturn: []redis.FTSearchReturn{{FieldName: \"__v_score\"}},\n\t\t\tSortBy: []redis.FTSearchSortBy{{FieldName: \"__v_score\", Asc: true}},\n\t\t\tParams: map[string]interface{}{\"vec\": \"aaaaaaaa\"},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 2 @v $vec]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(2))\n\t})\n\n\tIt(\"should test VAMANA with LVQ8 compression\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT32\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LVQ8\",\n\t\t\tTrainingThreshold: 1024,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 20)\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tvec := make([]float32, 8)\n\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA compression with both vector types\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\n\t\t// Test FLOAT16 with LVQ8\n\t\tvamanaOptions16 := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT16\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LVQ8\",\n\t\t\tTrainingThreshold: 1024,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx16\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v16\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions16}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx16\")\n\n\t\t// Test FLOAT32 with LVQ8\n\t\tvamanaOptions32 := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT32\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LVQ8\",\n\t\t\tTrainingThreshold: 1024,\n\t\t}\n\t\tval, err = client.FTCreate(ctx, \"idx32\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v32\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions32}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx32\")\n\n\t\t// Add data to both indices\n\t\tfor i := 0; i < 15; i++ {\n\t\t\tvec := make([]float32, 8)\n\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc16_%d\", i), \"v16\", encodeFloat16Vector(vec))\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc32_%d\", i), \"v32\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tqueryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}\n\n\t\t// Test FLOAT16 index\n\t\tsearchOptions16 := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat16Vector(queryVec)},\n\t\t}\n\t\tres16, err := client.FTSearchWithArgs(ctx, \"idx16\", \"*=>[KNN 3 @v16 $vec as score]\", searchOptions16).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res16.Total).To(BeEquivalentTo(3))\n\n\t\t// Test FLOAT32 index\n\t\tsearchOptions32 := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(queryVec)},\n\t\t}\n\t\tres32, err := client.FTSearchWithArgs(ctx, \"idx32\", \"*=>[KNN 3 @v32 $vec as score]\", searchOptions32).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res32.Total).To(BeEquivalentTo(3))\n\t})\n\n\tIt(\"should test VAMANA construction window size\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:                   \"FLOAT32\",\n\t\t\tDim:                    6,\n\t\t\tDistanceMetric:         \"L2\",\n\t\t\tConstructionWindowSize: 300,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 20)\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tvec := make([]float32, 6)\n\t\t\tfor j := 0; j < 6; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA graph max degree\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:           \"FLOAT32\",\n\t\t\tDim:            6,\n\t\t\tDistanceMetric: \"COSINE\",\n\t\t\tGraphMaxDegree: 64,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 25)\n\t\tfor i := 0; i < 25; i++ {\n\t\t\tvec := make([]float32, 6)\n\t\t\tfor j := 0; j < 6; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 6 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(6))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA search window size\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:             \"FLOAT32\",\n\t\t\tDim:              6,\n\t\t\tDistanceMetric:   \"L2\",\n\t\t\tSearchWindowSize: 20,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 30)\n\t\tfor i := 0; i < 30; i++ {\n\t\t\tvec := make([]float32, 6)\n\t\t\tfor j := 0; j < 6; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 8 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(8))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should test VAMANA all advanced parameters\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:                   \"FLOAT32\",\n\t\t\tDim:                    8,\n\t\t\tDistanceMetric:         \"L2\",\n\t\t\tCompression:            \"LVQ8\",\n\t\t\tConstructionWindowSize: 200,\n\t\t\tGraphMaxDegree:         32,\n\t\t\tSearchWindowSize:       15,\n\t\t\tEpsilon:                0.01,\n\t\t\tTrainingThreshold:      1024,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 15)\n\t\tfor i := 0; i < 15; i++ {\n\t\t\tvec := make([]float32, 8)\n\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\tExpect(res.Docs[0].ID).To(BeEquivalentTo(\"doc0\"))\n\t})\n\n\tIt(\"should fail when using a non-zero offset with a zero limit\", Label(\"search\", \"ftsearch\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"testIdx\", &redis.FTCreateOptions{}, &redis.FieldSchema{\n\t\t\tFieldName: \"txt\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"testIdx\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"hello world\")\n\n\t\t// Attempt to search with a non-zero offset and zero limit.\n\t\t_, err = client.FTSearchWithArgs(ctx, \"testIdx\", \"hello\", &redis.FTSearchOptions{\n\t\t\tLimitOffset: 5,\n\t\t\tLimit:       0,\n\t\t}).Result()\n\t\tExpect(err).To(HaveOccurred())\n\t})\n\n\tIt(\"should evaluate exponentiation precedence in APPLY expressions correctly\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"txns\", &redis.FTCreateOptions{}, &redis.FieldSchema{\n\t\t\tFieldName: \"dummy\",\n\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txns\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"dummy\", \"dummy\")\n\n\t\tcorrectOptions := &redis.FTAggregateOptions{\n\t\t\tApply: []redis.FTAggregateApply{\n\t\t\t\t{Field: \"(2*3^2)\", As: \"Value\"},\n\t\t\t},\n\t\t\tLimit:       1,\n\t\t\tLimitOffset: 0,\n\t\t}\n\t\tcorrectRes, err := client.FTAggregateWithArgs(ctx, \"txns\", \"*\", correctOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(correctRes.Rows[0].Fields[\"Value\"]).To(BeEquivalentTo(\"18\"))\n\t})\n\n\tIt(\"should return a syntax error when empty strings are used for numeric parameters\", Label(\"search\", \"ftsearch\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"idx\", &redis.FTCreateOptions{}, &redis.FieldSchema{\n\t\t\tFieldName: \"n\",\n\t\t\tFieldType: redis.SearchFieldTypeNumeric,\n\t\t}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"n\", 0)\n\n\t\t_, err = client.FTSearchWithArgs(ctx, \"idx\", \"*\", &redis.FTSearchOptions{\n\t\t\tFilters: []redis.FTSearchFilter{{\n\t\t\t\tFieldName: \"n\",\n\t\t\t\tMin:       \"\",\n\t\t\t\tMax:       \"\",\n\t\t\t}},\n\t\t\tDialectVersion: 2,\n\t\t}).Result()\n\t\tExpect(err).To(HaveOccurred())\n\t})\n\n\tIt(\"should return NaN as default for AVG reducer when no numeric values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestAvg\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestAvg\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"grp\", \"g1\")\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchAvg, Args: []interface{}{\"@n\"}, As: \"avg\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestAvg\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\n\t\tExpect(res.Rows[0].Fields[\"avg\"]).To(SatisfyAny(Equal(\"nan\"), Equal(\"NaN\")))\n\t})\n\n\tIt(\"should return 1 as default for COUNT reducer when no numeric values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestCount\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestCount\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"grp\", \"g1\")\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchCount, As: \"cnt\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestCount\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\n\t\tExpect(res.Rows[0].Fields[\"cnt\"]).To(BeEquivalentTo(\"1\"))\n\t})\n\n\tIt(\"should return NaN as default for SUM reducer when no numeric values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestSum\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestSum\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"grp\", \"g1\")\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchSum, Args: []interface{}{\"@n\"}, As: \"sum\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestSum\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\n\t\tExpect(res.Rows[0].Fields[\"sum\"]).To(SatisfyAny(Equal(\"nan\"), Equal(\"NaN\")))\n\t})\n\n\tIt(\"should return the full requested number of results by re-running the query when some results expire\", Label(\"search\", \"ftsearch\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggExpired\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"order\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggExpired\")\n\n\t\tfor i := 1; i <= 15; i++ {\n\t\t\tkey := fmt.Sprintf(\"doc%d\", i)\n\t\t\t_, err := client.HSet(ctx, key, \"order\", i).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\n\t\t_, err = client.Del(ctx, \"doc3\", \"doc7\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\toptions := &redis.FTSearchOptions{\n\t\t\tSortBy:      []redis.FTSearchSortBy{{FieldName: \"order\", Asc: true}},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       10,\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"aggExpired\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tExpect(len(res.Docs)).To(BeEquivalentTo(10))\n\n\t\tfor _, doc := range res.Docs {\n\t\t\tExpect(doc.ID).ToNot(Or(Equal(\"doc3\"), Equal(\"doc7\")))\n\t\t}\n\t})\n\n\tIt(\"should stop processing and return an error when a timeout occurs\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTimeoutHeavy\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTimeoutHeavy\")\n\n\t\tconst totalDocs = 100000\n\t\tfor i := 0; i < totalDocs; i++ {\n\t\t\tkey := fmt.Sprintf(\"doc%d\", i)\n\t\t\t_, err := client.HSet(ctx, key, \"n\", i).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\t\t// default behaviour was changed in 8.0.1, set to fail to validate the timeout was triggered\n\t\terr = client.ConfigSet(ctx, \"search-on-timeout\", \"fail\").Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\toptions := &redis.FTAggregateOptions{\n\t\t\tSortBy:      []redis.FTAggregateSortBy{{FieldName: \"@n\", Desc: true}},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       100000,\n\t\t\tTimeout:     1, // 1 ms timeout, expected to trigger a timeout error.\n\t\t}\n\t\t_, err = client.FTAggregateWithArgs(ctx, \"aggTimeoutHeavy\", \"*\", options).Result()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(strings.ToLower(err.Error())).To(ContainSubstring(\"timeout\"))\n\t})\n\n\tIt(\"should return 0 as default for COUNT_DISTINCT reducer when no values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestCountDistinct\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"x\", FieldType: redis.SearchFieldTypeText},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestCountDistinct\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"grp\", \"g1\")\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchCountDistinct, Args: []interface{}{\"@x\"}, As: \"distinct_count\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestCountDistinct\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\t\tExpect(res.Rows[0].Fields[\"distinct_count\"]).To(BeEquivalentTo(\"0\"))\n\t})\n\n\tIt(\"should return 0 as default for COUNT_DISTINCTISH reducer when no values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestCountDistinctIsh\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"y\", FieldType: redis.SearchFieldTypeText},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestCountDistinctIsh\")\n\n\t\t_, err = client.HSet(ctx, \"doc1\", \"grp\", \"g1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchCountDistinctish, Args: []interface{}{\"@y\"}, As: \"distinctish_count\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestCountDistinctIsh\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\t\tExpect(res.Rows[0].Fields[\"distinctish_count\"]).To(BeEquivalentTo(\"0\"))\n\t})\n\n\tIt(\"should use BM25 as the default scorer\", Label(\"search\", \"ftsearch\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"scoringTest\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"scoringTest\")\n\n\t\t_, err = client.HSet(ctx, \"doc1\", \"description\", \"red apple\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_, err = client.HSet(ctx, \"doc2\", \"description\", \"green apple\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tresDefault, err := client.FTSearchWithArgs(ctx, \"scoringTest\", \"apple\", &redis.FTSearchOptions{WithScores: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resDefault.Total).To(BeNumerically(\">\", 0))\n\n\t\tresBM25, err := client.FTSearchWithArgs(ctx, \"scoringTest\", \"apple\", &redis.FTSearchOptions{WithScores: true, Scorer: \"BM25\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resBM25.Total).To(BeNumerically(\">\", 0))\n\t\tExpect(resDefault.Total).To(BeEquivalentTo(resBM25.Total))\n\t\tExpect(resDefault.Docs[0].ID).To(BeElementOf(\"doc1\", \"doc2\"))\n\t\tExpect(resDefault.Docs[1].ID).To(BeElementOf(\"doc1\", \"doc2\"))\n\t})\n\n\tIt(\"should return 0 as default for STDDEV reducer when no numeric values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestStddev\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestStddev\")\n\n\t\t_, err = client.HSet(ctx, \"doc1\", \"grp\", \"g1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchStdDev, Args: []interface{}{\"@n\"}, As: \"stddev\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestStddev\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\n\t\tExpect(res.Rows[0].Fields[\"stddev\"]).To(BeEquivalentTo(\"0\"))\n\t})\n\n\tIt(\"should return NaN as default for QUANTILE reducer when no numeric values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestQuantile\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestQuantile\")\n\n\t\t_, err = client.HSet(ctx, \"doc1\", \"grp\", \"g1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchQuantile, Args: []interface{}{\"@n\", 0.5}, As: \"quantile\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestQuantile\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\t\tExpect(res.Rows[0].Fields[\"quantile\"]).To(SatisfyAny(Equal(\"nan\"), Equal(\"NaN\")))\n\t})\n\n\tIt(\"should return nil as default for FIRST_VALUE reducer when no values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestFirstValue\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"t\", FieldType: redis.SearchFieldTypeText},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestFirstValue\")\n\n\t\t_, err = client.HSet(ctx, \"doc1\", \"grp\", \"g1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchFirstValue, Args: []interface{}{\"@t\"}, As: \"first_val\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestFirstValue\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\t\tExpect(res.Rows[0].Fields[\"first_val\"]).To(BeNil())\n\t})\n\n\tIt(\"should fail to add an alias that is an existing index name\", Label(\"search\", \"ftalias\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tval, err = client.FTCreate(ctx, \"idx2\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"name\", FieldType: redis.SearchFieldTypeText},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx2\")\n\n\t\t_, err = client.FTAliasAdd(ctx, \"idx2\", \"idx1\").Result()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(strings.ToLower(err.Error())).To(ContainSubstring(\"alias\"))\n\t})\n\n\tIt(\"should test ft.search with CountOnly param\", Label(\"search\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"txtIndex\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txtIndex\")\n\n\t\t_, err = client.HSet(ctx, \"doc1\", \"txt\", \"hello world\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_, err = client.HSet(ctx, \"doc2\", \"txt\", \"hello go\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t_, err = client.HSet(ctx, \"doc3\", \"txt\", \"hello redis\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\toptsCountOnly := &redis.FTSearchOptions{\n\t\t\tCountOnly:      true,\n\t\t\tLimitOffset:    0,\n\t\t\tLimit:          2, // even though we limit to 2, with count-only no docs are returned\n\t\t\tDialectVersion: 2,\n\t\t}\n\t\tresCountOnly, err := client.FTSearchWithArgs(ctx, \"txtIndex\", \"hello\", optsCountOnly).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resCountOnly.Total).To(BeEquivalentTo(3))\n\t\tExpect(len(resCountOnly.Docs)).To(BeEquivalentTo(0))\n\n\t\toptsLimit := &redis.FTSearchOptions{\n\t\t\tCountOnly:      false,\n\t\t\tLimitOffset:    0,\n\t\t\tLimit:          2, // we expect to get 2 documents even though total count is 3\n\t\t\tDialectVersion: 2,\n\t\t}\n\t\tresLimit, err := client.FTSearchWithArgs(ctx, \"txtIndex\", \"hello\", optsLimit).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resLimit.Total).To(BeEquivalentTo(3))\n\t\tExpect(len(resLimit.Docs)).To(BeEquivalentTo(2))\n\t})\n\n\tIt(\"should reject deprecated configuration keys\", Label(\"search\", \"ftconfig\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\t// List of deprecated configuration keys.\n\t\tdeprecatedKeys := []string{\n\t\t\t\"_FREE_RESOURCE_ON_THREAD\",\n\t\t\t\"_NUMERIC_COMPRESS\",\n\t\t\t\"_NUMERIC_RANGES_PARENTS\",\n\t\t\t\"_PRINT_PROFILE_CLOCK\",\n\t\t\t\"_PRIORITIZE_INTERSECT_UNION_CHILDREN\",\n\t\t\t\"BG_INDEX_SLEEP_GAP\",\n\t\t\t\"CONN_PER_SHARD\",\n\t\t\t\"CURSOR_MAX_IDLE\",\n\t\t\t\"CURSOR_REPLY_THRESHOLD\",\n\t\t\t\"DEFAULT_DIALECT\",\n\t\t\t\"EXTLOAD\",\n\t\t\t\"FORK_GC_CLEAN_THRESHOLD\",\n\t\t\t\"FORK_GC_RETRY_INTERVAL\",\n\t\t\t\"FORK_GC_RUN_INTERVAL\",\n\t\t\t\"FORKGC_SLEEP_BEFORE_EXIT\",\n\t\t\t\"FRISOINI\",\n\t\t\t\"GC_POLICY\",\n\t\t\t\"GCSCANSIZE\",\n\t\t\t\"INDEX_CURSOR_LIMIT\",\n\t\t\t\"MAXAGGREGATERESULTS\",\n\t\t\t\"MAXDOCTABLESIZE\",\n\t\t\t\"MAXPREFIXEXPANSIONS\",\n\t\t\t\"MAXSEARCHRESULTS\",\n\t\t\t\"MIN_OPERATION_WORKERS\",\n\t\t\t\"MIN_PHONETIC_TERM_LEN\",\n\t\t\t\"MINPREFIX\",\n\t\t\t\"MINSTEMLEN\",\n\t\t\t\"NO_MEM_POOLS\",\n\t\t\t\"NOGC\",\n\t\t\t\"ON_TIMEOUT\",\n\t\t\t\"MULTI_TEXT_SLOP\",\n\t\t\t\"PARTIAL_INDEXED_DOCS\",\n\t\t\t\"RAW_DOCID_ENCODING\",\n\t\t\t\"SEARCH_THREADS\",\n\t\t\t\"TIERED_HNSW_BUFFER_LIMIT\",\n\t\t\t\"TIMEOUT\",\n\t\t\t\"TOPOLOGY_VALIDATION_TIMEOUT\",\n\t\t\t\"UNION_ITERATOR_HEAP\",\n\t\t\t\"VSS_MAX_RESIZE\",\n\t\t\t\"WORKERS\",\n\t\t\t\"WORKERS_PRIORITY_BIAS_THRESHOLD\",\n\t\t\t\"MT_MODE\",\n\t\t\t\"WORKER_THREADS\",\n\t\t}\n\n\t\tfor _, key := range deprecatedKeys {\n\t\t\t_, err := client.FTConfigSet(ctx, key, \"test_value\").Result()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t}\n\n\t\tval, err := client.ConfigGet(ctx, \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\t// Since FT.CONFIG is deprecated since redis 8, use CONFIG instead with new search parameters.\n\t\tkeys := make([]string, 0, len(val))\n\t\tfor key := range val {\n\t\t\tkeys = append(keys, key)\n\t\t}\n\t\tExpect(keys).To(ContainElement(ContainSubstring(\"search\")))\n\t})\n\n\tIt(\"should return INF for MIN reducer and -INF for MAX reducer when no numeric values are present\", Label(\"search\", \"ftaggregate\"), func() {\n\t\tSkipBeforeRedisVersion(7.9, \"requires Redis 8.x\")\n\t\tval, err := client.FTCreate(ctx, \"aggTestMinMax\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"grp\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"n\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"aggTestMinMax\")\n\n\t\t_, err = client.HSet(ctx, \"doc1\", \"grp\", \"g1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\treducers := []redis.FTAggregateReducer{\n\t\t\t{Reducer: redis.SearchMin, Args: []interface{}{\"@n\"}, As: \"minValue\"},\n\t\t\t{Reducer: redis.SearchMax, Args: []interface{}{\"@n\"}, As: \"maxValue\"},\n\t\t}\n\t\tgroupBy := []redis.FTAggregateGroupBy{\n\t\t\t{Fields: []interface{}{\"@grp\"}, Reduce: reducers},\n\t\t}\n\t\toptions := &redis.FTAggregateOptions{GroupBy: groupBy}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"aggTestMinMax\", \"*\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Rows).ToNot(BeEmpty())\n\n\t\tExpect(res.Rows[0].Fields[\"minValue\"]).To(BeEquivalentTo(\"inf\"))\n\t\tExpect(res.Rows[0].Fields[\"maxValue\"]).To(BeEquivalentTo(\"-inf\"))\n\t})\n\n\tIt(\"should test VAMANA with LVQ4 compression\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT32\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LVQ4\",\n\t\t\tTrainingThreshold: 1024,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 20)\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tvec := make([]float32, 8)\n\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\t// Don't check specific document ID as vector search is probabilistic\n\t\tExpect(res.Docs).To(HaveLen(5))\n\t})\n\n\tIt(\"should test VAMANA with LeanVec4x8 compression and reduce parameter\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT32\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LeanVec4x8\",\n\t\t\tTrainingThreshold: 1024,\n\t\t\tReduceDim:         4, // Reduce dimension to 4 (half of original 8)\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 20)\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tvec := make([]float32, 8)\n\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\t// Don't check specific document ID as vector search is probabilistic\n\t\tExpect(res.Docs).To(HaveLen(5))\n\t})\n\n\tIt(\"should test VAMANA compression algorithms with FLOAT16 type\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\n\t\tcompressionAlgorithms := []string{\"LVQ4\", \"LVQ4x4\", \"LVQ4x8\", \"LeanVec4x8\", \"LeanVec8x8\"}\n\n\t\tfor _, compression := range compressionAlgorithms {\n\t\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\t\tType:              \"FLOAT16\",\n\t\t\t\tDim:               8,\n\t\t\t\tDistanceMetric:    \"L2\",\n\t\t\t\tCompression:       compression,\n\t\t\t\tTrainingThreshold: 1024,\n\t\t\t}\n\n\t\t\t// Add reduce parameter for LeanVec compressions\n\t\t\tif strings.HasPrefix(compression, \"LeanVec\") {\n\t\t\t\tvamanaOptions.ReduceDim = 4\n\t\t\t}\n\n\t\t\tindexName := fmt.Sprintf(\"idx_%s\", compression)\n\t\t\tval, err := client.FTCreate(ctx, indexName,\n\t\t\t\t&redis.FTCreateOptions{},\n\t\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\t\tWaitForIndexing(client, indexName)\n\n\t\t\tfor i := 0; i < 15; i++ {\n\t\t\t\tvec := make([]float32, 8)\n\t\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\t\tvec[j] = float32(i + j)\n\t\t\t\t}\n\t\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc_%s_%d\", compression, i), \"v\", encodeFloat16Vector(vec))\n\t\t\t}\n\n\t\t\tqueryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}\n\t\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 2,\n\t\t\t\tNoContent:      true,\n\t\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat16Vector(queryVec)},\n\t\t\t}\n\t\t\tres, err := client.FTSearchWithArgs(ctx, indexName, \"*=>[KNN 3 @v $vec as score]\", searchOptions).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\t}\n\t})\n\n\tIt(\"should test VAMANA compression algorithms with FLOAT32 type\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\n\t\tcompressionAlgorithms := []string{\"LVQ4\", \"LVQ4x4\", \"LVQ4x8\", \"LeanVec4x8\", \"LeanVec8x8\"}\n\n\t\tfor _, compression := range compressionAlgorithms {\n\t\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\t\tType:              \"FLOAT32\",\n\t\t\t\tDim:               8,\n\t\t\t\tDistanceMetric:    \"L2\",\n\t\t\t\tCompression:       compression,\n\t\t\t\tTrainingThreshold: 1024,\n\t\t\t}\n\n\t\t\t// Add reduce parameter for LeanVec compressions\n\t\t\tif strings.HasPrefix(compression, \"LeanVec\") {\n\t\t\t\tvamanaOptions.ReduceDim = 4\n\t\t\t}\n\n\t\t\tindexName := fmt.Sprintf(\"idx_%s\", compression)\n\t\t\tval, err := client.FTCreate(ctx, indexName,\n\t\t\t\t&redis.FTCreateOptions{},\n\t\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\t\tWaitForIndexing(client, indexName)\n\n\t\t\tfor i := 0; i < 15; i++ {\n\t\t\t\tvec := make([]float32, 8)\n\t\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\t\tvec[j] = float32(i + j)\n\t\t\t\t}\n\t\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc_%s_%d\", compression, i), \"v\", encodeFloat32Vector(vec))\n\t\t\t}\n\n\t\t\tqueryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}\n\t\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 2,\n\t\t\t\tNoContent:      true,\n\t\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(queryVec)},\n\t\t\t}\n\t\t\tres, err := client.FTSearchWithArgs(ctx, indexName, \"*=>[KNN 3 @v $vec as score]\", searchOptions).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\t}\n\t})\n\n\tIt(\"should test VAMANA compression with different distance metrics\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\n\t\tcompressionAlgorithms := []string{\"LVQ4\", \"LVQ4x4\", \"LVQ4x8\", \"LeanVec4x8\", \"LeanVec8x8\"}\n\t\tdistanceMetrics := []string{\"L2\", \"COSINE\", \"IP\"}\n\n\t\tfor _, compression := range compressionAlgorithms {\n\t\t\tfor _, metric := range distanceMetrics {\n\t\t\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\t\t\tType:              \"FLOAT32\",\n\t\t\t\t\tDim:               8,\n\t\t\t\t\tDistanceMetric:    metric,\n\t\t\t\t\tCompression:       compression,\n\t\t\t\t\tTrainingThreshold: 1024,\n\t\t\t\t}\n\n\t\t\t\t// Add reduce parameter for LeanVec compressions\n\t\t\t\tif strings.HasPrefix(compression, \"LeanVec\") {\n\t\t\t\t\tvamanaOptions.ReduceDim = 4\n\t\t\t\t}\n\n\t\t\t\tindexName := fmt.Sprintf(\"idx_%s_%s\", compression, metric)\n\t\t\t\tval, err := client.FTCreate(ctx, indexName,\n\t\t\t\t\t&redis.FTCreateOptions{},\n\t\t\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tWaitForIndexing(client, indexName)\n\n\t\t\t\tfor i := 0; i < 10; i++ {\n\t\t\t\t\tvec := make([]float32, 8)\n\t\t\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\t\t\tvec[j] = float32(i + j)\n\t\t\t\t\t}\n\t\t\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc_%s_%s_%d\", compression, metric, i), \"v\", encodeFloat32Vector(vec))\n\t\t\t\t}\n\n\t\t\t\tqueryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}\n\t\t\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\t\t\tDialectVersion: 2,\n\t\t\t\t\tNoContent:      true,\n\t\t\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(queryVec)},\n\t\t\t\t}\n\t\t\t\tres, err := client.FTSearchWithArgs(ctx, indexName, \"*=>[KNN 3 @v $vec as score]\", searchOptions).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\t\t}\n\t\t}\n\t})\n\n\tIt(\"should test VAMANA compression with all advanced parameters\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\n\t\tcompressionAlgorithms := []string{\"LVQ4\", \"LVQ4x4\", \"LVQ4x8\", \"LeanVec4x8\", \"LeanVec8x8\"}\n\n\t\tfor _, compression := range compressionAlgorithms {\n\t\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\t\tType:                   \"FLOAT32\",\n\t\t\t\tDim:                    8,\n\t\t\t\tDistanceMetric:         \"L2\",\n\t\t\t\tCompression:            compression,\n\t\t\t\tConstructionWindowSize: 200,\n\t\t\t\tGraphMaxDegree:         32,\n\t\t\t\tSearchWindowSize:       15,\n\t\t\t\tEpsilon:                0.01,\n\t\t\t\tTrainingThreshold:      1024,\n\t\t\t}\n\n\t\t\t// Add reduce parameter for LeanVec compressions\n\t\t\tif strings.HasPrefix(compression, \"LeanVec\") {\n\t\t\t\tvamanaOptions.ReduceDim = 4\n\t\t\t}\n\n\t\t\tindexName := fmt.Sprintf(\"idx_%s_advanced\", compression)\n\t\t\tval, err := client.FTCreate(ctx, indexName,\n\t\t\t\t&redis.FTCreateOptions{},\n\t\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\t\tWaitForIndexing(client, indexName)\n\n\t\t\tfor i := 0; i < 15; i++ {\n\t\t\t\tvec := make([]float32, 8)\n\t\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\t\tvec[j] = float32(i + j)\n\t\t\t\t}\n\t\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc_%s_advanced_%d\", compression, i), \"v\", encodeFloat32Vector(vec))\n\t\t\t}\n\n\t\t\tqueryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}\n\t\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 2,\n\t\t\t\tNoContent:      true,\n\t\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(queryVec)},\n\t\t\t}\n\t\t\tres, err := client.FTSearchWithArgs(ctx, indexName, \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\t}\n\t})\n\n\tIt(\"should fail when using reduce parameter with non-LeanVec compression\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT32\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LVQ8\",\n\t\t\tTrainingThreshold: 1024,\n\t\t\tReduceDim:         4, // This should fail for LVQ8\n\t\t}\n\t\t_, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).To(HaveOccurred())\n\t})\n\n\tIt(\"should test VAMANA with LVQ4 compression in RESP3\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT32\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LVQ4\",\n\t\t\tTrainingThreshold: 1024,\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 20)\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tvec := make([]float32, 8)\n\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\t// Don't check specific document ID as vector search is probabilistic\n\t\tExpect(res.Docs).To(HaveLen(5))\n\t})\n\n\tIt(\"should test VAMANA with LeanVec4x8 compression and reduce parameter in RESP3\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\tType:              \"FLOAT32\",\n\t\t\tDim:               8,\n\t\t\tDistanceMetric:    \"L2\",\n\t\t\tCompression:       \"LeanVec4x8\",\n\t\t\tTrainingThreshold: 1024,\n\t\t\tReduceDim:         4, // Reduce dimension to 4 (half of original 8)\n\t\t}\n\t\tval, err := client.FTCreate(ctx, \"idx1\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tvectors := make([][]float32, 20)\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tvec := make([]float32, 8)\n\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\tvec[j] = float32(i + j)\n\t\t\t}\n\t\t\tvectors[i] = vec\n\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc%d\", i), \"v\", encodeFloat32Vector(vec))\n\t\t}\n\n\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\tDialectVersion: 2,\n\t\t\tNoContent:      true,\n\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat32Vector(vectors[0])},\n\t\t}\n\t\tres, err := client.FTSearchWithArgs(ctx, \"idx1\", \"*=>[KNN 5 @v $vec as score]\", searchOptions).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.Total).To(BeEquivalentTo(5))\n\t\t// Don't check specific document ID as vector search is probabilistic\n\t\tExpect(res.Docs).To(HaveLen(5))\n\t})\n\n\tIt(\"should test VAMANA compression algorithms with FLOAT16 type in RESP3\", Label(\"search\", \"ftcreate\", \"vamana\"), func() {\n\t\tSkipBeforeRedisVersion(8.2, \"VAMANA requires Redis 8.2+\")\n\n\t\tcompressionAlgorithms := []string{\"LVQ4\", \"LVQ4x4\", \"LVQ4x8\", \"LeanVec4x8\", \"LeanVec8x8\"}\n\n\t\tfor _, compression := range compressionAlgorithms {\n\t\t\tvamanaOptions := &redis.FTVamanaOptions{\n\t\t\t\tType:              \"FLOAT16\",\n\t\t\t\tDim:               8,\n\t\t\t\tDistanceMetric:    \"L2\",\n\t\t\t\tCompression:       compression,\n\t\t\t\tTrainingThreshold: 1024,\n\t\t\t}\n\n\t\t\t// Add reduce parameter for LeanVec compressions\n\t\t\tif strings.HasPrefix(compression, \"LeanVec\") {\n\t\t\t\tvamanaOptions.ReduceDim = 4\n\t\t\t}\n\n\t\t\tindexName := fmt.Sprintf(\"idx_resp3_%s\", compression)\n\t\t\tval, err := client.FTCreate(ctx, indexName,\n\t\t\t\t&redis.FTCreateOptions{},\n\t\t\t\t&redis.FieldSchema{FieldName: \"v\", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\t\tWaitForIndexing(client, indexName)\n\n\t\t\t// Add test data\n\t\t\tfor i := 0; i < 15; i++ {\n\t\t\t\tvec := make([]float32, 8)\n\t\t\t\tfor j := 0; j < 8; j++ {\n\t\t\t\t\tvec[j] = float32(i + j)\n\t\t\t\t}\n\t\t\t\tclient.HSet(ctx, fmt.Sprintf(\"doc_resp3_%s_%d\", compression, i), \"v\", encodeFloat16Vector(vec))\n\t\t\t}\n\n\t\t\tqueryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}\n\t\t\tsearchOptions := &redis.FTSearchOptions{\n\t\t\t\tDialectVersion: 2,\n\t\t\t\tNoContent:      true,\n\t\t\t\tParams:         map[string]interface{}{\"vec\": encodeFloat16Vector(queryVec)},\n\t\t\t}\n\t\t\tres, err := client.FTSearchWithArgs(ctx, indexName, \"*=>[KNN 3 @v $vec as score]\", searchOptions).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res.Total).To(BeEquivalentTo(3))\n\t\t}\n\t})\n\n\tIt(\"should parse FTInfo response with vector fields\", Label(\"search\", \"ftinfo\", \"NonRedisEnterprise\"), func() {\n\t\t// Create an index with multiple field types including vector\n\t\tval, err := client.FTCreate(ctx, \"idx_vector\",\n\t\t\t&redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{\n\t\t\t\tFieldName: \"prompt\",\n\t\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t\t},\n\t\t\t&redis.FieldSchema{\n\t\t\t\tFieldName: \"response\",\n\t\t\t\tFieldType: redis.SearchFieldTypeText,\n\t\t\t},\n\t\t\t&redis.FieldSchema{\n\t\t\t\tFieldName: \"exact_digest\",\n\t\t\t\tFieldType: redis.SearchFieldTypeTag,\n\t\t\t\tSeparator: \",\",\n\t\t\t},\n\t\t\t&redis.FieldSchema{\n\t\t\t\tFieldName:  \"prompt_vector\",\n\t\t\t\tFieldType:  redis.SearchFieldTypeVector,\n\t\t\t\tVectorArgs: &redis.FTVectorArgs{HNSWOptions: &redis.FTHNSWOptions{Type: \"FLOAT32\", Dim: 1536, DistanceMetric: \"COSINE\", MaxEdgesPerNode: 16, MaxAllowedEdgesPerNode: 64}},\n\t\t\t},\n\t\t).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx_vector\")\n\n\t\t// Get FT.INFO\n\t\tresInfo, err := client.FTInfo(ctx, \"idx_vector\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Validate index definition\n\t\tExpect(resInfo.IndexDefinition.KeyType).To(Equal(\"HASH\"))\n\t\tExpect(resInfo.IndexDefinition.DefaultScore).To(Equal(float64(1)))\n\n\t\t// Validate attributes\n\t\tExpect(len(resInfo.Attributes)).To(Equal(4))\n\n\t\t// Check prompt field (TEXT)\n\t\tpromptAttr := resInfo.Attributes[0]\n\t\tExpect(promptAttr.Identifier).To(Equal(\"prompt\"))\n\t\tExpect(promptAttr.Attribute).To(Equal(\"prompt\"))\n\t\tExpect(promptAttr.Type).To(Equal(\"TEXT\"))\n\n\t\t// Check response field (TEXT)\n\t\tresponseAttr := resInfo.Attributes[1]\n\t\tExpect(responseAttr.Identifier).To(Equal(\"response\"))\n\t\tExpect(responseAttr.Attribute).To(Equal(\"response\"))\n\t\tExpect(responseAttr.Type).To(Equal(\"TEXT\"))\n\n\t\t// Check exact_digest field (TAG)\n\t\ttagAttr := resInfo.Attributes[2]\n\t\tExpect(tagAttr.Identifier).To(Equal(\"exact_digest\"))\n\t\tExpect(tagAttr.Attribute).To(Equal(\"exact_digest\"))\n\t\tExpect(tagAttr.Type).To(Equal(\"TAG\"))\n\n\t\t// Check prompt_vector field (VECTOR)\n\t\tvectorAttr := resInfo.Attributes[3]\n\t\tExpect(vectorAttr.Identifier).To(Equal(\"prompt_vector\"))\n\t\tExpect(vectorAttr.Attribute).To(Equal(\"prompt_vector\"))\n\t\tExpect(vectorAttr.Type).To(Equal(\"VECTOR\"))\n\n\t\t// Validate numeric fields\n\t\tExpect(resInfo.NumDocs).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.MaxDocID).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.NumTerms).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.NumRecords).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.Indexing).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.PercentIndexed).To(BeEquivalentTo(1))\n\t\tExpect(resInfo.HashIndexingFailures).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.Cleaning).To(BeEquivalentTo(0))\n\n\t\t// Validate memory stats\n\t\tExpect(resInfo.InvertedSzMB).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.VectorIndexSzMB).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.TotalInvertedIndexBlocks).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.OffsetVectorsSzMB).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.DocTableSizeMB).To(BeNumerically(\">=\", 0))\n\t\tExpect(resInfo.SortableValuesSizeMB).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.TagOverheadSzMB).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.TextOverheadSzMB).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.TotalIndexMemorySzMB).To(BeNumerically(\">=\", 0))\n\t\tExpect(resInfo.GeoshapesSzMB).To(BeEquivalentTo(0))\n\n\t\t// Validate average stats (should be \"nan\" for empty index)\n\t\tExpect(resInfo.RecordsPerDocAvg).To(Equal(\"nan\"))\n\t\tExpect(resInfo.BytesPerRecordAvg).To(Equal(\"nan\"))\n\t\tExpect(resInfo.OffsetsPerTermAvg).To(Equal(\"nan\"))\n\t\tExpect(resInfo.OffsetBitsPerRecordAvg).To(Equal(\"nan\"))\n\n\t\t// Validate cursor stats\n\t\tExpect(resInfo.CursorStats.GlobalIdle).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.CursorStats.GlobalTotal).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.CursorStats.IndexCapacity).To(BeNumerically(\">=\", 0))\n\t\tExpect(resInfo.CursorStats.IndexTotal).To(BeEquivalentTo(0))\n\n\t\t// Validate dialect stats\n\t\tExpect(resInfo.DialectStats).To(HaveKey(\"dialect_1\"))\n\t\tExpect(resInfo.DialectStats).To(HaveKey(\"dialect_2\"))\n\t\tExpect(resInfo.DialectStats).To(HaveKey(\"dialect_3\"))\n\t\tExpect(resInfo.DialectStats).To(HaveKey(\"dialect_4\"))\n\n\t\t// Validate GC stats\n\t\tExpect(resInfo.GCStats.BytesCollected).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.GCStats.TotalMsRun).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.GCStats.TotalCycles).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.GCStats.AverageCycleTimeMs).To(Equal(\"nan\"))\n\t\tExpect(resInfo.GCStats.LastRunTimeMs).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.GCStats.GCNumericTreesMissed).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.GCStats.GCBlocksDenied).To(BeEquivalentTo(0))\n\n\t\t// Validate Index Errors\n\t\tExpect(resInfo.IndexErrors.IndexingFailures).To(BeEquivalentTo(0))\n\t\tExpect(resInfo.IndexErrors.LastIndexingError).To(Equal(\"N/A\"))\n\t\tExpect(resInfo.IndexErrors.LastIndexingErrorKey).To(Equal(\"N/A\"))\n\n\t\t// Validate field statistics\n\t\tExpect(len(resInfo.FieldStatistics)).To(Equal(4))\n\t\tfor _, fieldStat := range resInfo.FieldStatistics {\n\t\t\tExpect(fieldStat.Identifier).To(BeElementOf(\"prompt\", \"response\", \"exact_digest\", \"prompt_vector\"))\n\t\t\tExpect(fieldStat.Attribute).To(BeElementOf(\"prompt\", \"response\", \"exact_digest\", \"prompt_vector\"))\n\t\t\tExpect(fieldStat.IndexErrors.IndexingFailures).To(BeEquivalentTo(0))\n\t\t\tExpect(fieldStat.IndexErrors.LastIndexingError).To(Equal(\"N/A\"))\n\t\t\tExpect(fieldStat.IndexErrors.LastIndexingErrorKey).To(Equal(\"N/A\"))\n\t\t}\n\t})\n})\n\n// Hybrid Search Tests\nvar _ = Describe(\"FT.HYBRID Commands\", func() {\n\tctx := context.TODO()\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(&redis.Options{Addr: \":6379\", Protocol: 2})\n\t\t// Create index with text, numeric, tag fields and vector fields\n\t\terr := client.FTCreate(ctx, \"hybrid_idx\", &redis.FTCreateOptions{},\n\t\t\t&redis.FieldSchema{FieldName: \"description\", FieldType: redis.SearchFieldTypeText},\n\t\t\t&redis.FieldSchema{FieldName: \"price\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t\t&redis.FieldSchema{FieldName: \"color\", FieldType: redis.SearchFieldTypeTag},\n\t\t\t&redis.FieldSchema{FieldName: \"item_type\", FieldType: redis.SearchFieldTypeTag},\n\t\t\t&redis.FieldSchema{FieldName: \"size\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t\t&redis.FieldSchema{\n\t\t\t\tFieldName: \"embedding\",\n\t\t\t\tFieldType: redis.SearchFieldTypeVector,\n\t\t\t\tVectorArgs: &redis.FTVectorArgs{\n\t\t\t\t\tFlatOptions: &redis.FTFlatOptions{\n\t\t\t\t\t\tType:           \"FLOAT32\",\n\t\t\t\t\t\tDim:            4,\n\t\t\t\t\t\tDistanceMetric: \"L2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t&redis.FieldSchema{\n\t\t\t\tFieldName: \"embedding_hnsw\",\n\t\t\t\tFieldType: redis.SearchFieldTypeVector,\n\t\t\t\tVectorArgs: &redis.FTVectorArgs{\n\t\t\t\t\tHNSWOptions: &redis.FTHNSWOptions{\n\t\t\t\t\t\tType:           \"FLOAT32\",\n\t\t\t\t\t\tDim:            4,\n\t\t\t\t\t\tDistanceMetric: \"L2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tWaitForIndexing(client, \"hybrid_idx\")\n\n\t\t// Add test data\n\t\titems := []struct {\n\t\t\tkey         string\n\t\t\tdescription string\n\t\t\tprice       int\n\t\t\tcolor       string\n\t\t\titemType    string\n\t\t\tsize        int\n\t\t\tembedding   []float32\n\t\t}{\n\t\t\t{\"item:0\", \"red shoes\", 15, \"red\", \"shoes\", 10, []float32{1.0, 2.0, 7.0, 8.0}},\n\t\t\t{\"item:1\", \"green shoes with red laces\", 16, \"green\", \"shoes\", 11, []float32{1.0, 4.0, 7.0, 8.0}},\n\t\t\t{\"item:2\", \"red dress\", 17, \"red\", \"dress\", 12, []float32{1.0, 2.0, 6.0, 5.0}},\n\t\t\t{\"item:3\", \"orange dress\", 18, \"orange\", \"dress\", 10, []float32{2.0, 3.0, 6.0, 5.0}},\n\t\t\t{\"item:4\", \"black shoes\", 19, \"black\", \"shoes\", 11, []float32{5.0, 6.0, 7.0, 8.0}},\n\t\t}\n\n\t\tfor _, item := range items {\n\t\t\tclient.HSet(ctx, item.key, map[string]interface{}{\n\t\t\t\t\"description\":    item.description,\n\t\t\t\t\"price\":          item.price,\n\t\t\t\t\"color\":          item.color,\n\t\t\t\t\"item_type\":      item.itemType,\n\t\t\t\t\"size\":           item.size,\n\t\t\t\t\"embedding\":      encodeFloat32Vector(item.embedding),\n\t\t\t\t\"embedding_hnsw\": encodeFloat32Vector(item.embedding),\n\t\t\t})\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\terr := client.FTDropIndex(ctx, \"hybrid_idx\").Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should perform basic hybrid search\", Label(\"search\", \"fthybrid\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\t// Basic hybrid search combining text and vector search\n\t\tsearchQuery := \"@color:{red}\"\n\t\tvectorData := encodeFloat32Vector([]float32{-100, -200, -200, -300})\n\n\t\tcmd := client.FTHybrid(ctx, \"hybrid_idx\", searchQuery, \"embedding\", &redis.VectorFP32{Val: vectorData})\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\">\", 0))\n\n\t\t// Check that results contain expected fields\n\t\tfor _, result := range res.Results {\n\t\t\tExpect(result).To(HaveKey(\"__score\"))\n\t\t\tExpect(result).To(HaveKey(\"__key\"))\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with scorer\", Label(\"search\", \"fthybrid\", \"scorer\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\t// Test with TFIDF scorer\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{\n\t\t\t\t\tQuery:  \"@color:{red}\",\n\t\t\t\t\tScorer: \"TFIDF\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField: \"embedding\",\n\t\t\t\t\tVectorData:  &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad:        []string{\"@description\", \"@color\", \"@price\", \"@size\", \"@__score\"},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\n\t\t// Verify that we got results with the fields we asked for\n\t\tfor _, result := range res.Results {\n\t\t\t// Since we're using TFIDF scorer, the search results should be scored accordingly\n\t\t\tExpect(result).To(HaveKey(\"__score\"))\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with vector filter\", Label(\"search\", \"fthybrid\", \"filter\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\t// This query won't have results from search, so we can validate vector filter\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{none}\"}, // This won't match anything\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField: \"embedding\",\n\t\t\t\t\tVectorData:  &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},\n\t\t\t\t\tFilter:      \"@price:[15 16] @size:[10 11]\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad: []string{\"@description\", \"@color\", \"@price\", \"@size\", \"@__score\"},\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\n\t\t// Verify that all results match the filter criteria\n\t\tfor _, result := range res.Results {\n\t\t\tif price, exists := result[\"price\"]; exists {\n\t\t\t\tpriceStr := fmt.Sprintf(\"%v\", price)\n\t\t\t\tpriceFloat, err := helper.ParseFloat(priceStr)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(priceFloat).To(BeNumerically(\">=\", 15))\n\t\t\t\tExpect(priceFloat).To(BeNumerically(\"<=\", 16))\n\t\t\t}\n\t\t\tif size, exists := result[\"size\"]; exists {\n\t\t\t\tsizeStr := fmt.Sprintf(\"%v\", size)\n\t\t\t\tsizeFloat, err := helper.ParseFloat(sizeStr)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(sizeFloat).To(BeNumerically(\">=\", 10))\n\t\t\t\tExpect(sizeFloat).To(BeNumerically(\"<=\", 11))\n\t\t\t}\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with KNN method\", Label(\"search\", \"fthybrid\", \"knn\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{none}\"}, // This won't match anything\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:  \"embedding\",\n\t\t\t\t\tVectorData:   &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},\n\t\t\t\t\tMethod:       \"KNN\",\n\t\t\t\t\tMethodParams: []interface{}{\"K\", 3}, // K=3 as key-value pair\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(Equal(3)) // Should return exactly K=3 results\n\t\tExpect(len(res.Results)).To(Equal(3))\n\t})\n\n\tIt(\"should perform hybrid search with RANGE method\", Label(\"search\", \"fthybrid\", \"range\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{none}\"}, // This won't match anything\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:  \"embedding\",\n\t\t\t\t\tVectorData:   &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t\tMethod:       \"RANGE\",\n\t\t\t\t\tMethodParams: []interface{}{\"RADIUS\", 2}, // RADIUS=2 as key-value pair\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\t})\n\n\tIt(\"should perform hybrid search with LINEAR combine method\", Label(\"search\", \"fthybrid\", \"combine\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField: \"embedding\",\n\t\t\t\t\tVectorData:  &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t},\n\t\t\t},\n\t\t\tCombine: &redis.FTHybridCombineOptions{\n\t\t\t\tMethod: redis.FTHybridCombineLinear,\n\t\t\t\tAlpha:  0.5,\n\t\t\t\tBeta:   0.5,\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\t})\n\n\tIt(\"should perform hybrid search with RRF combine method\", Label(\"search\", \"fthybrid\", \"rrf\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField: \"embedding\",\n\t\t\t\t\tVectorData:  &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t},\n\t\t\t},\n\t\t\tCombine: &redis.FTHybridCombineOptions{\n\t\t\t\tMethod:   redis.FTHybridCombineRRF,\n\t\t\t\tWindow:   3,\n\t\t\t\tConstant: 0.5,\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\t})\n\n\tIt(\"should perform hybrid search with LOAD and APPLY\", Label(\"search\", \"fthybrid\", \"load\", \"apply\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField: \"embedding\",\n\t\t\t\t\tVectorData:  &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad: []string{\"@description\", \"@color\", \"@price\", \"@size\", \"@__score\"},\n\t\t\tApply: []redis.FTHybridApply{\n\t\t\t\t{\n\t\t\t\t\tExpression: \"@price - (@price * 0.1)\",\n\t\t\t\t\tAsField:    \"price_discount\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExpression: \"@price_discount * 0.2\",\n\t\t\t\t\tAsField:    \"tax_discount\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\n\t\t// Verify that applied fields exist\n\t\tfor _, result := range res.Results {\n\t\t\tExpect(result).To(HaveKey(\"price_discount\"))\n\t\t\tExpect(result).To(HaveKey(\"tax_discount\"))\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with LIMIT\", Label(\"search\", \"fthybrid\", \"limit\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField: \"embedding\",\n\t\t\t\t\tVectorData:  &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       2,\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 2))\n\t})\n\n\tIt(\"should perform hybrid search with SORTBY\", Label(\"search\", \"fthybrid\", \"sortby\"), func() {\n\t\tSkipBeforeRedisVersion(8.4, \"no support\")\n\t\tSkipAfterRedisVersion(8.5, \"inline vector blobs not supported in Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField: \"embedding\",\n\t\t\t\t\tVectorData:  &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad: []string{\"@color\", \"@price\"},\n\t\t\tApply: []redis.FTHybridApply{\n\t\t\t\t{\n\t\t\t\t\tExpression: \"@price - (@price * 0.1)\",\n\t\t\t\t\tAsField:    \"price_discount\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tSortBy: []redis.FTSearchSortBy{\n\t\t\t\t{FieldName: \"@price_discount\", Desc: true},\n\t\t\t\t{FieldName: \"@color\", Asc: true},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       5,\n\t\t}\n\n\t\tcmd := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options)\n\n\t\tres, err := cmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 5))\n\n\t\t// Check that results are sorted - first result should have higher price_discount\n\t\tif len(res.Results) > 1 {\n\t\t\tfirstPriceStr := fmt.Sprintf(\"%v\", res.Results[0][\"price_discount\"])\n\t\t\tsecondPriceStr := fmt.Sprintf(\"%v\", res.Results[1][\"price_discount\"])\n\t\t\tfirstPrice, err1 := helper.ParseFloat(firstPriceStr)\n\t\t\tsecondPrice, err2 := helper.ParseFloat(secondPriceStr)\n\n\t\t\tif err1 == nil && err2 == nil && firstPrice != secondPrice {\n\t\t\t\tExpect(firstPrice).To(BeNumerically(\">=\", secondPrice))\n\t\t\t}\n\t\t}\n\t})\n\n\t// Redis 8.6+ tests using PARAMS-based vector approach\n\tIt(\"should perform basic hybrid search (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\t// Basic hybrid search combining text and vector search\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{-100, -200, -200, -300})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\">\", 0))\n\n\t\t// Check that results contain expected fields\n\t\tfor _, result := range res.Results {\n\t\t\tExpect(result).To(HaveKey(\"__score\"))\n\t\t\tExpect(result).To(HaveKey(\"__key\"))\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with scorer (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"scorer\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\t// Test with TFIDF scorer\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{\n\t\t\t\t\tQuery:  \"@color:{red}\",\n\t\t\t\t\tScorer: \"TFIDF\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad:        []string{\"@description\", \"@color\", \"@price\", \"@size\", \"@__score\"},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\n\t\t// Verify that we got results with the fields we asked for\n\t\tfor _, result := range res.Results {\n\t\t\t// Since we're using TFIDF scorer, the search results should be scored accordingly\n\t\t\tExpect(result).To(HaveKey(\"__score\"))\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with vector filter (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"filter\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\t// This query won't have results from search, so we can validate vector filter\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{none}\"}, // This won't match anything\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t\tFilter:          \"@price:[15 16] @size:[10 11]\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad: []string{\"@description\", \"@color\", \"@price\", \"@size\", \"@__score\"},\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\n\t\t// Verify that all results match the filter criteria\n\t\tfor _, result := range res.Results {\n\t\t\tif price, exists := result[\"price\"]; exists {\n\t\t\t\tpriceStr := fmt.Sprintf(\"%v\", price)\n\t\t\t\tpriceFloat, err := helper.ParseFloat(priceStr)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(priceFloat).To(BeNumerically(\">=\", 15))\n\t\t\t\tExpect(priceFloat).To(BeNumerically(\"<=\", 16))\n\t\t\t}\n\t\t\tif size, exists := result[\"size\"]; exists {\n\t\t\t\tsizeStr := fmt.Sprintf(\"%v\", size)\n\t\t\t\tsizeFloat, err := helper.ParseFloat(sizeStr)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(sizeFloat).To(BeNumerically(\">=\", 10))\n\t\t\t\tExpect(sizeFloat).To(BeNumerically(\"<=\", 11))\n\t\t\t}\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with KNN method (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"knn\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{none}\"}, // This won't match anything\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 2, 3})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t\tMethod:          \"KNN\",\n\t\t\t\t\tMethodParams:    []interface{}{\"K\", 3}, // K=3 as key-value pair\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(Equal(3)) // Should return exactly K=3 results\n\t\tExpect(len(res.Results)).To(Equal(3))\n\t})\n\n\tIt(\"should perform hybrid search with RANGE method (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"range\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{none}\"}, // This won't match anything\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t\tMethod:          \"RANGE\",\n\t\t\t\t\tMethodParams:    []interface{}{\"RADIUS\", 2}, // RADIUS=2 as key-value pair\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\t})\n\n\tIt(\"should perform hybrid search with LINEAR combine method (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"combine\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tCombine: &redis.FTHybridCombineOptions{\n\t\t\t\tMethod: redis.FTHybridCombineLinear,\n\t\t\t\tAlpha:  0.5,\n\t\t\t\tBeta:   0.5,\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\t})\n\n\tIt(\"should perform hybrid search with RRF combine method (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"rrf\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tCombine: &redis.FTHybridCombineOptions{\n\t\t\t\tMethod:   redis.FTHybridCombineRRF,\n\t\t\t\tWindow:   3,\n\t\t\t\tConstant: 0.5,\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\t})\n\n\tIt(\"should perform hybrid search with LOAD and APPLY (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"load\", \"apply\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad: []string{\"@description\", \"@color\", \"@price\", \"@size\", \"@__score\"},\n\t\t\tApply: []redis.FTHybridApply{\n\t\t\t\t{\n\t\t\t\t\tExpression: \"@price - (@price * 0.1)\",\n\t\t\t\t\tAsField:    \"price_discount\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExpression: \"@price_discount * 0.2\",\n\t\t\t\t\tAsField:    \"tax_discount\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       3,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 3))\n\n\t\t// Verify that applied fields exist\n\t\tfor _, result := range res.Results {\n\t\t\tExpect(result).To(HaveKey(\"price_discount\"))\n\t\t\tExpect(result).To(HaveKey(\"tax_discount\"))\n\t\t}\n\t})\n\n\tIt(\"should perform hybrid search with LIMIT (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"limit\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       2,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 2))\n\t})\n\n\tIt(\"should perform hybrid search with SORTBY (8.6+ PARAMS)\", Label(\"search\", \"fthybrid\", \"sortby\", \"params\"), func() {\n\t\tSkipBeforeRedisVersion(8.6, \"PARAMS-based vector support requires Redis 8.6+\")\n\t\toptions := &redis.FTHybridOptions{\n\t\t\tCountExpressions: 2,\n\t\t\tSearchExpressions: []redis.FTHybridSearchExpression{\n\t\t\t\t{Query: \"@color:{red}\"},\n\t\t\t},\n\t\t\tVectorExpressions: []redis.FTHybridVectorExpression{\n\t\t\t\t{\n\t\t\t\t\tVectorField:     \"embedding\",\n\t\t\t\t\tVectorData:      &redis.VectorFP32{Val: encodeFloat32Vector([]float32{1, 2, 7, 6})},\n\t\t\t\t\tVectorParamName: \"vec\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tLoad: []string{\"@color\", \"@price\"},\n\t\t\tApply: []redis.FTHybridApply{\n\t\t\t\t{\n\t\t\t\t\tExpression: \"@price - (@price * 0.1)\",\n\t\t\t\t\tAsField:    \"price_discount\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tSortBy: []redis.FTSearchSortBy{\n\t\t\t\t{FieldName: \"@price_discount\", Desc: true},\n\t\t\t\t{FieldName: \"@color\", Asc: true},\n\t\t\t},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       5,\n\t\t}\n\n\t\tres, err := client.FTHybridWithArgs(ctx, \"hybrid_idx\", options).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res.TotalResults).To(BeNumerically(\">\", 0))\n\t\tExpect(len(res.Results)).To(BeNumerically(\"<=\", 5))\n\n\t\t// Check that results are sorted - first result should have higher price_discount\n\t\tif len(res.Results) > 1 {\n\t\t\tfirstPriceStr := fmt.Sprintf(\"%v\", res.Results[0][\"price_discount\"])\n\t\t\tsecondPriceStr := fmt.Sprintf(\"%v\", res.Results[1][\"price_discount\"])\n\t\t\tfirstPrice, err1 := helper.ParseFloat(firstPriceStr)\n\t\t\tsecondPrice, err2 := helper.ParseFloat(secondPriceStr)\n\n\t\t\tif err1 == nil && err2 == nil && firstPrice != secondPrice {\n\t\t\t\tExpect(firstPrice).To(BeNumerically(\">=\", secondPrice))\n\t\t\t}\n\t\t}\n\t})\n})\n\nfunc _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) {\n\tids := make([]string, len(result.Docs))\n\tfor i, doc := range result.Docs {\n\t\tids[i] = doc.ID\n\t}\n\tExpect(ids).To(ConsistOf(expectedDocIDs))\n\tExpect(result.Total).To(BeEquivalentTo(len(expectedDocIDs)))\n}\n\nvar _ = Describe(\"RediSearch FT.Config with Resp2 and Resp3\", Label(\"search\", \"NonRedisEnterprise\"), func() {\n\n\tvar clientResp2 *redis.Client\n\tvar clientResp3 *redis.Client\n\tBeforeEach(func() {\n\t\tclientResp2 = redis.NewClient(&redis.Options{Addr: \":6379\", Protocol: 2})\n\t\tclientResp3 = redis.NewClient(&redis.Options{Addr: \":6379\", Protocol: 3, UnstableResp3: true})\n\t\tExpect(clientResp3.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(clientResp2.Close()).NotTo(HaveOccurred())\n\t\tExpect(clientResp3.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should FTConfigSet and FTConfigGet with resp2 and resp3\", Label(\"search\", \"ftconfigget\", \"ftconfigset\", \"NonRedisEnterprise\"), func() {\n\t\tval, err := clientResp3.FTConfigSet(ctx, \"MINPREFIX\", \"1\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\n\t\tres2, err := clientResp2.FTConfigGet(ctx, \"MINPREFIX\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res2).To(BeEquivalentTo(map[string]interface{}{\"MINPREFIX\": \"1\"}))\n\n\t\tres3, err := clientResp3.FTConfigGet(ctx, \"MINPREFIX\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res3).To(BeEquivalentTo(map[string]interface{}{\"MINPREFIX\": \"1\"}))\n\t})\n\n\tIt(\"should FTConfigGet all resp2 and resp3\", Label(\"search\", \"NonRedisEnterprise\"), func() {\n\t\tres2, err := clientResp2.FTConfigGet(ctx, \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tres3, err := clientResp3.FTConfigGet(ctx, \"*\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(len(res3)).To(BeEquivalentTo(len(res2)))\n\t\tExpect(res2[\"DEFAULT_DIALECT\"]).To(BeEquivalentTo(res2[\"DEFAULT_DIALECT\"]))\n\t})\n})\n\nvar _ = Describe(\"RediSearch commands Resp 3\", Label(\"search\"), func() {\n\tctx := context.TODO()\n\tvar client *redis.Client\n\tvar client2 *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(&redis.Options{Addr: \":6379\", Protocol: 3, UnstableResp3: true})\n\t\tclient2 = redis.NewClient(&redis.Options{Addr: \":6379\", Protocol: 3})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should handle FTAggregate with Unstable RESP3 Search Module and without stability\", Label(\"search\", \"ftcreate\", \"ftaggregate\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"PrimaryKey\", FieldType: redis.SearchFieldTypeText, Sortable: true}\n\t\tnum1 := &redis.FieldSchema{FieldName: \"CreatedDateTimeUTC\", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, num1).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"PrimaryKey\", \"9::362330\", \"CreatedDateTimeUTC\", \"637387878524969984\")\n\t\tclient.HSet(ctx, \"doc2\", \"PrimaryKey\", \"9::362329\", \"CreatedDateTimeUTC\", \"637387875859270016\")\n\n\t\toptions := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: \"@CreatedDateTimeUTC * 10\", As: \"CreatedDateTimeUTC\"}}}\n\t\tres, err := client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).RawResult()\n\t\tresults := res.(map[interface{}]interface{})[\"results\"].([]interface{})\n\t\tExpect(results[0].(map[interface{}]interface{})[\"extra_attributes\"].(map[interface{}]interface{})[\"CreatedDateTimeUTC\"]).\n\t\t\tTo(Or(BeEquivalentTo(\"6373878785249699840\"), BeEquivalentTo(\"6373878758592700416\")))\n\t\tExpect(results[1].(map[interface{}]interface{})[\"extra_attributes\"].(map[interface{}]interface{})[\"CreatedDateTimeUTC\"]).\n\t\t\tTo(Or(BeEquivalentTo(\"6373878785249699840\"), BeEquivalentTo(\"6373878758592700416\")))\n\n\t\trawVal := client.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).RawVal()\n\t\trawValResults := rawVal.(map[interface{}]interface{})[\"results\"].([]interface{})\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(rawValResults[0]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1])))\n\t\tExpect(rawValResults[1]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1])))\n\n\t\t// Test with UnstableResp3 false - should return error instead of panic\n\t\toptions = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: \"@CreatedDateTimeUTC * 10\", As: \"CreatedDateTimeUTC\"}}}\n\t\trawRes, err := client2.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).RawResult()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"RESP3 responses for this command are disabled\"))\n\t\tExpect(rawRes).To(BeNil())\n\n\t\trawVal = client2.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).RawVal()\n\t\tExpect(client2.FTAggregateWithArgs(ctx, \"idx1\", \"*\", options).Err()).To(HaveOccurred())\n\t\tExpect(rawVal).To(BeNil())\n\n\t})\n\n\tIt(\"should handle FTInfo with Unstable RESP3 Search Module and without stability\", Label(\"search\", \"ftcreate\", \"ftinfo\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tresInfo, err := client.FTInfo(ctx, \"idx1\").RawResult()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tattributes := resInfo.(map[interface{}]interface{})[\"attributes\"].([]interface{})\n\t\tflags := attributes[0].(map[interface{}]interface{})[\"flags\"].([]interface{})\n\t\tExpect(flags).To(ConsistOf(\"SORTABLE\", \"NOSTEM\"))\n\n\t\tvalInfo := client.FTInfo(ctx, \"idx1\").RawVal()\n\t\tattributes = valInfo.(map[interface{}]interface{})[\"attributes\"].([]interface{})\n\t\tflags = attributes[0].(map[interface{}]interface{})[\"flags\"].([]interface{})\n\t\tExpect(flags).To(ConsistOf(\"SORTABLE\", \"NOSTEM\"))\n\n\t\t// Test with UnstableResp3 false - should return error instead of panic\n\t\trawResInfo, err := client2.FTInfo(ctx, \"idx1\").RawResult()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"RESP3 responses for this command are disabled\"))\n\t\tExpect(rawResInfo).To(BeNil())\n\n\t\trawValInfo := client2.FTInfo(ctx, \"idx1\").RawVal()\n\t\tExpect(client2.FTInfo(ctx, \"idx1\").Err()).To(HaveOccurred())\n\t\tExpect(rawValInfo).To(BeNil())\n\t})\n\n\tIt(\"should handle FTSpellCheck with Unstable RESP3 Search Module and without stability\", Label(\"search\", \"ftcreate\", \"ftspellcheck\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"f1\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"f2\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tclient.HSet(ctx, \"doc1\", \"f1\", \"some valid content\", \"f2\", \"this is sample text\")\n\t\tclient.HSet(ctx, \"doc2\", \"f1\", \"very important\", \"f2\", \"lorem ipsum\")\n\n\t\tresSpellCheck, err := client.FTSpellCheck(ctx, \"idx1\", \"impornant\").RawResult()\n\t\tvalSpellCheck := client.FTSpellCheck(ctx, \"idx1\", \"impornant\").RawVal()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(valSpellCheck).To(BeEquivalentTo(resSpellCheck))\n\t\tresults := resSpellCheck.(map[interface{}]interface{})[\"results\"].(map[interface{}]interface{})\n\t\tExpect(results[\"impornant\"].([]interface{})[0].(map[interface{}]interface{})[\"important\"]).To(BeEquivalentTo(0.5))\n\n\t\t// Test with UnstableResp3 false - should return error instead of panic\n\t\trawResSpellCheck, err := client2.FTSpellCheck(ctx, \"idx1\", \"impornant\").RawResult()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"RESP3 responses for this command are disabled\"))\n\t\tExpect(rawResSpellCheck).To(BeNil())\n\n\t\trawValSpellCheck := client2.FTSpellCheck(ctx, \"idx1\", \"impornant\").RawVal()\n\t\tExpect(client2.FTSpellCheck(ctx, \"idx1\", \"impornant\").Err()).To(HaveOccurred())\n\t\tExpect(rawValSpellCheck).To(BeNil())\n\t})\n\n\tIt(\"should handle FTSearch with Unstable RESP3 Search Module and without stability\", Label(\"search\", \"ftcreate\", \"ftsearch\"), func() {\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{StopWords: []interface{}{\"foo\", \"bar\", \"baz\"}}, &redis.FieldSchema{FieldName: \"txt\", FieldType: redis.SearchFieldTypeText}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tclient.HSet(ctx, \"doc1\", \"txt\", \"foo baz\")\n\t\tclient.HSet(ctx, \"doc2\", \"txt\", \"hello world\")\n\t\tres1, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo bar\", &redis.FTSearchOptions{NoContent: true}).RawResult()\n\t\tval1 := client.FTSearchWithArgs(ctx, \"txt\", \"foo bar\", &redis.FTSearchOptions{NoContent: true}).RawVal()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val1).To(BeEquivalentTo(res1))\n\t\ttotalResults := res1.(map[interface{}]interface{})[\"total_results\"]\n\t\tExpect(totalResults).To(BeEquivalentTo(int64(0)))\n\t\tres2, err := client.FTSearchWithArgs(ctx, \"txt\", \"foo bar hello world\", &redis.FTSearchOptions{NoContent: true}).RawResult()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\ttotalResults2 := res2.(map[interface{}]interface{})[\"total_results\"]\n\t\tExpect(totalResults2).To(BeEquivalentTo(int64(1)))\n\n\t\t// Test with UnstableResp3 false - should return error instead of panic\n\t\trawRes2, err := client2.FTSearchWithArgs(ctx, \"txt\", \"foo bar hello world\", &redis.FTSearchOptions{NoContent: true}).RawResult()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"RESP3 responses for this command are disabled\"))\n\t\tExpect(rawRes2).To(BeNil())\n\n\t\trawVal2 := client2.FTSearchWithArgs(ctx, \"txt\", \"foo bar hello world\", &redis.FTSearchOptions{NoContent: true}).RawVal()\n\t\tExpect(client2.FTSearchWithArgs(ctx, \"txt\", \"foo bar hello world\", &redis.FTSearchOptions{NoContent: true}).Err()).To(HaveOccurred())\n\t\tExpect(rawVal2).To(BeNil())\n\t})\n\tIt(\"should handle FTSynDump with Unstable RESP3 Search Module and without stability\", Label(\"search\", \"ftsyndump\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"title\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"body\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"idx1\", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"idx1\")\n\n\t\tresSynUpdate, err := client.FTSynUpdate(ctx, \"idx1\", \"id1\", []interface{}{\"boy\", \"child\", \"offspring\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\n\t\tresSynUpdate, err = client.FTSynUpdate(ctx, \"idx1\", \"id1\", []interface{}{\"baby\", \"child\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\n\t\tresSynUpdate, err = client.FTSynUpdate(ctx, \"idx1\", \"id1\", []interface{}{\"tree\", \"wood\"}).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(resSynUpdate).To(BeEquivalentTo(\"OK\"))\n\n\t\tresSynDump, err := client.FTSynDump(ctx, \"idx1\").RawResult()\n\t\tvalSynDump := client.FTSynDump(ctx, \"idx1\").RawVal()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(valSynDump).To(BeEquivalentTo(resSynDump))\n\t\tExpect(resSynDump.(map[interface{}]interface{})[\"baby\"]).To(BeEquivalentTo([]interface{}{\"id1\"}))\n\n\t\t// Test with UnstableResp3 false - should return error instead of panic\n\t\trawResSynDump, err := client2.FTSynDump(ctx, \"idx1\").RawResult()\n\t\tExpect(err).To(HaveOccurred())\n\t\tExpect(err.Error()).To(ContainSubstring(\"RESP3 responses for this command are disabled\"))\n\t\tExpect(rawResSynDump).To(BeNil())\n\n\t\trawValSynDump := client2.FTSynDump(ctx, \"idx1\").RawVal()\n\t\tExpect(client2.FTSynDump(ctx, \"idx1\").Err()).To(HaveOccurred())\n\t\tExpect(rawValSynDump).To(BeNil())\n\t})\n\n\tIt(\"should test not affected Resp 3 Search method - FTExplain\", Label(\"search\", \"ftexplain\"), func() {\n\t\ttext1 := &redis.FieldSchema{FieldName: \"f1\", FieldType: redis.SearchFieldTypeText}\n\t\ttext2 := &redis.FieldSchema{FieldName: \"f2\", FieldType: redis.SearchFieldTypeText}\n\t\ttext3 := &redis.FieldSchema{FieldName: \"f3\", FieldType: redis.SearchFieldTypeText}\n\t\tval, err := client.FTCreate(ctx, \"txt\", &redis.FTCreateOptions{}, text1, text2, text3).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(BeEquivalentTo(\"OK\"))\n\t\tWaitForIndexing(client, \"txt\")\n\t\tres1, err := client.FTExplain(ctx, \"txt\", \"@f3:f3_val @f2:f2_val @f1:f1_val\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(res1).ToNot(BeEmpty())\n\n\t\t// Test with UnstableResp3 false\n\t\tExpect(func() {\n\t\t\tres2, err := client2.FTExplain(ctx, \"txt\", \"@f3:f3_val @f2:f2_val @f1:f1_val\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res2).ToNot(BeEmpty())\n\t\t}).ShouldNot(Panic())\n\t})\n})\n"
  },
  {
    "path": "sentinel.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/rand\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n//------------------------------------------------------------------------------\n\n// FailoverOptions are used to configure a failover client and should\n// be passed to NewFailoverClient.\ntype FailoverOptions struct {\n\t// The master name.\n\tMasterName string\n\t// A seed list of host:port addresses of sentinel nodes.\n\tSentinelAddrs []string\n\n\t// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.\n\tClientName string\n\n\t// If specified with SentinelPassword, enables ACL-based authentication (via\n\t// AUTH <user> <pass>).\n\tSentinelUsername string\n\t// Sentinel password from \"requirepass <password>\" (if enabled) in Sentinel\n\t// configuration, or, if SentinelUsername is also supplied, used for ACL-based\n\t// authentication.\n\tSentinelPassword string\n\n\t// Allows routing read-only commands to the closest master or replica node.\n\t// This option only works with NewFailoverClusterClient.\n\tRouteByLatency bool\n\t// Allows routing read-only commands to the random master or replica node.\n\t// This option only works with NewFailoverClusterClient.\n\tRouteRandomly bool\n\n\t// Route all commands to replica read-only nodes.\n\tReplicaOnly bool\n\n\t// Use replicas disconnected with master when cannot get connected replicas\n\t// Now, this option only works in RandomReplicaAddr function.\n\tUseDisconnectedReplicas bool\n\n\t// Following options are copied from Options struct.\n\n\tDialer    func(ctx context.Context, network, addr string) (net.Conn, error)\n\tOnConnect func(ctx context.Context, cn *Conn) error\n\n\tProtocol int\n\tUsername string\n\tPassword string\n\n\t// Push notifications are always enabled for RESP3 connections\n\t// CredentialsProvider allows the username and password to be updated\n\t// before reconnecting. It should return the current username and password.\n\tCredentialsProvider func() (username string, password string)\n\n\t// CredentialsProviderContext is an enhanced parameter of CredentialsProvider,\n\t// done to maintain API compatibility. In the future,\n\t// there might be a merge between CredentialsProviderContext and CredentialsProvider.\n\t// There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider.\n\tCredentialsProviderContext func(ctx context.Context) (username string, password string, err error)\n\n\t// StreamingCredentialsProvider is used to retrieve the credentials\n\t// for the connection from an external source. Those credentials may change\n\t// during the connection lifetime. This is useful for managed identity\n\t// scenarios where the credentials are retrieved from an external source.\n\t//\n\t// Currently, this is a placeholder for the future implementation.\n\tStreamingCredentialsProvider auth.StreamingCredentialsProvider\n\tDB                           int\n\n\tMaxRetries      int\n\tMinRetryBackoff time.Duration\n\tMaxRetryBackoff time.Duration\n\n\tDialTimeout time.Duration\n\n\t// DialerRetries is the maximum number of retry attempts when dialing fails.\n\t//\n\t// default: 5\n\tDialerRetries int\n\n\t// DialerRetryTimeout is the backoff duration between retry attempts.\n\t//\n\t// default: 100 milliseconds\n\tDialerRetryTimeout time.Duration\n\n\t// DialerRetryBackoff controls the delay between dial retry attempts.\n\t// See Options.DialerRetryBackoff for details.\n\tDialerRetryBackoff func(attempt int) time.Duration\n\n\tReadTimeout           time.Duration\n\tWriteTimeout          time.Duration\n\tContextTimeoutEnabled bool\n\n\t// ReadBufferSize is the size of the bufio.Reader buffer for each connection.\n\t// Larger buffers can improve performance for commands that return large responses.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tReadBufferSize int\n\n\t// WriteBufferSize is the size of the bufio.Writer buffer for each connection.\n\t// Larger buffers can improve performance for large pipelines and commands with many arguments.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tWriteBufferSize int\n\n\tPoolFIFO bool\n\n\tPoolSize int\n\n\t// MaxConcurrentDials is the maximum number of concurrent connection creation goroutines.\n\t// If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize.\n\tMaxConcurrentDials int\n\n\tPoolTimeout           time.Duration\n\tMinIdleConns          int\n\tMaxIdleConns          int\n\tMaxActiveConns        int\n\tConnMaxIdleTime       time.Duration\n\tConnMaxLifetime       time.Duration\n\tConnMaxLifetimeJitter time.Duration\n\n\tTLSConfig *tls.Config\n\n\t// DisableIndentity - Disable set-lib on connect.\n\t//\n\t// default: false\n\t//\n\t// Deprecated: Use DisableIdentity instead.\n\tDisableIndentity bool\n\n\t// DisableIdentity is used to disable CLIENT SETINFO command on connect.\n\t//\n\t// default: false\n\tDisableIdentity bool\n\n\tIdentitySuffix string\n\n\t// FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing.\n\t// When a node is marked as failing, it will be avoided for this duration.\n\t// Only applies to failover cluster clients. Default is 15 seconds.\n\tFailingTimeoutSeconds int\n\n\tUnstableResp3 bool\n\n\t// PushNotificationProcessor is the processor for handling push notifications.\n\t// If nil, a default processor will be created for RESP3 connections.\n\tPushNotificationProcessor push.NotificationProcessor\n\n\t// MaintNotificationsConfig is not supported for FailoverClients at the moment\n\t// MaintNotificationsConfig provides custom configuration for maintnotifications upgrades.\n\t// When MaintNotificationsConfig.Mode is not \"disabled\", the client will handle\n\t// upgrade notifications gracefully and manage connection/pool state transitions\n\t// seamlessly. Requires Protocol: 3 (RESP3) for push notifications.\n\t// If nil, maintnotifications upgrades are disabled.\n\t// (however if Mode is nil, it defaults to \"auto\" - enable if server supports it)\n\t//MaintNotificationsConfig *maintnotifications.Config\n}\n\nfunc (opt *FailoverOptions) clientOptions() *Options {\n\treturn &Options{\n\t\tAddr:       \"FailoverClient\",\n\t\tClientName: opt.ClientName,\n\n\t\tDialer:    opt.Dialer,\n\t\tOnConnect: opt.OnConnect,\n\n\t\tDB:                           opt.DB,\n\t\tProtocol:                     opt.Protocol,\n\t\tUsername:                     opt.Username,\n\t\tPassword:                     opt.Password,\n\t\tCredentialsProvider:          opt.CredentialsProvider,\n\t\tCredentialsProviderContext:   opt.CredentialsProviderContext,\n\t\tStreamingCredentialsProvider: opt.StreamingCredentialsProvider,\n\n\t\tMaxRetries:      opt.MaxRetries,\n\t\tMinRetryBackoff: opt.MinRetryBackoff,\n\t\tMaxRetryBackoff: opt.MaxRetryBackoff,\n\n\t\tReadBufferSize:  opt.ReadBufferSize,\n\t\tWriteBufferSize: opt.WriteBufferSize,\n\n\t\tDialTimeout:        opt.DialTimeout,\n\t\tDialerRetries:      opt.DialerRetries,\n\t\tDialerRetryTimeout: opt.DialerRetryTimeout,\n\t\tDialerRetryBackoff: opt.DialerRetryBackoff,\n\t\tReadTimeout:        opt.ReadTimeout,\n\t\tWriteTimeout:       opt.WriteTimeout,\n\n\t\tContextTimeoutEnabled: opt.ContextTimeoutEnabled,\n\n\t\tPoolFIFO:              opt.PoolFIFO,\n\t\tPoolSize:              opt.PoolSize,\n\t\tMaxConcurrentDials:    opt.MaxConcurrentDials,\n\t\tPoolTimeout:           opt.PoolTimeout,\n\t\tMinIdleConns:          opt.MinIdleConns,\n\t\tMaxIdleConns:          opt.MaxIdleConns,\n\t\tMaxActiveConns:        opt.MaxActiveConns,\n\t\tConnMaxIdleTime:       opt.ConnMaxIdleTime,\n\t\tConnMaxLifetime:       opt.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,\n\n\t\tTLSConfig: opt.TLSConfig,\n\n\t\tDisableIdentity:  opt.DisableIdentity,\n\t\tDisableIndentity: opt.DisableIndentity,\n\n\t\tIdentitySuffix:            opt.IdentitySuffix,\n\t\tUnstableResp3:             opt.UnstableResp3,\n\t\tPushNotificationProcessor: opt.PushNotificationProcessor,\n\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeDisabled,\n\t\t},\n\t}\n}\n\nfunc (opt *FailoverOptions) sentinelOptions(addr string) *Options {\n\treturn &Options{\n\t\tAddr:       addr,\n\t\tClientName: opt.ClientName,\n\n\t\tDialer:    opt.Dialer,\n\t\tOnConnect: opt.OnConnect,\n\n\t\tDB:       0,\n\t\tUsername: opt.SentinelUsername,\n\t\tPassword: opt.SentinelPassword,\n\n\t\tMaxRetries:      opt.MaxRetries,\n\t\tMinRetryBackoff: opt.MinRetryBackoff,\n\t\tMaxRetryBackoff: opt.MaxRetryBackoff,\n\n\t\t// The sentinel client uses a 4KiB read/write buffer size.\n\t\tReadBufferSize:  4096,\n\t\tWriteBufferSize: 4096,\n\n\t\tDialTimeout:        opt.DialTimeout,\n\t\tDialerRetries:      opt.DialerRetries,\n\t\tDialerRetryTimeout: opt.DialerRetryTimeout,\n\t\tDialerRetryBackoff: opt.DialerRetryBackoff,\n\t\tReadTimeout:        opt.ReadTimeout,\n\t\tWriteTimeout:       opt.WriteTimeout,\n\n\t\tContextTimeoutEnabled: opt.ContextTimeoutEnabled,\n\n\t\tPoolFIFO:              opt.PoolFIFO,\n\t\tPoolSize:              opt.PoolSize,\n\t\tMaxConcurrentDials:    opt.MaxConcurrentDials,\n\t\tPoolTimeout:           opt.PoolTimeout,\n\t\tMinIdleConns:          opt.MinIdleConns,\n\t\tMaxIdleConns:          opt.MaxIdleConns,\n\t\tMaxActiveConns:        opt.MaxActiveConns,\n\t\tConnMaxIdleTime:       opt.ConnMaxIdleTime,\n\t\tConnMaxLifetime:       opt.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,\n\n\t\tTLSConfig: opt.TLSConfig,\n\n\t\tDisableIdentity:  opt.DisableIdentity,\n\t\tDisableIndentity: opt.DisableIndentity,\n\n\t\tIdentitySuffix:            opt.IdentitySuffix,\n\t\tUnstableResp3:             opt.UnstableResp3,\n\t\tPushNotificationProcessor: opt.PushNotificationProcessor,\n\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeDisabled,\n\t\t},\n\t}\n}\n\nfunc (opt *FailoverOptions) clusterOptions() *ClusterOptions {\n\treturn &ClusterOptions{\n\t\tClientName: opt.ClientName,\n\n\t\tDialer:    opt.Dialer,\n\t\tOnConnect: opt.OnConnect,\n\n\t\tProtocol:                     opt.Protocol,\n\t\tUsername:                     opt.Username,\n\t\tPassword:                     opt.Password,\n\t\tCredentialsProvider:          opt.CredentialsProvider,\n\t\tCredentialsProviderContext:   opt.CredentialsProviderContext,\n\t\tStreamingCredentialsProvider: opt.StreamingCredentialsProvider,\n\n\t\tMaxRedirects: opt.MaxRetries,\n\n\t\tReadOnly:       opt.ReplicaOnly,\n\t\tRouteByLatency: opt.RouteByLatency,\n\t\tRouteRandomly:  opt.RouteRandomly,\n\n\t\tMinRetryBackoff: opt.MinRetryBackoff,\n\t\tMaxRetryBackoff: opt.MaxRetryBackoff,\n\n\t\tReadBufferSize:  opt.ReadBufferSize,\n\t\tWriteBufferSize: opt.WriteBufferSize,\n\n\t\tDialTimeout:        opt.DialTimeout,\n\t\tDialerRetries:      opt.DialerRetries,\n\t\tDialerRetryTimeout: opt.DialerRetryTimeout,\n\t\tDialerRetryBackoff: opt.DialerRetryBackoff,\n\t\tReadTimeout:        opt.ReadTimeout,\n\t\tWriteTimeout:       opt.WriteTimeout,\n\n\t\tContextTimeoutEnabled: opt.ContextTimeoutEnabled,\n\n\t\tPoolFIFO:           opt.PoolFIFO,\n\t\tPoolSize:           opt.PoolSize,\n\t\tMaxConcurrentDials: opt.MaxConcurrentDials,\n\t\tPoolTimeout:        opt.PoolTimeout,\n\t\tMinIdleConns:       opt.MinIdleConns,\n\t\tMaxIdleConns:       opt.MaxIdleConns,\n\t\tMaxActiveConns:     opt.MaxActiveConns,\n\t\tConnMaxIdleTime:    opt.ConnMaxIdleTime,\n\t\tConnMaxLifetime:    opt.ConnMaxLifetime,\n\n\t\tTLSConfig: opt.TLSConfig,\n\n\t\tDisableIdentity:           opt.DisableIdentity,\n\t\tDisableIndentity:          opt.DisableIndentity,\n\t\tIdentitySuffix:            opt.IdentitySuffix,\n\t\tFailingTimeoutSeconds:     opt.FailingTimeoutSeconds,\n\t\tPushNotificationProcessor: opt.PushNotificationProcessor,\n\n\t\tMaintNotificationsConfig: &maintnotifications.Config{\n\t\t\tMode: maintnotifications.ModeDisabled,\n\t\t},\n\t}\n}\n\n// ParseFailoverURL parses a URL into FailoverOptions that can be used to connect to Redis.\n// The URL must be in the form:\n//\n//\tredis://<user>:<password>@<host>:<port>/<db_number>\n//\tor\n//\trediss://<user>:<password>@<host>:<port>/<db_number>\n//\n// To add additional addresses, specify the query parameter, \"addr\" one or more times. e.g:\n//\n//\tredis://<user>:<password>@<host>:<port>/<db_number>?addr=<host2>:<port2>&addr=<host3>:<port3>\n//\tor\n//\trediss://<user>:<password>@<host>:<port>/<db_number>?addr=<host2>:<port2>&addr=<host3>:<port3>\n//\n// Most Option fields can be set using query parameters, with the following restrictions:\n//   - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries\n//   - only scalar type fields are supported (bool, int, time.Duration)\n//   - for time.Duration fields, values must be a valid input for time.ParseDuration();\n//     additionally a plain integer as value (i.e. without unit) is interpreted as seconds\n//   - to disable a duration field, use value less than or equal to 0; to use the default\n//     value, leave the value blank or remove the parameter\n//   - only the last value is interpreted if a parameter is given multiple times\n//   - fields \"network\", \"addr\", \"sentinel_username\" and \"sentinel_password\" can only be set using other\n//     URL attributes (scheme, host, userinfo, resp.), query parameters using these\n//     names will be treated as unknown parameters\n//   - unknown parameter names will result in an error\n//   - use \"skip_verify=true\" to ignore TLS certificate validation\n//\n// Example:\n//\n//\tredis://user:password@localhost:6789?master_name=mymaster&dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791\n//\tis equivalent to:\n//\t&FailoverOptions{\n//\t\tMasterName:  \"mymaster\",\n//\t\tAddr:        [\"localhost:6789\", \"localhost:6790\", \"localhost:6791\"]\n//\t\tDialTimeout: 3 * time.Second, // no time unit = seconds\n//\t\tReadTimeout: 6 * time.Second,\n//\t}\nfunc ParseFailoverURL(redisURL string) (*FailoverOptions, error) {\n\tu, err := url.Parse(redisURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn setupFailoverConn(u)\n}\n\nfunc setupFailoverConn(u *url.URL) (*FailoverOptions, error) {\n\to := &FailoverOptions{}\n\n\to.SentinelUsername, o.SentinelPassword = getUserPassword(u)\n\n\th, p := getHostPortWithDefaults(u)\n\to.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p))\n\n\tswitch u.Scheme {\n\tcase \"rediss\":\n\t\to.TLSConfig = &tls.Config{ServerName: h, MinVersion: tls.VersionTLS12}\n\tcase \"redis\":\n\t\to.TLSConfig = nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: invalid URL scheme: %s\", u.Scheme)\n\t}\n\n\tf := strings.FieldsFunc(u.Path, func(r rune) bool {\n\t\treturn r == '/'\n\t})\n\tswitch len(f) {\n\tcase 0:\n\t\to.DB = 0\n\tcase 1:\n\t\tvar err error\n\t\tif o.DB, err = strconv.Atoi(f[0]); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"redis: invalid database number: %q\", f[0])\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: invalid URL path: %s\", u.Path)\n\t}\n\n\treturn setupFailoverConnParams(u, o)\n}\n\nfunc setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, error) {\n\tq := queryOptions{q: u.Query()}\n\n\to.MasterName = q.string(\"master_name\")\n\to.ClientName = q.string(\"client_name\")\n\to.RouteByLatency = q.bool(\"route_by_latency\")\n\to.RouteRandomly = q.bool(\"route_randomly\")\n\to.ReplicaOnly = q.bool(\"replica_only\")\n\to.UseDisconnectedReplicas = q.bool(\"use_disconnected_replicas\")\n\to.Protocol = q.int(\"protocol\")\n\to.Username = q.string(\"username\")\n\to.Password = q.string(\"password\")\n\to.MaxRetries = q.int(\"max_retries\")\n\to.MinRetryBackoff = q.duration(\"min_retry_backoff\")\n\to.MaxRetryBackoff = q.duration(\"max_retry_backoff\")\n\to.DialTimeout = q.duration(\"dial_timeout\")\n\to.DialerRetries = q.int(\"dialer_retries\")\n\to.DialerRetryTimeout = q.duration(\"dialer_retry_timeout\")\n\to.ReadTimeout = q.duration(\"read_timeout\")\n\to.WriteTimeout = q.duration(\"write_timeout\")\n\to.ContextTimeoutEnabled = q.bool(\"context_timeout_enabled\")\n\to.PoolFIFO = q.bool(\"pool_fifo\")\n\to.PoolSize = q.int(\"pool_size\")\n\to.MaxConcurrentDials = q.int(\"max_concurrent_dials\")\n\to.MinIdleConns = q.int(\"min_idle_conns\")\n\to.MaxIdleConns = q.int(\"max_idle_conns\")\n\to.MaxActiveConns = q.int(\"max_active_conns\")\n\to.ConnMaxLifetime = q.duration(\"conn_max_lifetime\")\n\tif q.has(\"conn_max_lifetime_jitter\") {\n\t\to.ConnMaxLifetimeJitter = min(q.duration(\"conn_max_lifetime_jitter\"), o.ConnMaxLifetime)\n\t}\n\to.ConnMaxIdleTime = q.duration(\"conn_max_idle_time\")\n\to.PoolTimeout = q.duration(\"pool_timeout\")\n\to.DisableIdentity = q.bool(\"disableIdentity\")\n\to.IdentitySuffix = q.string(\"identitySuffix\")\n\to.UnstableResp3 = q.bool(\"unstable_resp3\")\n\n\tif q.err != nil {\n\t\treturn nil, q.err\n\t}\n\n\tif tmp := q.string(\"db\"); tmp != \"\" {\n\t\tdb, err := strconv.Atoi(tmp)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"redis: invalid database number: %w\", err)\n\t\t}\n\t\to.DB = db\n\t}\n\n\taddrs := q.strings(\"addr\")\n\tfor _, addr := range addrs {\n\t\th, p, err := net.SplitHostPort(addr)\n\t\tif err != nil || h == \"\" || p == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"redis: unable to parse addr param: %s\", addr)\n\t\t}\n\n\t\to.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p))\n\t}\n\n\tif o.TLSConfig != nil && q.has(\"skip_verify\") {\n\t\to.TLSConfig.InsecureSkipVerify = q.bool(\"skip_verify\")\n\t}\n\n\t// any parameters left?\n\tif r := q.remaining(); len(r) > 0 {\n\t\treturn nil, fmt.Errorf(\"redis: unexpected option: %s\", strings.Join(r, \", \"))\n\t}\n\n\treturn o, nil\n}\n\n// NewFailoverClient returns a Redis client that uses Redis Sentinel\n// for automatic failover. It's safe for concurrent use by multiple\n// goroutines.\nfunc NewFailoverClient(failoverOpt *FailoverOptions) *Client {\n\tif failoverOpt == nil {\n\t\tpanic(\"redis: NewFailoverClient nil options\")\n\t}\n\n\tif failoverOpt.RouteByLatency {\n\t\tpanic(\"to route commands by latency, use NewFailoverClusterClient\")\n\t}\n\tif failoverOpt.RouteRandomly {\n\t\tpanic(\"to route commands randomly, use NewFailoverClusterClient\")\n\t}\n\n\tsentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))\n\tcopy(sentinelAddrs, failoverOpt.SentinelAddrs)\n\n\trand.Shuffle(len(sentinelAddrs), func(i, j int) {\n\t\tsentinelAddrs[i], sentinelAddrs[j] = sentinelAddrs[j], sentinelAddrs[i]\n\t})\n\n\tfailover := &sentinelFailover{\n\t\topt:           failoverOpt,\n\t\tsentinelAddrs: sentinelAddrs,\n\t}\n\n\topt := failoverOpt.clientOptions()\n\topt.Dialer = masterReplicaDialer(failover)\n\topt.init()\n\n\trdb := &Client{\n\t\tbaseClient: &baseClient{\n\t\t\topt: opt,\n\t\t},\n\t}\n\trdb.init()\n\n\t// Initialize push notification processor using shared helper\n\t// Use void processor by default for RESP2 connections\n\trdb.pushProcessor = initializePushProcessor(opt)\n\n\t// Generate unique pool names for metrics\n\tuniqueID := generateUniqueID()\n\tmainPoolName := opt.Addr + \"_\" + uniqueID\n\tpubsubPoolName := opt.Addr + \"_\" + uniqueID + \"_pubsub\"\n\n\tvar err error\n\trdb.connPool, err = newConnPool(opt, rdb.dialHook, mainPoolName)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"redis: failed to create connection pool: %w\", err))\n\t}\n\trdb.pubSubPool, err = newPubSubPool(opt, rdb.dialHook, pubsubPoolName)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"redis: failed to create pubsub pool: %w\", err))\n\t}\n\n\trdb.onClose = rdb.wrappedOnClose(failover.Close)\n\n\tfailover.mu.Lock()\n\tfailover.onFailover = func(ctx context.Context, addr string) {\n\t\tif connPool, ok := rdb.connPool.(*pool.ConnPool); ok {\n\t\t\t_ = connPool.Filter(func(cn *pool.Conn) bool {\n\t\t\t\treturn cn.RemoteAddr().String() != addr\n\t\t\t})\n\t\t}\n\t}\n\tfailover.mu.Unlock()\n\n\treturn rdb\n}\n\nfunc masterReplicaDialer(\n\tfailover *sentinelFailover,\n) func(ctx context.Context, network, addr string) (net.Conn, error) {\n\treturn func(ctx context.Context, network, _ string) (net.Conn, error) {\n\t\tvar addr string\n\t\tvar err error\n\n\t\tif failover.opt.ReplicaOnly {\n\t\t\taddr, err = failover.RandomReplicaAddr(ctx)\n\t\t} else {\n\t\t\taddr, err = failover.MasterAddr(ctx)\n\t\t\tif err == nil {\n\t\t\t\tfailover.trySwitchMaster(ctx, addr)\n\t\t\t}\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif failover.opt.Dialer != nil {\n\t\t\treturn failover.opt.Dialer(ctx, network, addr)\n\t\t}\n\n\t\tnetDialer := &net.Dialer{\n\t\t\tTimeout:   failover.opt.DialTimeout,\n\t\t\tKeepAlive: 5 * time.Minute,\n\t\t}\n\t\tif failover.opt.TLSConfig == nil {\n\t\t\treturn netDialer.DialContext(ctx, network, addr)\n\t\t}\n\t\treturn tls.DialWithDialer(netDialer, network, addr, failover.opt.TLSConfig)\n\t}\n}\n\n//------------------------------------------------------------------------------\n\n// SentinelClient is a client for a Redis Sentinel.\ntype SentinelClient struct {\n\t*baseClient\n}\n\nfunc NewSentinelClient(opt *Options) *SentinelClient {\n\tif opt == nil {\n\t\tpanic(\"redis: NewSentinelClient nil options\")\n\t}\n\topt.init()\n\tc := &SentinelClient{\n\t\tbaseClient: &baseClient{\n\t\t\topt: opt,\n\t\t},\n\t}\n\n\t// Initialize push notification processor using shared helper\n\t// Use void processor for Sentinel clients\n\tc.pushProcessor = NewVoidPushNotificationProcessor()\n\n\tc.initHooks(hooks{\n\t\tdial:    c.baseClient.dial,\n\t\tprocess: c.baseClient.process,\n\t})\n\n\t// Generate unique pool names for metrics\n\tuniqueID := generateUniqueID()\n\tmainPoolName := opt.Addr + \"_\" + uniqueID\n\tpubsubPoolName := opt.Addr + \"_\" + uniqueID + \"_pubsub\"\n\n\tvar err error\n\tc.connPool, err = newConnPool(opt, c.dialHook, mainPoolName)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"redis: failed to create connection pool: %w\", err))\n\t}\n\tc.pubSubPool, err = newPubSubPool(opt, c.dialHook, pubsubPoolName)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"redis: failed to create pubsub pool: %w\", err))\n\t}\n\n\treturn c\n}\n\n// GetPushNotificationHandler returns the handler for a specific push notification name.\n// Returns nil if no handler is registered for the given name.\nfunc (c *SentinelClient) GetPushNotificationHandler(pushNotificationName string) push.NotificationHandler {\n\treturn c.pushProcessor.GetHandler(pushNotificationName)\n}\n\n// RegisterPushNotificationHandler registers a handler for a specific push notification name.\n// Returns an error if a handler is already registered for this push notification name.\n// If protected is true, the handler cannot be unregistered.\nfunc (c *SentinelClient) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error {\n\treturn c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected)\n}\n\nfunc (c *SentinelClient) Process(ctx context.Context, cmd Cmder) error {\n\terr := c.processHook(ctx, cmd)\n\tcmd.SetErr(err)\n\treturn err\n}\n\nfunc (c *SentinelClient) pubSub() *PubSub {\n\tpubsub := &PubSub{\n\t\topt: c.opt,\n\t\tnewConn: func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) {\n\t\t\tcn, err := c.pubSubPool.NewConn(ctx, c.opt.Network, addr, channels)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// will return nil if already initialized\n\t\t\terr = c.initConn(ctx, cn)\n\t\t\tif err != nil {\n\t\t\t\t_ = cn.Close()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// Track connection in PubSubPool\n\t\t\tc.pubSubPool.TrackConn(cn)\n\t\t\treturn cn, nil\n\t\t},\n\t\tcloseConn: func(cn *pool.Conn) error {\n\t\t\t// Untrack connection from PubSubPool\n\t\t\tc.pubSubPool.UntrackConn(cn)\n\t\t\t_ = cn.Close()\n\t\t\treturn nil\n\t\t},\n\t\tpushProcessor: c.pushProcessor,\n\t}\n\tpubsub.init()\n\n\treturn pubsub\n}\n\n// Ping is used to test if a connection is still alive, or to\n// measure latency.\nfunc (c *SentinelClient) Ping(ctx context.Context) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"ping\")\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Subscribe subscribes the client to the specified channels.\n// Channels can be omitted to create empty subscription.\nfunc (c *SentinelClient) Subscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.Subscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\n// PSubscribe subscribes the client to the given patterns.\n// Patterns can be omitted to create empty subscription.\nfunc (c *SentinelClient) PSubscribe(ctx context.Context, channels ...string) *PubSub {\n\tpubsub := c.pubSub()\n\tif len(channels) > 0 {\n\t\t_ = pubsub.PSubscribe(ctx, channels...)\n\t}\n\treturn pubsub\n}\n\nfunc (c *SentinelClient) GetMasterAddrByName(ctx context.Context, name string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"sentinel\", \"get-master-addr-by-name\", name)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c *SentinelClient) Sentinels(ctx context.Context, name string) *MapStringStringSliceCmd {\n\tcmd := NewMapStringStringSliceCmd(ctx, \"sentinel\", \"sentinels\", name)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Failover forces a failover as if the master was not reachable, and without\n// asking for agreement to other Sentinels.\nfunc (c *SentinelClient) Failover(ctx context.Context, name string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"sentinel\", \"failover\", name)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Reset resets all the masters with matching name. The pattern argument is a\n// glob-style pattern. The reset process clears any previous state in a master\n// (including a failover in progress), and removes every replica and sentinel\n// already discovered and associated with the master.\nfunc (c *SentinelClient) Reset(ctx context.Context, pattern string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"sentinel\", \"reset\", pattern)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// FlushConfig forces Sentinel to rewrite its configuration on disk, including\n// the current Sentinel state.\nfunc (c *SentinelClient) FlushConfig(ctx context.Context) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"sentinel\", \"flushconfig\")\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Master shows the state and info of the specified master.\nfunc (c *SentinelClient) Master(ctx context.Context, name string) *MapStringStringCmd {\n\tcmd := NewMapStringStringCmd(ctx, \"sentinel\", \"master\", name)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Masters shows a list of monitored masters and their state.\nfunc (c *SentinelClient) Masters(ctx context.Context) *SliceCmd {\n\tcmd := NewSliceCmd(ctx, \"sentinel\", \"masters\")\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Replicas shows a list of replicas for the specified master and their state.\nfunc (c *SentinelClient) Replicas(ctx context.Context, name string) *MapStringStringSliceCmd {\n\tcmd := NewMapStringStringSliceCmd(ctx, \"sentinel\", \"replicas\", name)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// CkQuorum checks if the current Sentinel configuration is able to reach the\n// quorum needed to failover a master, and the majority needed to authorize the\n// failover. This command should be used in monitoring systems to check if a\n// Sentinel deployment is ok.\nfunc (c *SentinelClient) CkQuorum(ctx context.Context, name string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"sentinel\", \"ckquorum\", name)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Monitor tells the Sentinel to start monitoring a new master with the specified\n// name, ip, port, and quorum.\nfunc (c *SentinelClient) Monitor(ctx context.Context, name, ip, port, quorum string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"sentinel\", \"monitor\", name, ip, port, quorum)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Set is used in order to change configuration parameters of a specific master.\nfunc (c *SentinelClient) Set(ctx context.Context, name, option, value string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"sentinel\", \"set\", name, option, value)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Remove is used in order to remove the specified master: the master will no\n// longer be monitored, and will totally be removed from the internal state of\n// the Sentinel.\nfunc (c *SentinelClient) Remove(ctx context.Context, name string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"sentinel\", \"remove\", name)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n//------------------------------------------------------------------------------\n\ntype sentinelFailover struct {\n\topt *FailoverOptions\n\n\tsentinelAddrs []string\n\n\tonFailover func(ctx context.Context, addr string)\n\tonUpdate   func(ctx context.Context)\n\n\tmu         sync.RWMutex\n\tmasterAddr string\n\tsentinel   *SentinelClient\n\tpubsub     *PubSub\n}\n\nfunc (c *sentinelFailover) Close() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif c.sentinel != nil {\n\t\treturn c.closeSentinel()\n\t}\n\treturn nil\n}\n\nfunc (c *sentinelFailover) closeSentinel() error {\n\tfirstErr := c.pubsub.Close()\n\tc.pubsub = nil\n\n\terr := c.sentinel.Close()\n\tif err != nil && firstErr == nil {\n\t\tfirstErr = err\n\t}\n\tc.sentinel = nil\n\n\treturn firstErr\n}\n\nfunc (c *sentinelFailover) RandomReplicaAddr(ctx context.Context) (string, error) {\n\tif c.opt == nil {\n\t\treturn \"\", errors.New(\"opt is nil\")\n\t}\n\n\taddresses, err := c.replicaAddrs(ctx, false)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(addresses) == 0 && c.opt.UseDisconnectedReplicas {\n\t\taddresses, err = c.replicaAddrs(ctx, true)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif len(addresses) == 0 {\n\t\treturn c.MasterAddr(ctx)\n\t}\n\treturn addresses[rand.Intn(len(addresses))], nil\n}\n\nfunc (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {\n\tc.mu.RLock()\n\tsentinel := c.sentinel\n\tc.mu.RUnlock()\n\n\tif sentinel != nil {\n\t\taddr, err := c.getMasterAddr(ctx, sentinel)\n\t\tif err != nil {\n\t\t\tif isContextError(ctx.Err()) {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\t// Continue on other errors\n\t\t\tinternal.Logger.Printf(ctx, \"sentinel: GetMasterAddrByName name=%q failed: %s\",\n\t\t\t\tc.opt.MasterName, err)\n\t\t} else {\n\t\t\treturn addr, nil\n\t\t}\n\t}\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.sentinel != nil {\n\t\taddr, err := c.getMasterAddr(ctx, c.sentinel)\n\t\tif err != nil {\n\t\t\t_ = c.closeSentinel()\n\t\t\tif isContextError(ctx.Err()) {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\t// Continue on other errors\n\t\t\tinternal.Logger.Printf(ctx, \"sentinel: GetMasterAddrByName name=%q failed: %s\",\n\t\t\t\tc.opt.MasterName, err)\n\t\t} else {\n\t\t\treturn addr, nil\n\t\t}\n\t}\n\n\t// short circuit if no sentinels configured\n\tif len(c.sentinelAddrs) == 0 {\n\t\treturn \"\", errors.New(\"redis: no sentinels configured\")\n\t}\n\n\tvar (\n\t\tmasterAddr string\n\t\twg         sync.WaitGroup\n\t\tonce       sync.Once\n\t\terrCh      = make(chan error, len(c.sentinelAddrs))\n\t)\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tfor i, sentinelAddr := range c.sentinelAddrs {\n\t\twg.Add(1)\n\t\tgo func(i int, addr string) {\n\t\t\tdefer wg.Done()\n\t\t\tsentinelCli := NewSentinelClient(c.opt.sentinelOptions(addr))\n\t\t\taddrVal, err := sentinelCli.GetMasterAddrByName(ctx, c.opt.MasterName).Result()\n\t\t\tif err != nil {\n\t\t\t\tinternal.Logger.Printf(ctx, \"sentinel: GetMasterAddrByName addr=%s, master=%q failed: %s\",\n\t\t\t\t\taddr, c.opt.MasterName, err)\n\t\t\t\t_ = sentinelCli.Close()\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tonce.Do(func() {\n\t\t\t\tmasterAddr = net.JoinHostPort(addrVal[0], addrVal[1])\n\t\t\t\t// Push working sentinel to the top\n\t\t\t\tc.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]\n\t\t\t\tc.setSentinel(ctx, sentinelCli)\n\t\t\t\tinternal.Logger.Printf(ctx, \"sentinel: selected addr=%s masterAddr=%s\", addr, masterAddr)\n\t\t\t\tcancel()\n\t\t\t})\n\t\t}(i, sentinelAddr)\n\t}\n\n\twg.Wait()\n\tclose(errCh)\n\tif masterAddr != \"\" {\n\t\treturn masterAddr, nil\n\t}\n\terrs := make([]error, 0, len(errCh))\n\tfor err := range errCh {\n\t\terrs = append(errs, err)\n\t}\n\treturn \"\", fmt.Errorf(\"redis: all sentinels specified in configuration are unreachable: %w\", errors.Join(errs...))\n}\n\nfunc (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {\n\tc.mu.RLock()\n\tsentinel := c.sentinel\n\tc.mu.RUnlock()\n\n\tif sentinel != nil {\n\t\taddrs, err := c.getReplicaAddrs(ctx, sentinel)\n\t\tif err != nil {\n\t\t\tif isContextError(ctx.Err()) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// Continue on other errors\n\t\t\tinternal.Logger.Printf(ctx, \"sentinel: Replicas name=%q failed: %s\",\n\t\t\t\tc.opt.MasterName, err)\n\t\t} else if len(addrs) > 0 {\n\t\t\treturn addrs, nil\n\t\t}\n\t}\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.sentinel != nil {\n\t\taddrs, err := c.getReplicaAddrs(ctx, c.sentinel)\n\t\tif err != nil {\n\t\t\t_ = c.closeSentinel()\n\t\t\tif isContextError(ctx.Err()) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// Continue on other errors\n\t\t\tinternal.Logger.Printf(ctx, \"sentinel: Replicas name=%q failed: %s\",\n\t\t\t\tc.opt.MasterName, err)\n\t\t} else if len(addrs) > 0 {\n\t\t\treturn addrs, nil\n\t\t} else {\n\t\t\t// No error and no replicas.\n\t\t\t_ = c.closeSentinel()\n\t\t}\n\t}\n\n\tvar sentinelReachable bool\n\n\tfor i, sentinelAddr := range c.sentinelAddrs {\n\t\tsentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))\n\n\t\treplicas, err := sentinel.Replicas(ctx, c.opt.MasterName).Result()\n\t\tif err != nil {\n\t\t\t_ = sentinel.Close()\n\t\t\tif isContextError(ctx.Err()) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tinternal.Logger.Printf(ctx, \"sentinel: Replicas master=%q failed: %s\",\n\t\t\t\tc.opt.MasterName, err)\n\t\t\tcontinue\n\t\t}\n\t\tsentinelReachable = true\n\t\taddrs := parseReplicaAddrs(replicas, useDisconnected)\n\t\tif len(addrs) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\t// Push working sentinel to the top.\n\t\tc.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]\n\t\tc.setSentinel(ctx, sentinel)\n\n\t\treturn addrs, nil\n\t}\n\n\tif sentinelReachable {\n\t\treturn []string{}, nil\n\t}\n\treturn []string{}, errors.New(\"redis: all sentinels specified in configuration are unreachable\")\n}\n\nfunc (c *sentinelFailover) getMasterAddr(ctx context.Context, sentinel *SentinelClient) (string, error) {\n\taddr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn net.JoinHostPort(addr[0], addr[1]), nil\n}\n\nfunc (c *sentinelFailover) getReplicaAddrs(ctx context.Context, sentinel *SentinelClient) ([]string, error) {\n\taddrs, err := sentinel.Replicas(ctx, c.opt.MasterName).Result()\n\tif err != nil {\n\t\tinternal.Logger.Printf(ctx, \"sentinel: Replicas name=%q failed: %s\",\n\t\t\tc.opt.MasterName, err)\n\t\treturn nil, err\n\t}\n\treturn parseReplicaAddrs(addrs, false), nil\n}\n\nfunc parseReplicaAddrs(addrs []map[string]string, keepDisconnected bool) []string {\n\tnodes := make([]string, 0, len(addrs))\n\tfor _, node := range addrs {\n\t\tisDown := false\n\t\tif flags, ok := node[\"flags\"]; ok {\n\t\t\tfor _, flag := range strings.Split(flags, \",\") {\n\t\t\t\tswitch flag {\n\t\t\t\tcase \"s_down\", \"o_down\":\n\t\t\t\t\tisDown = true\n\t\t\t\tcase \"disconnected\":\n\t\t\t\t\tif !keepDisconnected {\n\t\t\t\t\t\tisDown = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !isDown && node[\"ip\"] != \"\" && node[\"port\"] != \"\" {\n\t\t\tnodes = append(nodes, net.JoinHostPort(node[\"ip\"], node[\"port\"]))\n\t\t}\n\t}\n\n\treturn nodes\n}\n\nfunc (c *sentinelFailover) trySwitchMaster(ctx context.Context, addr string) {\n\tc.mu.RLock()\n\tcurrentAddr := c.masterAddr //nolint:ifshort\n\tc.mu.RUnlock()\n\n\tif addr == currentAddr {\n\t\treturn\n\t}\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif addr == c.masterAddr {\n\t\treturn\n\t}\n\tc.masterAddr = addr\n\n\tinternal.Logger.Printf(ctx, \"sentinel: new master=%q addr=%q\",\n\t\tc.opt.MasterName, addr)\n\tif c.onFailover != nil {\n\t\tc.onFailover(ctx, addr)\n\t}\n}\n\nfunc (c *sentinelFailover) setSentinel(ctx context.Context, sentinel *SentinelClient) {\n\tif c.sentinel != nil {\n\t\tpanic(\"not reached\")\n\t}\n\tc.sentinel = sentinel\n\tc.discoverSentinels(ctx)\n\n\tc.pubsub = sentinel.Subscribe(ctx, \"+switch-master\", \"+replica-reconf-done\")\n\tgo c.listen(c.pubsub)\n}\n\nfunc (c *sentinelFailover) discoverSentinels(ctx context.Context) {\n\tsentinels, err := c.sentinel.Sentinels(ctx, c.opt.MasterName).Result()\n\tif err != nil {\n\t\tinternal.Logger.Printf(ctx, \"sentinel: Sentinels master=%q failed: %s\", c.opt.MasterName, err)\n\t\treturn\n\t}\n\tfor _, sentinel := range sentinels {\n\t\tip, ok := sentinel[\"ip\"]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tport, ok := sentinel[\"port\"]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif ip != \"\" && port != \"\" {\n\t\t\tsentinelAddr := net.JoinHostPort(ip, port)\n\t\t\tif !contains(c.sentinelAddrs, sentinelAddr) {\n\t\t\t\tinternal.Logger.Printf(ctx, \"sentinel: discovered new sentinel=%q for master=%q\",\n\t\t\t\t\tsentinelAddr, c.opt.MasterName)\n\t\t\t\tc.sentinelAddrs = append(c.sentinelAddrs, sentinelAddr)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *sentinelFailover) listen(pubsub *PubSub) {\n\tctx := context.TODO()\n\n\tif c.onUpdate != nil {\n\t\tc.onUpdate(ctx)\n\t}\n\n\tch := pubsub.Channel()\n\tfor msg := range ch {\n\t\tif msg.Channel == \"+switch-master\" {\n\t\t\tparts := strings.Split(msg.Payload, \" \")\n\t\t\tif parts[0] != c.opt.MasterName {\n\t\t\t\tinternal.Logger.Printf(pubsub.getContext(), \"sentinel: ignore addr for master=%q\", parts[0])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\taddr := net.JoinHostPort(parts[3], parts[4])\n\t\t\tc.trySwitchMaster(pubsub.getContext(), addr)\n\t\t}\n\n\t\tif c.onUpdate != nil {\n\t\t\tc.onUpdate(ctx)\n\t\t}\n\t}\n}\n\nfunc contains(slice []string, str string) bool {\n\tfor _, s := range slice {\n\t\tif s == str {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n//------------------------------------------------------------------------------\n\n// NewFailoverClusterClient returns a client that supports routing read-only commands\n// to a replica node.\nfunc NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {\n\tif failoverOpt == nil {\n\t\tpanic(\"redis: NewFailoverClusterClient nil options\")\n\t}\n\n\tsentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))\n\tcopy(sentinelAddrs, failoverOpt.SentinelAddrs)\n\n\tfailover := &sentinelFailover{\n\t\topt:           failoverOpt,\n\t\tsentinelAddrs: sentinelAddrs,\n\t}\n\n\topt := failoverOpt.clusterOptions()\n\tif failoverOpt.DB != 0 {\n\t\tonConnect := opt.OnConnect\n\n\t\topt.OnConnect = func(ctx context.Context, cn *Conn) error {\n\t\t\tif err := cn.Select(ctx, failoverOpt.DB).Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif onConnect != nil {\n\t\t\t\treturn onConnect(ctx, cn)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\topt.ClusterSlots = func(ctx context.Context) ([]ClusterSlot, error) {\n\t\tmasterAddr, err := failover.MasterAddr(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnodes := []ClusterNode{{\n\t\t\tAddr: masterAddr,\n\t\t}}\n\n\t\treplicaAddrs, err := failover.replicaAddrs(ctx, false)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, replicaAddr := range replicaAddrs {\n\t\t\tnodes = append(nodes, ClusterNode{\n\t\t\t\tAddr: replicaAddr,\n\t\t\t})\n\t\t}\n\n\t\tslots := []ClusterSlot{\n\t\t\t{\n\t\t\t\tStart: 0,\n\t\t\t\tEnd:   16383,\n\t\t\t\tNodes: nodes,\n\t\t\t},\n\t\t}\n\t\treturn slots, nil\n\t}\n\n\tc := NewClusterClient(opt)\n\n\tfailover.mu.Lock()\n\tfailover.onUpdate = func(ctx context.Context) {\n\t\tc.ReloadState(ctx)\n\t}\n\tfailover.mu.Unlock()\n\n\treturn c\n}\n"
  },
  {
    "path": "sentinel_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"slices\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"Sentinel PROTO 2\", func() {\n\tvar client *redis.Client\n\tBeforeEach(func() {\n\t\tclient = redis.NewFailoverClient(&redis.FailoverOptions{\n\t\t\tMasterName:    sentinelName,\n\t\t\tSentinelAddrs: sentinelAddrs,\n\t\t\tMaxRetries:    -1,\n\t\t\tProtocol:      2,\n\t\t})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\t_ = client.Close()\n\t})\n\n\tIt(\"should sentinel client PROTO 2\", func() {\n\t\tval, err := client.Do(ctx, \"HELLO\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).Should(ContainElements(\"proto\", int64(2)))\n\t})\n})\n\nvar _ = Describe(\"Sentinel resolution\", func() {\n\tIt(\"should resolve master without context exhaustion\", func() {\n\t\tshortCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)\n\t\tdefer cancel()\n\n\t\tclient := redis.NewFailoverClient(&redis.FailoverOptions{\n\t\t\tMasterName:    sentinelName,\n\t\t\tSentinelAddrs: sentinelAddrs,\n\t\t\tMaxRetries:    -1,\n\t\t})\n\n\t\terr := client.Ping(shortCtx).Err()\n\t\tExpect(err).NotTo(HaveOccurred(), \"expected master to resolve without context exhaustion\")\n\n\t\t_ = client.Close()\n\t})\n})\n\nvar _ = Describe(\"Sentinel\", func() {\n\tvar client *redis.Client\n\tvar master *redis.Client\n\tvar sentinel *redis.SentinelClient\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewFailoverClient(&redis.FailoverOptions{\n\t\t\tClientName:    \"sentinel_hi\",\n\t\t\tMasterName:    sentinelName,\n\t\t\tSentinelAddrs: sentinelAddrs,\n\t\t\tMaxRetries:    -1,\n\t\t})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\n\t\tsentinel = redis.NewSentinelClient(&redis.Options{\n\t\t\tAddr:       \":\" + sentinelPort1,\n\t\t\tMaxRetries: -1,\n\t\t})\n\n\t\taddr, err := sentinel.GetMasterAddrByName(ctx, sentinelName).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tmaster = redis.NewClient(&redis.Options{\n\t\t\tAddr:       net.JoinHostPort(addr[0], addr[1]),\n\t\t\tMaxRetries: -1,\n\t\t})\n\n\t\t// Wait until slaves are picked up by sentinel.\n\t\tEventually(func() string {\n\t\t\treturn sentinel1.Info(ctx).Val()\n\t\t}, \"20s\", \"100ms\").Should(ContainSubstring(\"slaves=2\"))\n\t\tEventually(func() string {\n\t\t\treturn sentinel2.Info(ctx).Val()\n\t\t}, \"20s\", \"100ms\").Should(ContainSubstring(\"slaves=2\"))\n\t\tEventually(func() string {\n\t\t\treturn sentinel3.Info(ctx).Val()\n\t\t}, \"20s\", \"100ms\").Should(ContainSubstring(\"slaves=2\"))\n\t})\n\n\tAfterEach(func() {\n\t\t_ = client.Close()\n\t\t_ = master.Close()\n\t\t_ = sentinel.Close()\n\t})\n\n\tIt(\"should facilitate failover\", func() {\n\t\t// Set value on master.\n\t\terr := client.Set(ctx, \"foo\", \"master\", 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Verify.\n\t\tval, err := client.Get(ctx, \"foo\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"master\"))\n\n\t\t// Verify master->slaves sync.\n\t\tvar slavesAddr []string\n\t\tEventually(func() []string {\n\t\t\tslavesAddr = redis.GetSlavesAddrByName(ctx, sentinel, sentinelName)\n\t\t\treturn slavesAddr\n\t\t}, \"20s\", \"50ms\").Should(HaveLen(2))\n\t\tEventually(func() bool {\n\t\t\tsync := true\n\t\t\tfor _, addr := range slavesAddr {\n\t\t\t\tslave := redis.NewClient(&redis.Options{\n\t\t\t\t\tAddr:       addr,\n\t\t\t\t\tMaxRetries: -1,\n\t\t\t\t})\n\t\t\t\tsync = slave.Get(ctx, \"foo\").Val() == \"master\"\n\t\t\t\t_ = slave.Close()\n\t\t\t}\n\t\t\treturn sync\n\t\t}, \"20s\", \"50ms\").Should(BeTrue())\n\n\t\t// Create subscription.\n\t\tpub := client.Subscribe(ctx, \"foo\")\n\t\tch := pub.Channel()\n\n\t\t// Kill master.\n\t\t/*\n\t\t\terr = master.Shutdown(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tEventually(func() error {\n\t\t\t\treturn master.Ping(ctx).Err()\n\t\t\t}, \"20s\", \"50ms\").Should(HaveOccurred())\n\t\t*/\n\n\t\t// Check that client picked up new master.\n\t\tEventually(func() string {\n\t\t\treturn client.Get(ctx, \"foo\").Val()\n\t\t}, \"20s\", \"100ms\").Should(Equal(\"master\"))\n\n\t\t// Check if subscription is renewed.\n\t\tvar msg *redis.Message\n\t\tEventually(func() <-chan *redis.Message {\n\t\t\t_ = client.Publish(ctx, \"foo\", \"hello\").Err()\n\t\t\treturn ch\n\t\t}, \"20s\", \"100ms\").Should(Receive(&msg))\n\t\tExpect(msg.Channel).To(Equal(\"foo\"))\n\t\tExpect(msg.Payload).To(Equal(\"hello\"))\n\t\tExpect(pub.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"supports DB selection\", func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\n\t\tclient = redis.NewFailoverClient(&redis.FailoverOptions{\n\t\t\tMasterName:    sentinelName,\n\t\t\tSentinelAddrs: sentinelAddrs,\n\t\t\tDB:            1,\n\t\t})\n\t\terr := client.Ping(ctx).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should sentinel client setname\", func() {\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t\tval, err := client.ClientList(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).Should(ContainSubstring(\"name=sentinel_hi\"))\n\t})\n\n\tIt(\"should sentinel client PROTO 3\", func() {\n\t\tval, err := client.Do(ctx, \"HELLO\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).Should(HaveKeyWithValue(\"proto\", int64(3)))\n\t})\n})\n\nvar _ = Describe(\"NewFailoverClusterClient PROTO 2\", func() {\n\tvar client *redis.ClusterClient\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewFailoverClusterClient(&redis.FailoverOptions{\n\t\t\tMasterName:    sentinelName,\n\t\t\tSentinelAddrs: sentinelAddrs,\n\t\t\tProtocol:      2,\n\n\t\t\tRouteRandomly: true,\n\t\t})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\t_ = client.Close()\n\t})\n\n\tIt(\"should sentinel cluster PROTO 2\", func() {\n\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\tval, err := client.Do(ctx, \"HELLO\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).Should(ContainElements(\"proto\", int64(2)))\n\t\t\treturn nil\n\t\t})\n\t})\n})\n\nvar _ = Describe(\"NewFailoverClusterClient\", func() {\n\tvar client *redis.ClusterClient\n\tvar master *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewFailoverClusterClient(&redis.FailoverOptions{\n\t\t\tClientName:    \"sentinel_cluster_hi\",\n\t\t\tMasterName:    sentinelName,\n\t\t\tSentinelAddrs: sentinelAddrs,\n\n\t\t\tRouteRandomly: true,\n\t\t\tDB:            1,\n\t\t})\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\n\t\tsentinel := redis.NewSentinelClient(&redis.Options{\n\t\t\tAddr:       \":\" + sentinelPort1,\n\t\t\tMaxRetries: -1,\n\t\t})\n\n\t\taddr, err := sentinel.GetMasterAddrByName(ctx, sentinelName).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tmaster = redis.NewClient(&redis.Options{\n\t\t\tAddr:       net.JoinHostPort(addr[0], addr[1]),\n\t\t\tMaxRetries: -1,\n\t\t})\n\n\t\t// Wait until slaves are picked up by sentinel.\n\t\tEventually(func() string {\n\t\t\treturn sentinel1.Info(ctx).Val()\n\t\t}, \"20s\", \"100ms\").Should(ContainSubstring(\"slaves=2\"))\n\t\tEventually(func() string {\n\t\t\treturn sentinel2.Info(ctx).Val()\n\t\t}, \"20s\", \"100ms\").Should(ContainSubstring(\"slaves=2\"))\n\t\tEventually(func() string {\n\t\t\treturn sentinel3.Info(ctx).Val()\n\t\t}, \"20s\", \"100ms\").Should(ContainSubstring(\"slaves=2\"))\n\t})\n\n\tAfterEach(func() {\n\t\t_ = client.Close()\n\t\t_ = master.Close()\n\t})\n\n\tIt(\"should facilitate failover\", func() {\n\t\t// Set value.\n\t\terr := client.Set(ctx, \"foo\", \"master\", 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tfor i := 0; i < 100; i++ {\n\t\t\t// Verify.\n\t\t\tEventually(func() string {\n\t\t\t\treturn client.Get(ctx, \"foo\").Val()\n\t\t\t}, \"20s\", \"1ms\").Should(Equal(\"master\"))\n\t\t}\n\n\t\t// Create subscription.\n\t\tsub := client.Subscribe(ctx, \"foo\")\n\t\tch := sub.Channel()\n\n\t\t// Kill master.\n\t\t/*\n\t\t\terr = master.Shutdown(ctx).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tEventually(func() error {\n\t\t\t\treturn master.Ping(ctx).Err()\n\t\t\t}, \"20s\", \"100ms\").Should(HaveOccurred())\n\t\t*/\n\n\t\t// Check that client picked up new master.\n\t\tEventually(func() string {\n\t\t\treturn client.Get(ctx, \"foo\").Val()\n\t\t}, \"20s\", \"100ms\").Should(Equal(\"master\"))\n\n\t\t// Check if subscription is renewed.\n\t\tvar msg *redis.Message\n\t\tEventually(func() <-chan *redis.Message {\n\t\t\t_ = client.Publish(ctx, \"foo\", \"hello\").Err()\n\t\t\treturn ch\n\t\t}, \"20s\", \"100ms\").Should(Receive(&msg))\n\t\tExpect(msg.Channel).To(Equal(\"foo\"))\n\t\tExpect(msg.Payload).To(Equal(\"hello\"))\n\t\tExpect(sub.Close()).NotTo(HaveOccurred())\n\n\t})\n\n\tIt(\"should sentinel cluster client setname\", func() {\n\t\terr := client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\treturn c.Ping(ctx).Err()\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\tval, err := c.ClientList(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).Should(ContainSubstring(\"name=sentinel_cluster_hi\"))\n\t\t\treturn nil\n\t\t})\n\t})\n\n\tIt(\"should sentinel cluster client db\", func() {\n\t\terr := client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\treturn c.Ping(ctx).Err()\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\tclientInfo, err := c.ClientInfo(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(clientInfo.DB).To(Equal(1))\n\t\t\treturn nil\n\t\t})\n\t})\n\n\tIt(\"should sentinel cluster PROTO 3\", func() {\n\t\t_ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error {\n\t\t\tval, err := client.Do(ctx, \"HELLO\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).Should(HaveKeyWithValue(\"proto\", int64(3)))\n\t\t\treturn nil\n\t\t})\n\t})\n})\n\nvar _ = Describe(\"SentinelAclAuth\", func() {\n\tconst (\n\t\taclSentinelUsername = \"sentinel-user\"\n\t\taclSentinelPassword = \"sentinel-pass\"\n\t)\n\n\tvar client *redis.Client\n\tvar sentinel *redis.SentinelClient\n\tsentinels := func() []*redis.Client {\n\t\treturn []*redis.Client{sentinel1, sentinel2, sentinel3}\n\t}\n\n\tBeforeEach(func() {\n\t\tauthCmd := redis.NewStatusCmd(ctx, \"ACL\", \"SETUSER\", aclSentinelUsername, \"ON\",\n\t\t\t\">\"+aclSentinelPassword, \"-@all\", \"+auth\", \"+client|getname\", \"+client|id\", \"+client|setname\",\n\t\t\t\"+command\", \"+hello\", \"+ping\", \"+client|setinfo\", \"+role\", \"+sentinel|get-master-addr-by-name\", \"+sentinel|master\",\n\t\t\t\"+sentinel|myid\", \"+sentinel|replicas\", \"+sentinel|sentinels\")\n\n\t\tfor _, process := range sentinels() {\n\t\t\terr := process.Process(ctx, authCmd)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\n\t\tclient = redis.NewFailoverClient(&redis.FailoverOptions{\n\t\t\tMasterName:       sentinelName,\n\t\t\tSentinelAddrs:    sentinelAddrs,\n\t\t\tMaxRetries:       -1,\n\t\t\tSentinelUsername: aclSentinelUsername,\n\t\t\tSentinelPassword: aclSentinelPassword,\n\t\t})\n\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\n\t\tsentinel = redis.NewSentinelClient(&redis.Options{\n\t\t\tAddr:       sentinelAddrs[0],\n\t\t\tMaxRetries: -1,\n\t\t\tUsername:   aclSentinelUsername,\n\t\t\tPassword:   aclSentinelPassword,\n\t\t})\n\n\t\t_, err := sentinel.GetMasterAddrByName(ctx, sentinelName).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// Wait until sentinels are picked up by each other.\n\t\tfor _, process := range sentinels() {\n\t\t\tEventually(func() string {\n\t\t\t\treturn process.Info(ctx).Val()\n\t\t\t}, \"20s\", \"100ms\").Should(ContainSubstring(\"sentinels=3\"))\n\t\t}\n\t})\n\n\tAfterEach(func() {\n\t\tunauthCommand := redis.NewStatusCmd(ctx, \"ACL\", \"DELUSER\", aclSentinelUsername)\n\n\t\tfor _, process := range sentinels() {\n\t\t\terr := process.Process(ctx, unauthCommand)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\n\t\t_ = client.Close()\n\t\t_ = sentinel.Close()\n\t})\n\n\tIt(\"should still facilitate operations\", func() {\n\t\terr := client.Set(ctx, \"wow\", \"acl-auth\", 0).Err()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tval, err := client.Get(ctx, \"wow\").Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(val).To(Equal(\"acl-auth\"))\n\t})\n})\n\n// renaming from TestParseFailoverURL to TestParseSentinelURL\n// to be easier to find Failed tests in the test output\nfunc TestParseSentinelURL(t *testing.T) {\n\tcases := []struct {\n\t\turl string\n\t\to   *redis.FailoverOptions\n\t\terr error\n\t}{\n\t\t{\n\t\t\turl: \"redis://localhost:6379?master_name=test\",\n\t\t\to:   &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6379\"}, MasterName: \"test\"},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?master_name=test\",\n\t\t\to:   &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6379\"}, MasterName: \"test\", DB: 5},\n\t\t},\n\t\t{\n\t\t\turl: \"rediss://localhost:6379/5?master_name=test\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6379\"}, MasterName: \"test\", DB: 5,\n\t\t\t\tTLSConfig: &tls.Config{\n\t\t\t\t\tServerName: \"localhost\",\n\t\t\t\t}},\n\t\t},\n\t\t{\n\t\t\turl: \"rediss://localhost:6379/5?master_name=test&skip_verify=true\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6379\"}, MasterName: \"test\", DB: 5,\n\t\t\t\tTLSConfig: &tls.Config{\n\t\t\t\t\tServerName:         \"localhost\",\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t}},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?master_name=test&db=2\",\n\t\t\to:   &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6379\"}, MasterName: \"test\", DB: 2},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&addr=localhost:6381\",\n\t\t\to:   &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\", \"localhost:6381\"}, DB: 5},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://foo:bar@localhost:6379/5?addr=localhost:6380\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tSentinelUsername: \"foo\", SentinelPassword: \"bar\", DB: 5},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://:bar@localhost:6379/5?addr=localhost:6380\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tSentinelUsername: \"\", SentinelPassword: \"bar\", DB: 5},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://foo@localhost:6379/5?addr=localhost:6380\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tSentinelUsername: \"foo\", SentinelPassword: \"\", DB: 5},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tSentinelUsername: \"foo\", SentinelPassword: \"bar\", DB: 5, DialTimeout: 3 * time.Second},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3s\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tSentinelUsername: \"foo\", SentinelPassword: \"bar\", DB: 5, DialTimeout: 3 * time.Second},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3ms\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tSentinelUsername: \"foo\", SentinelPassword: \"bar\", DB: 5, DialTimeout: 3 * time.Millisecond},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3&pool_fifo=true\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tSentinelUsername: \"foo\", SentinelPassword: \"bar\", DB: 5, DialTimeout: 3 * time.Second, PoolFIFO: true},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout=3&pool_fifo=false\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: 3 * time.Second, PoolFIFO: false},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout=3&pool_fifo\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: 3 * time.Second, PoolFIFO: false},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: 0},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout=0\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: -1},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout=-1\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: -1},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout=-2\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: -1},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout=\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: 0},\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost:6379/5?addr=localhost:6380&dial_timeout=0&abc=5\",\n\t\t\to: &redis.FailoverOptions{SentinelAddrs: []string{\"localhost:6380\", \"localhost:6379\"},\n\t\t\t\tDB: 5, DialTimeout: -1},\n\t\t\terr: errors.New(\"redis: unexpected option: abc\"),\n\t\t},\n\t\t{\n\t\t\turl: \"http://google.com\",\n\t\t\terr: errors.New(\"redis: invalid URL scheme: http\"),\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost/1/2/3/4\",\n\t\t\terr: errors.New(\"redis: invalid URL path: /1/2/3/4\"),\n\t\t},\n\t\t{\n\t\t\turl: \"12345\",\n\t\t\terr: errors.New(\"redis: invalid URL scheme: \"),\n\t\t},\n\t\t{\n\t\t\turl: \"redis://localhost/database\",\n\t\t\terr: errors.New(`redis: invalid database number: \"database\"`),\n\t\t},\n\t}\n\n\tfor i := range cases {\n\t\ttc := cases[i]\n\t\tt.Run(tc.url, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := redis.ParseFailoverURL(tc.url)\n\t\t\tif tc.err == nil && err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %q\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tc.err != nil && err == nil {\n\t\t\t\tt.Fatalf(\"got nil, expected %q\", tc.err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tc.err != nil && err != nil {\n\t\t\t\tif tc.err.Error() != err.Error() {\n\t\t\t\t\tt.Fatalf(\"got %q, expected %q\", err, tc.err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcompareFailoverOptions(t, actual, tc.o)\n\t\t})\n\t}\n}\n\nfunc compareFailoverOptions(t *testing.T, a, e *redis.FailoverOptions) {\n\tif a.MasterName != e.MasterName {\n\t\tt.Errorf(\"MasterName got %q, want %q\", a.MasterName, e.MasterName)\n\t}\n\tcompareSlices(t, a.SentinelAddrs, e.SentinelAddrs, \"SentinelAddrs\")\n\tif a.ClientName != e.ClientName {\n\t\tt.Errorf(\"ClientName got %q, want %q\", a.ClientName, e.ClientName)\n\t}\n\tif a.SentinelUsername != e.SentinelUsername {\n\t\tt.Errorf(\"SentinelUsername got %q, want %q\", a.SentinelUsername, e.SentinelUsername)\n\t}\n\tif a.SentinelPassword != e.SentinelPassword {\n\t\tt.Errorf(\"SentinelPassword got %q, want %q\", a.SentinelPassword, e.SentinelPassword)\n\t}\n\tif a.RouteByLatency != e.RouteByLatency {\n\t\tt.Errorf(\"RouteByLatency got %v, want %v\", a.RouteByLatency, e.RouteByLatency)\n\t}\n\tif a.RouteRandomly != e.RouteRandomly {\n\t\tt.Errorf(\"RouteRandomly got %v, want %v\", a.RouteRandomly, e.RouteRandomly)\n\t}\n\tif a.ReplicaOnly != e.ReplicaOnly {\n\t\tt.Errorf(\"ReplicaOnly got %v, want %v\", a.ReplicaOnly, e.ReplicaOnly)\n\t}\n\tif a.UseDisconnectedReplicas != e.UseDisconnectedReplicas {\n\t\tt.Errorf(\"UseDisconnectedReplicas got %v, want %v\", a.UseDisconnectedReplicas, e.UseDisconnectedReplicas)\n\t}\n\tif a.Protocol != e.Protocol {\n\t\tt.Errorf(\"Protocol got %v, want %v\", a.Protocol, e.Protocol)\n\t}\n\tif a.Username != e.Username {\n\t\tt.Errorf(\"Username got %q, want %q\", a.Username, e.Username)\n\t}\n\tif a.Password != e.Password {\n\t\tt.Errorf(\"Password got %q, want %q\", a.Password, e.Password)\n\t}\n\tif a.DB != e.DB {\n\t\tt.Errorf(\"DB got %v, want %v\", a.DB, e.DB)\n\t}\n\tif a.MaxRetries != e.MaxRetries {\n\t\tt.Errorf(\"MaxRetries got %v, want %v\", a.MaxRetries, e.MaxRetries)\n\t}\n\tif a.MinRetryBackoff != e.MinRetryBackoff {\n\t\tt.Errorf(\"MinRetryBackoff got %v, want %v\", a.MinRetryBackoff, e.MinRetryBackoff)\n\t}\n\tif a.MaxRetryBackoff != e.MaxRetryBackoff {\n\t\tt.Errorf(\"MaxRetryBackoff got %v, want %v\", a.MaxRetryBackoff, e.MaxRetryBackoff)\n\t}\n\tif a.DialTimeout != e.DialTimeout {\n\t\tt.Errorf(\"DialTimeout got %v, want %v\", a.DialTimeout, e.DialTimeout)\n\t}\n\tif a.ReadTimeout != e.ReadTimeout {\n\t\tt.Errorf(\"ReadTimeout got %v, want %v\", a.ReadTimeout, e.ReadTimeout)\n\t}\n\tif a.WriteTimeout != e.WriteTimeout {\n\t\tt.Errorf(\"WriteTimeout got %v, want %v\", a.WriteTimeout, e.WriteTimeout)\n\t}\n\tif a.ContextTimeoutEnabled != e.ContextTimeoutEnabled {\n\t\tt.Errorf(\"ContentTimeoutEnabled got %v, want %v\", a.ContextTimeoutEnabled, e.ContextTimeoutEnabled)\n\t}\n\tif a.PoolFIFO != e.PoolFIFO {\n\t\tt.Errorf(\"PoolFIFO got %v, want %v\", a.PoolFIFO, e.PoolFIFO)\n\t}\n\tif a.PoolSize != e.PoolSize {\n\t\tt.Errorf(\"PoolSize got %v, want %v\", a.PoolSize, e.PoolSize)\n\t}\n\tif a.PoolTimeout != e.PoolTimeout {\n\t\tt.Errorf(\"PoolTimeout got %v, want %v\", a.PoolTimeout, e.PoolTimeout)\n\t}\n\tif a.MinIdleConns != e.MinIdleConns {\n\t\tt.Errorf(\"MinIdleConns got %v, want %v\", a.MinIdleConns, e.MinIdleConns)\n\t}\n\tif a.MaxIdleConns != e.MaxIdleConns {\n\t\tt.Errorf(\"MaxIdleConns got %v, want %v\", a.MaxIdleConns, e.MaxIdleConns)\n\t}\n\tif a.MaxActiveConns != e.MaxActiveConns {\n\t\tt.Errorf(\"MaxActiveConns got %v, want %v\", a.MaxActiveConns, e.MaxActiveConns)\n\t}\n\tif a.ConnMaxIdleTime != e.ConnMaxIdleTime {\n\t\tt.Errorf(\"ConnMaxIdleTime got %v, want %v\", a.ConnMaxIdleTime, e.ConnMaxIdleTime)\n\t}\n\tif a.ConnMaxLifetime != e.ConnMaxLifetime {\n\t\tt.Errorf(\"ConnMaxLifeTime got %v, want %v\", a.ConnMaxLifetime, e.ConnMaxLifetime)\n\t}\n\tif a.ConnMaxLifetimeJitter != e.ConnMaxLifetimeJitter {\n\t\tt.Errorf(\"ConnMaxLifetimeJitter got %v, want %v\", a.ConnMaxLifetimeJitter, e.ConnMaxLifetimeJitter)\n\t}\n\tif a.DisableIdentity != e.DisableIdentity {\n\t\tt.Errorf(\"DisableIdentity got %v, want %v\", a.DisableIdentity, e.DisableIdentity)\n\t}\n\tif a.IdentitySuffix != e.IdentitySuffix {\n\t\tt.Errorf(\"IdentitySuffix got %v, want %v\", a.IdentitySuffix, e.IdentitySuffix)\n\t}\n\tif a.UnstableResp3 != e.UnstableResp3 {\n\t\tt.Errorf(\"UnstableResp3 got %v, want %v\", a.UnstableResp3, e.UnstableResp3)\n\t}\n\tif (a.TLSConfig == nil && e.TLSConfig != nil) || (a.TLSConfig != nil && e.TLSConfig == nil) {\n\t\tt.Errorf(\"TLSConfig error\")\n\t}\n\tif a.TLSConfig != nil && e.TLSConfig != nil {\n\t\tif a.TLSConfig.ServerName != e.TLSConfig.ServerName {\n\t\t\tt.Errorf(\"TLSConfig.ServerName got %q, want %q\", a.TLSConfig.ServerName, e.TLSConfig.ServerName)\n\t\t}\n\t}\n}\n\nfunc compareSlices(t *testing.T, a, b []string, name string) {\n\tslices.Sort(a)\n\tslices.Sort(b)\n\tif len(a) != len(b) {\n\t\tt.Errorf(\"%s got %q, want %q\", name, a, b)\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\tt.Errorf(\"%s got %q, want %q\", name, a, b)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "set_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n)\n\n// SetCmdable is an interface for Redis set commands.\n// Sets are unordered collections of unique strings.\ntype SetCmdable interface {\n\tSAdd(ctx context.Context, key string, members ...interface{}) *IntCmd\n\tSCard(ctx context.Context, key string) *IntCmd\n\tSDiff(ctx context.Context, keys ...string) *StringSliceCmd\n\tSDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd\n\tSInter(ctx context.Context, keys ...string) *StringSliceCmd\n\tSInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd\n\tSInterStore(ctx context.Context, destination string, keys ...string) *IntCmd\n\tSIsMember(ctx context.Context, key string, member interface{}) *BoolCmd\n\tSMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd\n\tSMembers(ctx context.Context, key string) *StringSliceCmd\n\tSMembersMap(ctx context.Context, key string) *StringStructMapCmd\n\tSMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd\n\tSPop(ctx context.Context, key string) *StringCmd\n\tSPopN(ctx context.Context, key string, count int64) *StringSliceCmd\n\tSRandMember(ctx context.Context, key string) *StringCmd\n\tSRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd\n\tSRem(ctx context.Context, key string, members ...interface{}) *IntCmd\n\tSScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd\n\tSUnion(ctx context.Context, keys ...string) *StringSliceCmd\n\tSUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd\n}\n\n// Returns the number of elements that were added to the set, not including all\n// the elements already present in the set.\n//\n// For more information about the command please refer to [SADD].\n//\n// [SADD]: (https://redis.io/docs/latest/commands/sadd/)\nfunc (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(members))\n\targs[0] = \"sadd\"\n\targs[1] = key\n\targs = appendArgs(args, members)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns the set cardinality (number of elements) of the set stored at key.\n// Returns 0 if key does not exist.\n//\n// For more information about the command please refer to [SCARD].\n//\n// [SCARD]: (https://redis.io/docs/latest/commands/scard/)\nfunc (c cmdable) SCard(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"scard\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns the members of the set resulting from the difference between the first set\n// and all the successive sets.\n// Keys that do not exist are considered to be empty sets.\n//\n// For more information about the command please refer to [SDIFF].\n//\n// [SDIFF]: (https://redis.io/docs/latest/commands/sdiff/)\nfunc (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"sdiff\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Stores the members of the set resulting from the difference between the first set\n// and all the successive sets into destination.\n// If destination already exists, it is overwritten.\n//\n// For more information about the command please refer to [SDIFFSTORE].\n//\n// [SDIFFSTORE]: (https://redis.io/docs/latest/commands/sdiffstore/)\nfunc (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd {\n\targs := make([]interface{}, 2+len(keys))\n\targs[0] = \"sdiffstore\"\n\targs[1] = destination\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns the members of the set resulting from the intersection of all the given sets.\n// Keys that do not exist are considered to be empty sets.\n// With one of the keys being an empty set, the resulting set is also empty.\n//\n// For more information about the command please refer to [SINTER].\n//\n// [SINTER]: (https://redis.io/docs/latest/commands/sinter/)\nfunc (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"sinter\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns the cardinality of the set resulting from the intersection of all the given sets.\n// Keys that do not exist are considered to be empty sets.\n// With one of the keys being an empty set, the resulting set is also empty.\n//\n// The limit parameter sets an upper bound on the number of results returned.\n// If limit is 0, no limit is applied.\n//\n// For more information about the command please refer to [SINTERCARD].\n//\n// [SINTERCARD]: (https://redis.io/docs/latest/commands/sintercard/)\nfunc (c cmdable) SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd {\n\tnumKeys := len(keys)\n\targs := make([]interface{}, 4+numKeys)\n\targs[0] = \"sintercard\"\n\targs[1] = numKeys\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\targs[2+numKeys] = \"limit\"\n\targs[3+numKeys] = limit\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Stores the members of the set resulting from the intersection of all the given sets\n// into destination.\n// If destination already exists, it is overwritten.\n//\n// For more information about the command please refer to [SINTERSTORE].\n//\n// [SINTERSTORE]: (https://redis.io/docs/latest/commands/sinterstore/)\nfunc (c cmdable) SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd {\n\targs := make([]interface{}, 2+len(keys))\n\targs[0] = \"sinterstore\"\n\targs[1] = destination\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns if member is a member of the set stored at key.\n// Returns true if the element is a member of the set, false if it is not a member\n// or if key does not exist.\n//\n// For more information about the command please refer to [SISMEMBER].\n//\n// [SISMEMBER]: (https://redis.io/docs/latest/commands/sismember/)\nfunc (c cmdable) SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"sismember\", key, member)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns whether each member is a member of the set stored at key.\n// For each member, returns true if the element is a member of the set, false if it is not\n// a member or if key does not exist.\n//\n// For more information about the command please refer to [SMISMEMBER].\n//\n// [SMISMEMBER]: (https://redis.io/docs/latest/commands/smismember/)\nfunc (c cmdable) SMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd {\n\targs := make([]interface{}, 2, 2+len(members))\n\targs[0] = \"smismember\"\n\targs[1] = key\n\targs = appendArgs(args, members)\n\tcmd := NewBoolSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns all the members of the set value stored at key.\n// Returns an empty slice if key does not exist.\n//\n// For more information about the command please refer to [SMEMBERS].\n//\n// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/)\nfunc (c cmdable) SMembers(ctx context.Context, key string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"smembers\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns all the members of the set value stored at key as a map.\n// Returns an empty map if key does not exist.\n//\n// For more information about the command please refer to [SMEMBERS].\n//\n// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/)\nfunc (c cmdable) SMembersMap(ctx context.Context, key string) *StringStructMapCmd {\n\tcmd := NewStringStructMapCmd(ctx, \"smembers\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Moves member from the set at source to the set at destination.\n// This operation is atomic. In every given moment the element will appear to be a member\n// of source or destination for other clients.\n//\n// For more information about the command please refer to [SMOVE].\n//\n// [SMOVE]: (https://redis.io/docs/latest/commands/smove/)\nfunc (c cmdable) SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"smove\", source, destination, member)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Removes and returns one or more random members from the set value stored at key.\n// This version returns a single random member.\n//\n// For more information about the command please refer to [SPOP].\n//\n// [SPOP]: (https://redis.io/docs/latest/commands/spop/)\nfunc (c cmdable) SPop(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"spop\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Removes and returns one or more random members from the set value stored at key.\n// This version returns up to count random members.\n//\n// For more information about the command please refer to [SPOP].\n//\n// [SPOP]: (https://redis.io/docs/latest/commands/spop/)\nfunc (c cmdable) SPopN(ctx context.Context, key string, count int64) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"spop\", key, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns a random member from the set value stored at key.\n// This version returns a single random member without removing it.\n//\n// For more information about the command please refer to [SRANDMEMBER].\n//\n// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/)\nfunc (c cmdable) SRandMember(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"srandmember\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns an array of random members from the set value stored at key.\n// This version returns up to count random members without removing them.\n// When called with a positive count, returns distinct elements.\n// When called with a negative count, allows for repeated elements.\n//\n// For more information about the command please refer to [SRANDMEMBER].\n//\n// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/)\nfunc (c cmdable) SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"srandmember\", key, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Removes the specified members from the set stored at key.\n// Specified members that are not a member of this set are ignored.\n// If key does not exist, it is treated as an empty set and this command returns 0.\n//\n// For more information about the command please refer to [SREM].\n//\n// [SREM]: (https://redis.io/docs/latest/commands/srem/)\nfunc (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(members))\n\targs[0] = \"srem\"\n\targs[1] = key\n\targs = appendArgs(args, members)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Returns the members of the set resulting from the union of all the given sets.\n// Keys that do not exist are considered to be empty sets.\n//\n// For more information about the command please refer to [SUNION].\n//\n// [SUNION]: (https://redis.io/docs/latest/commands/sunion/)\nfunc (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"sunion\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Stores the members of the set resulting from the union of all the given sets\n// into destination.\n// If destination already exists, it is overwritten.\n//\n// For more information about the command please refer to [SUNIONSTORE].\n//\n// [SUNIONSTORE]: (https://redis.io/docs/latest/commands/sunionstore/)\nfunc (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd {\n\targs := make([]interface{}, 2+len(keys))\n\targs[0] = \"sunionstore\"\n\targs[1] = destination\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Incrementally iterates the set elements stored at key.\n// This is a cursor-based iterator that allows scanning large sets efficiently.\n//\n// Parameters:\n//   - cursor: The cursor value for the iteration (use 0 to start a new scan)\n//   - match: Optional pattern to match elements (empty string means no pattern)\n//   - count: Optional hint about how many elements to return per iteration\n//\n// For more information about the command please refer to [SSCAN].\n//\n// [SSCAN]: (https://redis.io/docs/latest/commands/sscan/)\nfunc (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {\n\targs := []interface{}{\"sscan\", key, cursor}\n\tif match != \"\" {\n\t\targs = append(args, \"match\", match)\n\t}\n\tif count > 0 {\n\t\targs = append(args, \"count\", count)\n\t}\n\tcmd := NewScanCmd(ctx, c, args...)\n\tif hashtag.Present(match) {\n\t\tcmd.SetFirstKeyPos(4)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "sortedset_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n)\n\ntype SortedSetCmdable interface {\n\tBZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd\n\tBZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd\n\tBZMPop(ctx context.Context, timeout time.Duration, order string, count int64, keys ...string) *ZSliceWithKeyCmd\n\tZAdd(ctx context.Context, key string, members ...Z) *IntCmd\n\tZAddLT(ctx context.Context, key string, members ...Z) *IntCmd\n\tZAddGT(ctx context.Context, key string, members ...Z) *IntCmd\n\tZAddNX(ctx context.Context, key string, members ...Z) *IntCmd\n\tZAddXX(ctx context.Context, key string, members ...Z) *IntCmd\n\tZAddArgs(ctx context.Context, key string, args ZAddArgs) *IntCmd\n\tZAddArgsIncr(ctx context.Context, key string, args ZAddArgs) *FloatCmd\n\tZCard(ctx context.Context, key string) *IntCmd\n\tZCount(ctx context.Context, key, min, max string) *IntCmd\n\tZLexCount(ctx context.Context, key, min, max string) *IntCmd\n\tZIncrBy(ctx context.Context, key string, increment float64, member string) *FloatCmd\n\tZInter(ctx context.Context, store *ZStore) *StringSliceCmd\n\tZInterWithScores(ctx context.Context, store *ZStore) *ZSliceCmd\n\tZInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd\n\tZInterStore(ctx context.Context, destination string, store *ZStore) *IntCmd\n\tZMPop(ctx context.Context, order string, count int64, keys ...string) *ZSliceWithKeyCmd\n\tZMScore(ctx context.Context, key string, members ...string) *FloatSliceCmd\n\tZPopMax(ctx context.Context, key string, count ...int64) *ZSliceCmd\n\tZPopMin(ctx context.Context, key string, count ...int64) *ZSliceCmd\n\tZRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd\n\tZRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd\n\tZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd\n\tZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd\n\tZRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd\n\tZRangeArgs(ctx context.Context, z ZRangeArgs) *StringSliceCmd\n\tZRangeArgsWithScores(ctx context.Context, z ZRangeArgs) *ZSliceCmd\n\tZRangeStore(ctx context.Context, dst string, z ZRangeArgs) *IntCmd\n\tZRank(ctx context.Context, key, member string) *IntCmd\n\tZRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd\n\tZRem(ctx context.Context, key string, members ...interface{}) *IntCmd\n\tZRemRangeByRank(ctx context.Context, key string, start, stop int64) *IntCmd\n\tZRemRangeByScore(ctx context.Context, key, min, max string) *IntCmd\n\tZRemRangeByLex(ctx context.Context, key, min, max string) *IntCmd\n\tZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd\n\tZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd\n\tZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd\n\tZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd\n\tZRevRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd\n\tZRevRank(ctx context.Context, key, member string) *IntCmd\n\tZRevRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd\n\tZScore(ctx context.Context, key, member string) *FloatCmd\n\tZUnionStore(ctx context.Context, dest string, store *ZStore) *IntCmd\n\tZRandMember(ctx context.Context, key string, count int) *StringSliceCmd\n\tZRandMemberWithScores(ctx context.Context, key string, count int) *ZSliceCmd\n\tZUnion(ctx context.Context, store ZStore) *StringSliceCmd\n\tZUnionWithScores(ctx context.Context, store ZStore) *ZSliceCmd\n\tZDiff(ctx context.Context, keys ...string) *StringSliceCmd\n\tZDiffWithScores(ctx context.Context, keys ...string) *ZSliceCmd\n\tZDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd\n\tZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd\n}\n\n// BZPopMax Redis `BZPOPMAX key [key ...] timeout` command.\nfunc (c cmdable) BZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd {\n\targs := make([]interface{}, 1+len(keys)+1)\n\targs[0] = \"bzpopmax\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\targs[len(args)-1] = formatSec(ctx, timeout)\n\tcmd := NewZWithKeyCmd(ctx, args...)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BZPopMin Redis `BZPOPMIN key [key ...] timeout` command.\nfunc (c cmdable) BZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd {\n\targs := make([]interface{}, 1+len(keys)+1)\n\targs[0] = \"bzpopmin\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\targs[len(args)-1] = formatSec(ctx, timeout)\n\tcmd := NewZWithKeyCmd(ctx, args...)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// BZMPop is the blocking variant of ZMPOP.\n// When any of the sorted sets contains elements, this command behaves exactly like ZMPOP.\n// When all sorted sets are empty, Redis will block the connection until another client adds members to one of the keys or until the timeout elapses.\n// A timeout of zero can be used to block indefinitely.\n// example: client.BZMPop(ctx, 0,\"max\", 1, \"set\")\nfunc (c cmdable) BZMPop(ctx context.Context, timeout time.Duration, order string, count int64, keys ...string) *ZSliceWithKeyCmd {\n\targs := make([]interface{}, 3+len(keys), 6+len(keys))\n\targs[0] = \"bzmpop\"\n\targs[1] = formatSec(ctx, timeout)\n\targs[2] = len(keys)\n\tfor i, key := range keys {\n\t\targs[3+i] = key\n\t}\n\targs = append(args, strings.ToLower(order), \"count\", count)\n\tcmd := NewZSliceWithKeyCmd(ctx, args...)\n\tcmd.setReadTimeout(timeout)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZAddArgs WARN: The GT, LT and NX options are mutually exclusive.\ntype ZAddArgs struct {\n\tNX      bool\n\tXX      bool\n\tLT      bool\n\tGT      bool\n\tCh      bool\n\tMembers []Z\n}\n\nfunc (c cmdable) zAddArgs(key string, args ZAddArgs, incr bool) []interface{} {\n\ta := make([]interface{}, 0, 6+2*len(args.Members))\n\ta = append(a, \"zadd\", key)\n\n\t// The GT, LT and NX options are mutually exclusive.\n\tif args.NX {\n\t\ta = append(a, \"nx\")\n\t} else {\n\t\tif args.XX {\n\t\t\ta = append(a, \"xx\")\n\t\t}\n\t\tif args.GT {\n\t\t\ta = append(a, \"gt\")\n\t\t} else if args.LT {\n\t\t\ta = append(a, \"lt\")\n\t\t}\n\t}\n\tif args.Ch {\n\t\ta = append(a, \"ch\")\n\t}\n\tif incr {\n\t\ta = append(a, \"incr\")\n\t}\n\tfor _, m := range args.Members {\n\t\ta = append(a, m.Score)\n\t\ta = append(a, m.Member)\n\t}\n\treturn a\n}\n\nfunc (c cmdable) ZAddArgs(ctx context.Context, key string, args ZAddArgs) *IntCmd {\n\tcmd := NewIntCmd(ctx, c.zAddArgs(key, args, false)...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZAddArgsIncr(ctx context.Context, key string, args ZAddArgs) *FloatCmd {\n\tcmd := NewFloatCmd(ctx, c.zAddArgs(key, args, true)...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZAdd Redis `ZADD key score member [score member ...]` command.\nfunc (c cmdable) ZAdd(ctx context.Context, key string, members ...Z) *IntCmd {\n\treturn c.ZAddArgs(ctx, key, ZAddArgs{\n\t\tMembers: members,\n\t})\n}\n\n// ZAddLT Redis `ZADD key LT score member [score member ...]` command.\nfunc (c cmdable) ZAddLT(ctx context.Context, key string, members ...Z) *IntCmd {\n\treturn c.ZAddArgs(ctx, key, ZAddArgs{\n\t\tLT:      true,\n\t\tMembers: members,\n\t})\n}\n\n// ZAddGT Redis `ZADD key GT score member [score member ...]` command.\nfunc (c cmdable) ZAddGT(ctx context.Context, key string, members ...Z) *IntCmd {\n\treturn c.ZAddArgs(ctx, key, ZAddArgs{\n\t\tGT:      true,\n\t\tMembers: members,\n\t})\n}\n\n// ZAddNX Redis `ZADD key NX score member [score member ...]` command.\nfunc (c cmdable) ZAddNX(ctx context.Context, key string, members ...Z) *IntCmd {\n\treturn c.ZAddArgs(ctx, key, ZAddArgs{\n\t\tNX:      true,\n\t\tMembers: members,\n\t})\n}\n\n// ZAddXX Redis `ZADD key XX score member [score member ...]` command.\nfunc (c cmdable) ZAddXX(ctx context.Context, key string, members ...Z) *IntCmd {\n\treturn c.ZAddArgs(ctx, key, ZAddArgs{\n\t\tXX:      true,\n\t\tMembers: members,\n\t})\n}\n\nfunc (c cmdable) ZCard(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"zcard\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZCount(ctx context.Context, key, min, max string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"zcount\", key, min, max)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZLexCount(ctx context.Context, key, min, max string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"zlexcount\", key, min, max)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZIncrBy(ctx context.Context, key string, increment float64, member string) *FloatCmd {\n\tcmd := NewFloatCmd(ctx, \"zincrby\", key, increment, member)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZInterStore(ctx context.Context, destination string, store *ZStore) *IntCmd {\n\targs := make([]interface{}, 0, 3+store.len())\n\targs = append(args, \"zinterstore\", destination, len(store.Keys))\n\targs = store.appendArgs(args)\n\tcmd := NewIntCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(3)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZInter(ctx context.Context, store *ZStore) *StringSliceCmd {\n\targs := make([]interface{}, 0, 2+store.len())\n\targs = append(args, \"zinter\", len(store.Keys))\n\targs = store.appendArgs(args)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZInterWithScores(ctx context.Context, store *ZStore) *ZSliceCmd {\n\targs := make([]interface{}, 0, 3+store.len())\n\targs = append(args, \"zinter\", len(store.Keys))\n\targs = store.appendArgs(args)\n\targs = append(args, \"withscores\")\n\tcmd := NewZSliceCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd {\n\tnumKeys := len(keys)\n\targs := make([]interface{}, 4+numKeys)\n\targs[0] = \"zintercard\"\n\targs[1] = numKeys\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\targs[2+numKeys] = \"limit\"\n\targs[3+numKeys] = limit\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZMPop Pops one or more elements with the highest or lowest score from the first non-empty sorted set key from the list of provided key names.\n// direction: \"max\" (highest score) or \"min\" (lowest score), count: > 0\n// example: client.ZMPop(ctx, \"max\", 5, \"set1\", \"set2\")\nfunc (c cmdable) ZMPop(ctx context.Context, order string, count int64, keys ...string) *ZSliceWithKeyCmd {\n\targs := make([]interface{}, 2+len(keys), 5+len(keys))\n\targs[0] = \"zmpop\"\n\targs[1] = len(keys)\n\tfor i, key := range keys {\n\t\targs[2+i] = key\n\t}\n\targs = append(args, strings.ToLower(order), \"count\", count)\n\tcmd := NewZSliceWithKeyCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZMScore(ctx context.Context, key string, members ...string) *FloatSliceCmd {\n\targs := make([]interface{}, 2+len(members))\n\targs[0] = \"zmscore\"\n\targs[1] = key\n\tfor i, member := range members {\n\t\targs[2+i] = member\n\t}\n\tcmd := NewFloatSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZPopMax(ctx context.Context, key string, count ...int64) *ZSliceCmd {\n\targs := []interface{}{\n\t\t\"zpopmax\",\n\t\tkey,\n\t}\n\n\tswitch len(count) {\n\tcase 0:\n\t\tbreak\n\tcase 1:\n\t\targs = append(args, count[0])\n\tdefault:\n\t\tcmd := NewZSliceCmd(ctx)\n\t\tcmd.SetErr(errors.New(\"too many arguments\"))\n\t\treturn cmd\n\t}\n\n\tcmd := NewZSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZPopMin(ctx context.Context, key string, count ...int64) *ZSliceCmd {\n\targs := []interface{}{\n\t\t\"zpopmin\",\n\t\tkey,\n\t}\n\n\tswitch len(count) {\n\tcase 0:\n\t\tbreak\n\tcase 1:\n\t\targs = append(args, count[0])\n\tdefault:\n\t\tcmd := NewZSliceCmd(ctx)\n\t\tcmd.SetErr(errors.New(\"too many arguments\"))\n\t\treturn cmd\n\t}\n\n\tcmd := NewZSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRangeArgs is all the options of the ZRange command.\n// In version> 6.2.0, you can replace the(cmd):\n//\n//\tZREVRANGE,\n//\tZRANGEBYSCORE,\n//\tZREVRANGEBYSCORE,\n//\tZRANGEBYLEX,\n//\tZREVRANGEBYLEX.\n//\n// Please pay attention to your redis-server version.\n//\n// Rev, ByScore, ByLex and Offset+Count options require redis-server 6.2.0 and higher.\ntype ZRangeArgs struct {\n\tKey string\n\n\t// When the ByScore option is provided, the open interval(exclusive) can be set.\n\t// By default, the score intervals specified by <Start> and <Stop> are closed (inclusive).\n\t// It is similar to the deprecated(6.2.0+) ZRangeByScore command.\n\t// For example:\n\t//\t\tZRangeArgs{\n\t//\t\t\tKey: \t\t\t\t\"example-key\",\n\t//\t \t\tStart: \t\t\t\t\"(3\",\n\t//\t \t\tStop: \t\t\t\t8,\n\t//\t\t\tByScore:\t\t\ttrue,\n\t//\t \t}\n\t// \t \tcmd: \"ZRange example-key (3 8 ByScore\"  (3 < score <= 8).\n\t//\n\t// For the ByLex option, it is similar to the deprecated(6.2.0+) ZRangeByLex command.\n\t// You can set the <Start> and <Stop> options as follows:\n\t//\t\tZRangeArgs{\n\t//\t\t\tKey: \t\t\t\t\"example-key\",\n\t//\t \t\tStart: \t\t\t\t\"[abc\",\n\t//\t \t\tStop: \t\t\t\t\"(def\",\n\t//\t\t\tByLex:\t\t\t\ttrue,\n\t//\t \t}\n\t//\t\tcmd: \"ZRange example-key [abc (def ByLex\"\n\t//\n\t// For normal cases (ByScore==false && ByLex==false), <Start> and <Stop> should be set to the index range (int).\n\t// You can read the documentation for more information: https://redis.io/commands/zrange\n\tStart interface{}\n\tStop  interface{}\n\n\t// The ByScore and ByLex options are mutually exclusive.\n\tByScore bool\n\tByLex   bool\n\n\tRev bool\n\n\t// limit offset count.\n\tOffset int64\n\tCount  int64\n}\n\nfunc (z ZRangeArgs) appendArgs(args []interface{}) []interface{} {\n\t// For Rev+ByScore/ByLex, we need to adjust the position of <Start> and <Stop>.\n\tif z.Rev && (z.ByScore || z.ByLex) {\n\t\targs = append(args, z.Key, z.Stop, z.Start)\n\t} else {\n\t\targs = append(args, z.Key, z.Start, z.Stop)\n\t}\n\n\tif z.ByScore {\n\t\targs = append(args, \"byscore\")\n\t} else if z.ByLex {\n\t\targs = append(args, \"bylex\")\n\t}\n\tif z.Rev {\n\t\targs = append(args, \"rev\")\n\t}\n\tif z.Offset != 0 || z.Count != 0 {\n\t\targs = append(args, \"limit\", z.Offset, z.Count)\n\t}\n\treturn args\n}\n\nfunc (c cmdable) ZRangeArgs(ctx context.Context, z ZRangeArgs) *StringSliceCmd {\n\targs := make([]interface{}, 0, 9)\n\targs = append(args, \"zrange\")\n\targs = z.appendArgs(args)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRangeArgsWithScores(ctx context.Context, z ZRangeArgs) *ZSliceCmd {\n\targs := make([]interface{}, 0, 10)\n\targs = append(args, \"zrange\")\n\targs = z.appendArgs(args)\n\targs = append(args, \"withscores\")\n\tcmd := NewZSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd {\n\treturn c.ZRangeArgs(ctx, ZRangeArgs{\n\t\tKey:   key,\n\t\tStart: start,\n\t\tStop:  stop,\n\t})\n}\n\nfunc (c cmdable) ZRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd {\n\treturn c.ZRangeArgsWithScores(ctx, ZRangeArgs{\n\t\tKey:   key,\n\t\tStart: start,\n\t\tStop:  stop,\n\t})\n}\n\ntype ZRangeBy struct {\n\tMin, Max      string\n\tOffset, Count int64\n}\n\nfunc (c cmdable) zRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy, withScores bool) *StringSliceCmd {\n\targs := []interface{}{zcmd, key, opt.Min, opt.Max}\n\tif withScores {\n\t\targs = append(args, \"withscores\")\n\t}\n\tif opt.Offset != 0 || opt.Count != 0 {\n\t\targs = append(\n\t\t\targs,\n\t\t\t\"limit\",\n\t\t\topt.Offset,\n\t\t\topt.Count,\n\t\t)\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRangeByScore returns members in a sorted set within a range of scores.\n//\n// Deprecated: Use ZRangeArgs with ByScore option instead as of Redis 6.2.0.\nfunc (c cmdable) ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {\n\treturn c.zRangeBy(ctx, \"zrangebyscore\", key, opt, false)\n}\n\n// ZRangeByLex returns members in a sorted set within a lexicographical range.\n//\n// Deprecated: Use ZRangeArgs with ByLex option instead as of Redis 6.2.0.\nfunc (c cmdable) ZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {\n\treturn c.zRangeBy(ctx, \"zrangebylex\", key, opt, false)\n}\n\nfunc (c cmdable) ZRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd {\n\targs := []interface{}{\"zrangebyscore\", key, opt.Min, opt.Max, \"withscores\"}\n\tif opt.Offset != 0 || opt.Count != 0 {\n\t\targs = append(\n\t\t\targs,\n\t\t\t\"limit\",\n\t\t\topt.Offset,\n\t\t\topt.Count,\n\t\t)\n\t}\n\tcmd := NewZSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRangeStore(ctx context.Context, dst string, z ZRangeArgs) *IntCmd {\n\targs := make([]interface{}, 0, 10)\n\targs = append(args, \"zrangestore\", dst)\n\targs = z.appendArgs(args)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRank(ctx context.Context, key, member string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"zrank\", key, member)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRankWithScore according to the Redis documentation, if member does not exist\n// in the sorted set or key does not exist, it will return a redis.Nil error.\nfunc (c cmdable) ZRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd {\n\tcmd := NewRankWithScoreCmd(ctx, \"zrank\", key, member, \"withscore\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRem(ctx context.Context, key string, members ...interface{}) *IntCmd {\n\targs := make([]interface{}, 2, 2+len(members))\n\targs[0] = \"zrem\"\n\targs[1] = key\n\targs = appendArgs(args, members)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRemRangeByRank(ctx context.Context, key string, start, stop int64) *IntCmd {\n\tcmd := NewIntCmd(\n\t\tctx,\n\t\t\"zremrangebyrank\",\n\t\tkey,\n\t\tstart,\n\t\tstop,\n\t)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRemRangeByScore(ctx context.Context, key, min, max string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"zremrangebyscore\", key, min, max)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRemRangeByLex(ctx context.Context, key, min, max string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"zremrangebylex\", key, min, max)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRevRange returns members in a sorted set within a range of indexes in reverse order.\n//\n// Deprecated: Use ZRangeArgs with Rev option instead as of Redis 6.2.0.\nfunc (c cmdable) ZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"zrevrange\", key, start, stop)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRevRangeWithScores according to the Redis documentation, if member does not exist\n// in the sorted set or key does not exist, it will return a redis.Nil error.\nfunc (c cmdable) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd {\n\tcmd := NewZSliceCmd(ctx, \"zrevrange\", key, start, stop, \"withscores\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) zRevRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy) *StringSliceCmd {\n\targs := []interface{}{zcmd, key, opt.Max, opt.Min}\n\tif opt.Offset != 0 || opt.Count != 0 {\n\t\targs = append(\n\t\t\targs,\n\t\t\t\"limit\",\n\t\t\topt.Offset,\n\t\t\topt.Count,\n\t\t)\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRevRangeByScore returns members in a sorted set within a range of scores in reverse order.\n//\n// Deprecated: Use ZRangeArgs with Rev and ByScore options instead as of Redis 6.2.0.\nfunc (c cmdable) ZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {\n\treturn c.zRevRangeBy(ctx, \"zrevrangebyscore\", key, opt)\n}\n\n// ZRevRangeByLex returns members in a sorted set within a lexicographical range in reverse order.\n//\n// Deprecated: Use ZRangeArgs with Rev and ByLex options instead as of Redis 6.2.0.\nfunc (c cmdable) ZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {\n\treturn c.zRevRangeBy(ctx, \"zrevrangebylex\", key, opt)\n}\n\nfunc (c cmdable) ZRevRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd {\n\targs := []interface{}{\"zrevrangebyscore\", key, opt.Max, opt.Min, \"withscores\"}\n\tif opt.Offset != 0 || opt.Count != 0 {\n\t\targs = append(\n\t\t\targs,\n\t\t\t\"limit\",\n\t\t\topt.Offset,\n\t\t\topt.Count,\n\t\t)\n\t}\n\tcmd := NewZSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRevRank(ctx context.Context, key, member string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"zrevrank\", key, member)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZRevRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd {\n\tcmd := NewRankWithScoreCmd(ctx, \"zrevrank\", key, member, \"withscore\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZScore(ctx context.Context, key, member string) *FloatCmd {\n\tcmd := NewFloatCmd(ctx, \"zscore\", key, member)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZUnion(ctx context.Context, store ZStore) *StringSliceCmd {\n\targs := make([]interface{}, 0, 2+store.len())\n\targs = append(args, \"zunion\", len(store.Keys))\n\targs = store.appendArgs(args)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZUnionWithScores(ctx context.Context, store ZStore) *ZSliceCmd {\n\targs := make([]interface{}, 0, 3+store.len())\n\targs = append(args, \"zunion\", len(store.Keys))\n\targs = store.appendArgs(args)\n\targs = append(args, \"withscores\")\n\tcmd := NewZSliceCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZUnionStore(ctx context.Context, dest string, store *ZStore) *IntCmd {\n\targs := make([]interface{}, 0, 3+store.len())\n\targs = append(args, \"zunionstore\", dest, len(store.Keys))\n\targs = store.appendArgs(args)\n\tcmd := NewIntCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(3)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRandMember redis-server version >= 6.2.0.\nfunc (c cmdable) ZRandMember(ctx context.Context, key string, count int) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"zrandmember\", key, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZRandMemberWithScores redis-server version >= 6.2.0.\nfunc (c cmdable) ZRandMemberWithScores(ctx context.Context, key string, count int) *ZSliceCmd {\n\tcmd := NewZSliceCmd(ctx, \"zrandmember\", key, count, \"withscores\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZDiff redis-server version >= 6.2.0.\nfunc (c cmdable) ZDiff(ctx context.Context, keys ...string) *StringSliceCmd {\n\targs := make([]interface{}, 2+len(keys))\n\targs[0] = \"zdiff\"\n\targs[1] = len(keys)\n\tfor i, key := range keys {\n\t\targs[i+2] = key\n\t}\n\n\tcmd := NewStringSliceCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZDiffWithScores redis-server version >= 6.2.0.\nfunc (c cmdable) ZDiffWithScores(ctx context.Context, keys ...string) *ZSliceCmd {\n\targs := make([]interface{}, 3+len(keys))\n\targs[0] = \"zdiff\"\n\targs[1] = len(keys)\n\tfor i, key := range keys {\n\t\targs[i+2] = key\n\t}\n\targs[len(keys)+2] = \"withscores\"\n\n\tcmd := NewZSliceCmd(ctx, args...)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// ZDiffStore redis-server version >=6.2.0.\nfunc (c cmdable) ZDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd {\n\targs := make([]interface{}, 0, 3+len(keys))\n\targs = append(args, \"zdiffstore\", destination, len(keys))\n\tfor _, key := range keys {\n\t\targs = append(args, key)\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) ZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {\n\targs := []interface{}{\"zscan\", key, cursor}\n\tif match != \"\" {\n\t\targs = append(args, \"match\", match)\n\t}\n\tif count > 0 {\n\t\targs = append(args, \"count\", count)\n\t}\n\tcmd := NewScanCmd(ctx, c, args...)\n\tif hashtag.Present(match) {\n\t\tcmd.SetFirstKeyPos(4)\n\t}\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Z represents sorted set member.\ntype Z struct {\n\tScore  float64\n\tMember interface{}\n}\n\n// ZWithKey represents sorted set member including the name of the key where it was popped.\ntype ZWithKey struct {\n\tZ\n\tKey string\n}\n\n// ZStore is used as an arg to ZInter/ZInterStore and ZUnion/ZUnionStore.\ntype ZStore struct {\n\tKeys    []string\n\tWeights []float64\n\t// Can be SUM, MIN or MAX.\n\tAggregate string\n}\n\nfunc (z ZStore) len() (n int) {\n\tn = len(z.Keys)\n\tif len(z.Weights) > 0 {\n\t\tn += 1 + len(z.Weights)\n\t}\n\tif z.Aggregate != \"\" {\n\t\tn += 2\n\t}\n\treturn n\n}\n\nfunc (z ZStore) appendArgs(args []interface{}) []interface{} {\n\tfor _, key := range z.Keys {\n\t\targs = append(args, key)\n\t}\n\tif len(z.Weights) > 0 {\n\t\targs = append(args, \"weights\")\n\t\tfor _, weights := range z.Weights {\n\t\t\targs = append(args, weights)\n\t\t}\n\t}\n\tif z.Aggregate != \"\" {\n\t\targs = append(args, \"aggregate\", z.Aggregate)\n\t}\n\treturn args\n}\n"
  },
  {
    "path": "stream_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/internal/otel\"\n)\n\ntype StreamCmdable interface {\n\tXAdd(ctx context.Context, a *XAddArgs) *StringCmd\n\tXAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd\n\tXDel(ctx context.Context, stream string, ids ...string) *IntCmd\n\tXDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd\n\tXLen(ctx context.Context, stream string) *IntCmd\n\tXRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd\n\tXRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd\n\tXRevRange(ctx context.Context, stream string, start, stop string) *XMessageSliceCmd\n\tXRevRangeN(ctx context.Context, stream string, start, stop string, count int64) *XMessageSliceCmd\n\tXRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd\n\tXReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd\n\tXGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd\n\tXGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd\n\tXGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd\n\tXGroupDestroy(ctx context.Context, stream, group string) *IntCmd\n\tXGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *IntCmd\n\tXGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd\n\tXReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd\n\tXAck(ctx context.Context, stream, group string, ids ...string) *IntCmd\n\tXPending(ctx context.Context, stream, group string) *XPendingCmd\n\tXPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd\n\tXClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd\n\tXClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd\n\tXAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimCmd\n\tXAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd\n\tXTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd\n\tXTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd\n\tXTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd\n\tXTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd\n\tXTrimMinID(ctx context.Context, key string, minID string) *IntCmd\n\tXTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd\n\tXTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd\n\tXTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd\n\tXInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd\n\tXInfoStream(ctx context.Context, key string) *XInfoStreamCmd\n\tXInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd\n\tXInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd\n\tXCfgSet(ctx context.Context, a *XCfgSetArgs) *StatusCmd\n}\n\n// XAddArgs accepts values in the following formats:\n//   - XAddArgs.Values = []interface{}{\"key1\", \"value1\", \"key2\", \"value2\"}\n//   - XAddArgs.Values = []string(\"key1\", \"value1\", \"key2\", \"value2\")\n//   - XAddArgs.Values = map[string]interface{}{\"key1\": \"value1\", \"key2\": \"value2\"}\n//\n// Note that map will not preserve the order of key-value pairs.\n// MaxLen/MaxLenApprox and MinID are in conflict, only one of them can be used.\n//\n// For idempotent production (at-most-once production):\n//   - ProducerID: A unique identifier for the producer (required for both IDMP and IDMPAUTO)\n//   - IdempotentID: A unique identifier for the message (used with IDMP)\n//   - IdempotentAuto: If true, Redis will auto-generate an idempotent ID based on message content (IDMPAUTO)\n//\n// ProducerID and IdempotentID are mutually exclusive with IdempotentAuto.\n// When using idempotent production, ID must be \"*\" or empty.\ntype XAddArgs struct {\n\tStream     string\n\tNoMkStream bool\n\tMaxLen     int64 // MAXLEN N\n\tMinID      string\n\t// Approx causes MaxLen and MinID to use \"~\" matcher (instead of \"=\").\n\tApprox         bool\n\tLimit          int64\n\tMode           string\n\tID             string\n\tValues         interface{}\n\tProducerID     string // Producer ID for idempotent production (IDMP or IDMPAUTO)\n\tIdempotentID   string // Idempotent ID for IDMP\n\tIdempotentAuto bool   // Use IDMPAUTO to auto-generate idempotent ID based on content\n}\n\nfunc (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd {\n\targs := make([]interface{}, 0, 15)\n\targs = append(args, \"xadd\", a.Stream)\n\tif a.NoMkStream {\n\t\targs = append(args, \"nomkstream\")\n\t}\n\n\tif a.Mode != \"\" {\n\t\targs = append(args, a.Mode)\n\t}\n\n\tif a.ProducerID != \"\" {\n\t\tif a.IdempotentAuto {\n\t\t\t// IDMPAUTO pid\n\t\t\targs = append(args, \"idmpauto\", a.ProducerID)\n\t\t} else if a.IdempotentID != \"\" {\n\t\t\t// IDMP pid iid\n\t\t\targs = append(args, \"idmp\", a.ProducerID, a.IdempotentID)\n\t\t}\n\t}\n\n\tswitch {\n\tcase a.MaxLen > 0:\n\t\tif a.Approx {\n\t\t\targs = append(args, \"maxlen\", \"~\", a.MaxLen)\n\t\t} else {\n\t\t\targs = append(args, \"maxlen\", \"=\", a.MaxLen)\n\t\t}\n\tcase a.MinID != \"\":\n\t\tif a.Approx {\n\t\t\targs = append(args, \"minid\", \"~\", a.MinID)\n\t\t} else {\n\t\t\targs = append(args, \"minid\", \"=\", a.MinID)\n\t\t}\n\t}\n\tif a.Limit > 0 {\n\t\targs = append(args, \"limit\", a.Limit)\n\t}\n\n\tif a.ID != \"\" {\n\t\targs = append(args, a.ID)\n\t} else {\n\t\targs = append(args, \"*\")\n\t}\n\targs = appendArg(args, a.Values)\n\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd {\n\targs := []interface{}{\"xackdel\", stream, group, mode, \"ids\", len(ids)}\n\tfor _, id := range ids {\n\t\targs = append(args, id)\n\t}\n\tcmd := NewSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd {\n\targs := []interface{}{\"xdel\", stream}\n\tfor _, id := range ids {\n\t\targs = append(args, id)\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd {\n\targs := []interface{}{\"xdelex\", stream, mode, \"ids\", len(ids)}\n\tfor _, id := range ids {\n\t\targs = append(args, id)\n\t}\n\tcmd := NewSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XLen(ctx context.Context, stream string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"xlen\", stream)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd {\n\tcmd := NewXMessageSliceCmd(ctx, \"xrange\", stream, start, stop)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd {\n\tcmd := NewXMessageSliceCmd(ctx, \"xrange\", stream, start, stop, \"count\", count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XRevRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd {\n\tcmd := NewXMessageSliceCmd(ctx, \"xrevrange\", stream, start, stop)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XRevRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd {\n\tcmd := NewXMessageSliceCmd(ctx, \"xrevrange\", stream, start, stop, \"count\", count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype XReadArgs struct {\n\tStreams []string // list of streams and ids, e.g. stream1 stream2 id1 id2\n\tCount   int64\n\tBlock   time.Duration\n\tID      string\n}\n\nfunc (c cmdable) XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd {\n\targs := make([]interface{}, 0, 2*len(a.Streams)+6)\n\targs = append(args, \"xread\")\n\n\tkeyPos := int8(1)\n\tif a.Count > 0 {\n\t\targs = append(args, \"count\")\n\t\targs = append(args, a.Count)\n\t\tkeyPos += 2\n\t}\n\tif a.Block >= 0 {\n\t\targs = append(args, \"block\")\n\t\targs = append(args, int64(a.Block/time.Millisecond))\n\t\tkeyPos += 2\n\t}\n\targs = append(args, \"streams\")\n\tkeyPos++\n\tfor _, s := range a.Streams {\n\t\targs = append(args, s)\n\t}\n\tif a.ID != \"\" {\n\t\tfor range a.Streams {\n\t\t\targs = append(args, a.ID)\n\t\t}\n\t}\n\n\tcmd := NewXStreamSliceCmd(ctx, args...)\n\tif a.Block >= 0 {\n\t\tcmd.setReadTimeout(a.Block)\n\t}\n\tcmd.SetFirstKeyPos(keyPos)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd {\n\treturn c.XRead(ctx, &XReadArgs{\n\t\tStreams: streams,\n\t\tBlock:   -1,\n\t})\n}\n\nfunc (c cmdable) XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"xgroup\", \"create\", stream, group, start)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"xgroup\", \"create\", stream, group, start, \"mkstream\")\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"xgroup\", \"setid\", stream, group, start)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XGroupDestroy(ctx context.Context, stream, group string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"xgroup\", \"destroy\", stream, group)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"xgroup\", \"createconsumer\", stream, group, consumer)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"xgroup\", \"delconsumer\", stream, group, consumer)\n\tcmd.SetFirstKeyPos(2)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype XReadGroupArgs struct {\n\tGroup    string\n\tConsumer string\n\tStreams  []string // list of streams and ids, e.g. stream1 stream2 id1 id2\n\tCount    int64\n\tBlock    time.Duration\n\tNoAck    bool\n\tClaim    time.Duration // Claim idle pending entries older than this duration\n}\n\nfunc (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd {\n\targs := make([]interface{}, 0, 10+len(a.Streams))\n\targs = append(args, \"xreadgroup\", \"group\", a.Group, a.Consumer)\n\n\tkeyPos := int8(4)\n\tif a.Count > 0 {\n\t\targs = append(args, \"count\", a.Count)\n\t\tkeyPos += 2\n\t}\n\tif a.Block >= 0 {\n\t\targs = append(args, \"block\", int64(a.Block/time.Millisecond))\n\t\tkeyPos += 2\n\t}\n\tif a.NoAck {\n\t\targs = append(args, \"noack\")\n\t\tkeyPos++\n\t}\n\tif a.Claim > 0 {\n\t\targs = append(args, \"claim\", int64(a.Claim/time.Millisecond))\n\t\tkeyPos += 2\n\t}\n\targs = append(args, \"streams\")\n\tkeyPos++\n\tfor _, s := range a.Streams {\n\t\targs = append(args, s)\n\t}\n\n\tcmd := NewXStreamSliceCmd(ctx, args...)\n\tif a.Block >= 0 {\n\t\tcmd.setReadTimeout(a.Block)\n\t}\n\tcmd.SetFirstKeyPos(keyPos)\n\t_ = c(ctx, cmd)\n\n\t// Record stream lag for each message (if command succeeded)\n\tif cmd.Err() == nil {\n\t\tstreams := cmd.Val()\n\t\tfor _, stream := range streams {\n\t\t\tfor _, msg := range stream.Messages {\n\t\t\t\t// Parse message ID to extract timestamp (format: \"millisecondsTime-sequenceNumber\")\n\t\t\t\tif parts := strings.SplitN(msg.ID, \"-\", 2); len(parts) == 2 {\n\t\t\t\t\tif timestampMs, err := strconv.ParseInt(parts[0], 10, 64); err == nil {\n\t\t\t\t\t\t// Calculate lag (time since message was created)\n\t\t\t\t\t\tmessageTime := time.Unix(0, timestampMs*int64(time.Millisecond))\n\t\t\t\t\t\tlag := time.Since(messageTime)\n\t\t\t\t\t\t// Record lag metric\n\t\t\t\t\t\totel.RecordStreamLag(ctx, lag, nil, stream.Stream, a.Group, a.Consumer)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn cmd\n}\n\nfunc (c cmdable) XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd {\n\targs := []interface{}{\"xack\", stream, group}\n\tfor _, id := range ids {\n\t\targs = append(args, id)\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XPending(ctx context.Context, stream, group string) *XPendingCmd {\n\tcmd := NewXPendingCmd(ctx, \"xpending\", stream, group)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype XPendingExtArgs struct {\n\tStream   string\n\tGroup    string\n\tIdle     time.Duration\n\tStart    string\n\tEnd      string\n\tCount    int64\n\tConsumer string\n}\n\nfunc (c cmdable) XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd {\n\targs := make([]interface{}, 0, 9)\n\targs = append(args, \"xpending\", a.Stream, a.Group)\n\tif a.Idle != 0 {\n\t\targs = append(args, \"idle\", formatMs(ctx, a.Idle))\n\t}\n\targs = append(args, a.Start, a.End, a.Count)\n\tif a.Consumer != \"\" {\n\t\targs = append(args, a.Consumer)\n\t}\n\tcmd := NewXPendingExtCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype XAutoClaimArgs struct {\n\tStream   string\n\tGroup    string\n\tMinIdle  time.Duration\n\tStart    string\n\tCount    int64\n\tConsumer string\n}\n\nfunc (c cmdable) XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimCmd {\n\targs := xAutoClaimArgs(ctx, a)\n\tcmd := NewXAutoClaimCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd {\n\targs := xAutoClaimArgs(ctx, a)\n\targs = append(args, \"justid\")\n\tcmd := NewXAutoClaimJustIDCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc xAutoClaimArgs(ctx context.Context, a *XAutoClaimArgs) []interface{} {\n\targs := make([]interface{}, 0, 8)\n\targs = append(args, \"xautoclaim\", a.Stream, a.Group, a.Consumer, formatMs(ctx, a.MinIdle), a.Start)\n\tif a.Count > 0 {\n\t\targs = append(args, \"count\", a.Count)\n\t}\n\treturn args\n}\n\ntype XClaimArgs struct {\n\tStream   string\n\tGroup    string\n\tConsumer string\n\tMinIdle  time.Duration\n\tMessages []string\n}\n\nfunc (c cmdable) XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd {\n\targs := xClaimArgs(a)\n\tcmd := NewXMessageSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd {\n\targs := xClaimArgs(a)\n\targs = append(args, \"justid\")\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc xClaimArgs(a *XClaimArgs) []interface{} {\n\targs := make([]interface{}, 0, 5+len(a.Messages))\n\targs = append(args,\n\t\t\"xclaim\",\n\t\ta.Stream,\n\t\ta.Group, a.Consumer,\n\t\tint64(a.MinIdle/time.Millisecond))\n\tfor _, id := range a.Messages {\n\t\targs = append(args, id)\n\t}\n\treturn args\n}\n\n// TODO: refactor xTrim, xTrimMode and the wrappers over the functions\n\n// xTrim If approx is true, add the \"~\" parameter, otherwise it is the default \"=\" (redis default).\n// example:\n//\n//\tXTRIM key MAXLEN/MINID threshold LIMIT limit.\n//\tXTRIM key MAXLEN/MINID ~ threshold LIMIT limit.\n//\n// The redis-server version is lower than 6.2, please set limit to 0.\nfunc (c cmdable) xTrim(\n\tctx context.Context, key, strategy string,\n\tapprox bool, threshold interface{}, limit int64,\n) *IntCmd {\n\targs := make([]interface{}, 0, 7)\n\targs = append(args, \"xtrim\", key, strategy)\n\tif approx {\n\t\targs = append(args, \"~\")\n\t} else {\n\t\targs = append(args, \"=\")\n\t}\n\targs = append(args, threshold)\n\tif limit > 0 {\n\t\targs = append(args, \"limit\", limit)\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// XTrimMaxLen No `~` rules are used, `limit` cannot be used.\n// cmd: XTRIM key MAXLEN maxLen\nfunc (c cmdable) XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd {\n\treturn c.xTrim(ctx, key, \"maxlen\", false, maxLen, 0)\n}\n\nfunc (c cmdable) XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd {\n\treturn c.xTrim(ctx, key, \"maxlen\", true, maxLen, limit)\n}\n\nfunc (c cmdable) XTrimMinID(ctx context.Context, key string, minID string) *IntCmd {\n\treturn c.xTrim(ctx, key, \"minid\", false, minID, 0)\n}\n\nfunc (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd {\n\treturn c.xTrim(ctx, key, \"minid\", true, minID, limit)\n}\n\nfunc (c cmdable) xTrimMode(\n\tctx context.Context, key, strategy string,\n\tapprox bool, threshold interface{}, limit int64,\n\tmode string,\n) *IntCmd {\n\targs := make([]interface{}, 0, 7)\n\targs = append(args, \"xtrim\", key, strategy)\n\tif approx {\n\t\targs = append(args, \"~\")\n\t} else {\n\t\targs = append(args, \"=\")\n\t}\n\targs = append(args, threshold)\n\tif limit > 0 {\n\t\targs = append(args, \"limit\", limit)\n\t}\n\targs = append(args, mode)\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd {\n\treturn c.xTrimMode(ctx, key, \"maxlen\", false, maxLen, 0, mode)\n}\n\nfunc (c cmdable) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd {\n\treturn c.xTrimMode(ctx, key, \"maxlen\", true, maxLen, limit, mode)\n}\n\nfunc (c cmdable) XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd {\n\treturn c.xTrimMode(ctx, key, \"minid\", false, minID, 0, mode)\n}\n\nfunc (c cmdable) XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd {\n\treturn c.xTrimMode(ctx, key, \"minid\", true, minID, limit, mode)\n}\n\nfunc (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd {\n\tcmd := NewXInfoConsumersCmd(ctx, key, group)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd {\n\tcmd := NewXInfoGroupsCmd(ctx, key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) XInfoStream(ctx context.Context, key string) *XInfoStreamCmd {\n\tcmd := NewXInfoStreamCmd(ctx, key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// XInfoStreamFull XINFO STREAM FULL [COUNT count]\n// redis-server >= 6.0.\nfunc (c cmdable) XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd {\n\targs := make([]interface{}, 0, 6)\n\targs = append(args, \"xinfo\", \"stream\", key, \"full\")\n\tif count > 0 {\n\t\targs = append(args, \"count\", count)\n\t}\n\tcmd := NewXInfoStreamFullCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// XCfgSetArgs represents the arguments for the XCFGSET command.\n// Duration is the duration, in seconds, that Redis keeps each idempotent ID.\n// MaxSize is the maximum number of most recent idempotent IDs that Redis keeps for each producer ID.\ntype XCfgSetArgs struct {\n\tStream   string\n\tDuration int64\n\tMaxSize  int64\n}\n\n// XCfgSet sets the idempotent production configuration for a stream.\n// XCFGSET key [IDMP-DURATION duration] [IDMP-MAXSIZE maxsize]\nfunc (c cmdable) XCfgSet(ctx context.Context, a *XCfgSetArgs) *StatusCmd {\n\targs := make([]interface{}, 0, 6)\n\targs = append(args, \"xcfgset\", a.Stream)\n\tif a.Duration > 0 {\n\t\targs = append(args, \"idmp-duration\", a.Duration)\n\t}\n\tif a.MaxSize > 0 {\n\t\targs = append(args, \"idmp-maxsize\", a.MaxSize)\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "string_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n)\n\ntype StringCmdable interface {\n\tAppend(ctx context.Context, key, value string) *IntCmd\n\tDecr(ctx context.Context, key string) *IntCmd\n\tDecrBy(ctx context.Context, key string, decrement int64) *IntCmd\n\tDelExArgs(ctx context.Context, key string, a DelExArgs) *IntCmd\n\tDigest(ctx context.Context, key string) *DigestCmd\n\tGet(ctx context.Context, key string) *StringCmd\n\tGetRange(ctx context.Context, key string, start, end int64) *StringCmd\n\tGetSet(ctx context.Context, key string, value interface{}) *StringCmd\n\tGetEx(ctx context.Context, key string, expiration time.Duration) *StringCmd\n\tGetDel(ctx context.Context, key string) *StringCmd\n\tIncr(ctx context.Context, key string) *IntCmd\n\tIncrBy(ctx context.Context, key string, value int64) *IntCmd\n\tIncrByFloat(ctx context.Context, key string, value float64) *FloatCmd\n\tLCS(ctx context.Context, q *LCSQuery) *LCSCmd\n\tMGet(ctx context.Context, keys ...string) *SliceCmd\n\tMSet(ctx context.Context, values ...interface{}) *StatusCmd\n\tMSetNX(ctx context.Context, values ...interface{}) *BoolCmd\n\tMSetEX(ctx context.Context, args MSetEXArgs, values ...interface{}) *IntCmd\n\tSet(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd\n\tSetArgs(ctx context.Context, key string, value interface{}, a SetArgs) *StatusCmd\n\tSetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd\n\tSetIFEQ(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd\n\tSetIFEQGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd\n\tSetIFNE(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd\n\tSetIFNEGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd\n\tSetIFDEQ(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd\n\tSetIFDEQGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd\n\tSetIFDNE(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd\n\tSetIFDNEGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd\n\tSetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd\n\tSetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd\n\tSetRange(ctx context.Context, key string, offset int64, value string) *IntCmd\n\tStrLen(ctx context.Context, key string) *IntCmd\n}\n\nfunc (c cmdable) Append(ctx context.Context, key, value string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"append\", key, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Decr(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"decr\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) DecrBy(ctx context.Context, key string, decrement int64) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"decrby\", key, decrement)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// DelExArgs provides arguments for the DelExArgs function.\ntype DelExArgs struct {\n\t// Mode can be `IFEQ`, `IFNE`, `IFDEQ`, or `IFDNE`.\n\tMode string\n\n\t// MatchValue is used with IFEQ/IFNE modes for compare-and-delete operations.\n\t// - IFEQ: only delete if current value equals MatchValue\n\t// - IFNE: only delete if current value does not equal MatchValue\n\tMatchValue interface{}\n\n\t// MatchDigest is used with IFDEQ/IFDNE modes for digest-based compare-and-delete.\n\t// - IFDEQ: only delete if current value's digest equals MatchDigest\n\t// - IFDNE: only delete if current value's digest does not equal MatchDigest\n\t//\n\t// The digest is a uint64 xxh3 hash value.\n\t//\n\t// For examples of client-side digest generation, see:\n\t// example/digest-optimistic-locking/\n\tMatchDigest uint64\n}\n\n// DelExArgs Redis `DELEX key [IFEQ|IFNE|IFDEQ|IFDNE] match-value` command.\n// Compare-and-delete with flexible conditions.\n//\n// Returns the number of keys that were removed (0 or 1).\n//\n// NOTE DelExArgs is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) DelExArgs(ctx context.Context, key string, a DelExArgs) *IntCmd {\n\targs := []interface{}{\"delex\", key}\n\n\tif a.Mode != \"\" {\n\t\targs = append(args, a.Mode)\n\n\t\t// Add match value/digest based on mode\n\t\tswitch a.Mode {\n\t\tcase \"ifeq\", \"IFEQ\", \"ifne\", \"IFNE\":\n\t\t\tif a.MatchValue != nil {\n\t\t\t\targs = append(args, a.MatchValue)\n\t\t\t}\n\t\tcase \"ifdeq\", \"IFDEQ\", \"ifdne\", \"IFDNE\":\n\t\t\tif a.MatchDigest != 0 {\n\t\t\t\targs = append(args, fmt.Sprintf(\"%016x\", a.MatchDigest))\n\t\t\t}\n\t\t}\n\t}\n\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Digest returns the xxh3 hash (uint64) of the specified key's value.\n//\n// The digest is a 64-bit xxh3 hash that can be used for optimistic locking\n// with SetIFDEQ, SetIFDNE, and DelExArgs commands.\n//\n// For examples of client-side digest generation and usage patterns, see:\n// example/digest-optimistic-locking/\n//\n// Redis 8.4+. See https://redis.io/commands/digest/\n//\n// NOTE Digest is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) Digest(ctx context.Context, key string) *DigestCmd {\n\tcmd := NewDigestCmd(ctx, \"digest\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Get Redis `GET key` command. It returns redis.Nil error when key does not exist.\nfunc (c cmdable) Get(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"get\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) GetRange(ctx context.Context, key string, start, end int64) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"getrange\", key, start, end)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// GetSet returns the old value stored at key and sets it to the new value.\n//\n// Deprecated: Use SetArgs with Get option instead as of Redis 6.2.0.\nfunc (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"getset\", key, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// GetEx An expiration of zero removes the TTL associated with the key (i.e. GETEX key persist).\n// Requires Redis >= 6.2.0.\nfunc (c cmdable) GetEx(ctx context.Context, key string, expiration time.Duration) *StringCmd {\n\targs := make([]interface{}, 0, 4)\n\targs = append(args, \"getex\", key)\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == 0 {\n\t\targs = append(args, \"persist\")\n\t}\n\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// GetDel redis-server version >= 6.2.0.\nfunc (c cmdable) GetDel(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"getdel\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) Incr(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"incr\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) IncrBy(ctx context.Context, key string, value int64) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"incrby\", key, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd {\n\tcmd := NewFloatCmd(ctx, \"incrbyfloat\", key, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype SetCondition string\n\nconst (\n\t// NX only set the keys and their expiration if none exist\n\tNX SetCondition = \"NX\"\n\t// XX only set the keys and their expiration if all already exist\n\tXX SetCondition = \"XX\"\n)\n\ntype ExpirationMode string\n\nconst (\n\t// EX sets expiration in seconds\n\tEX ExpirationMode = \"EX\"\n\t// PX sets expiration in milliseconds\n\tPX ExpirationMode = \"PX\"\n\t// EXAT sets expiration as Unix timestamp in seconds\n\tEXAT ExpirationMode = \"EXAT\"\n\t// PXAT sets expiration as Unix timestamp in milliseconds\n\tPXAT ExpirationMode = \"PXAT\"\n\t// KEEPTTL keeps the existing TTL\n\tKEEPTTL ExpirationMode = \"KEEPTTL\"\n)\n\ntype ExpirationOption struct {\n\tMode  ExpirationMode\n\tValue int64\n}\n\nfunc (c cmdable) LCS(ctx context.Context, q *LCSQuery) *LCSCmd {\n\tcmd := NewLCSCmd(ctx, q)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) MGet(ctx context.Context, keys ...string) *SliceCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"mget\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// MSet is like Set but accepts multiple values:\n//   - MSet(\"key1\", \"value1\", \"key2\", \"value2\")\n//   - MSet([]string{\"key1\", \"value1\", \"key2\", \"value2\"})\n//   - MSet(map[string]interface{}{\"key1\": \"value1\", \"key2\": \"value2\"})\n//   - MSet(struct), For struct types, see HSet description.\nfunc (c cmdable) MSet(ctx context.Context, values ...interface{}) *StatusCmd {\n\targs := make([]interface{}, 1, 1+len(values))\n\targs[0] = \"mset\"\n\targs = appendArgs(args, values)\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// MSetNX is like SetNX but accepts multiple values:\n//   - MSetNX(\"key1\", \"value1\", \"key2\", \"value2\")\n//   - MSetNX([]string{\"key1\", \"value1\", \"key2\", \"value2\"})\n//   - MSetNX(map[string]interface{}{\"key1\": \"value1\", \"key2\": \"value2\"})\n//   - MSetNX(struct), For struct types, see HSet description.\nfunc (c cmdable) MSetNX(ctx context.Context, values ...interface{}) *BoolCmd {\n\targs := make([]interface{}, 1, 1+len(values))\n\targs[0] = \"msetnx\"\n\targs = appendArgs(args, values)\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype MSetEXArgs struct {\n\tCondition  SetCondition\n\tExpiration *ExpirationOption\n}\n\n// MSetEX sets the given keys to their respective values.\n// This command is an extension of the MSETNX that adds expiration and XX options.\n// Available since Redis 8.4\n// Important: When this method is used with Cluster clients, all keys\n// must be in the same hash slot, otherwise CROSSSLOT error will be returned.\n// For more information, see https://redis.io/commands/msetex\nfunc (c cmdable) MSetEX(ctx context.Context, args MSetEXArgs, values ...interface{}) *IntCmd {\n\texpandedArgs := appendArgs([]interface{}{}, values)\n\tnumkeys := len(expandedArgs) / 2\n\n\tcmdArgs := make([]interface{}, 0, 2+len(expandedArgs)+3)\n\tcmdArgs = append(cmdArgs, \"msetex\", numkeys)\n\tcmdArgs = append(cmdArgs, expandedArgs...)\n\n\tif args.Condition != \"\" {\n\t\tcmdArgs = append(cmdArgs, string(args.Condition))\n\t}\n\n\tif args.Expiration != nil {\n\t\tswitch args.Expiration.Mode {\n\t\tcase EX:\n\t\t\tcmdArgs = append(cmdArgs, \"ex\", args.Expiration.Value)\n\t\tcase PX:\n\t\t\tcmdArgs = append(cmdArgs, \"px\", args.Expiration.Value)\n\t\tcase EXAT:\n\t\t\tcmdArgs = append(cmdArgs, \"exat\", args.Expiration.Value)\n\t\tcase PXAT:\n\t\t\tcmdArgs = append(cmdArgs, \"pxat\", args.Expiration.Value)\n\t\tcase KEEPTTL:\n\t\t\tcmdArgs = append(cmdArgs, \"keepttl\")\n\t\t}\n\t}\n\n\tcmd := NewIntCmd(ctx, cmdArgs...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// Set Redis `SET key value [expiration]` command.\n// Use expiration for `SETEx`-like behavior.\n//\n// Zero expiration means the key has no expiration time.\n// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,\n// otherwise you will receive an error: (error) ERR syntax error.\nfunc (c cmdable) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd {\n\targs := make([]interface{}, 3, 5)\n\targs[0] = \"set\"\n\targs[1] = key\n\targs[2] = value\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetArgs provides arguments for the SetArgs function.\ntype SetArgs struct {\n\t// Mode can be `NX`, `XX`, `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` or empty.\n\tMode string\n\n\t// MatchValue is used with IFEQ/IFNE modes for compare-and-set operations.\n\t// - IFEQ: only set if current value equals MatchValue\n\t// - IFNE: only set if current value does not equal MatchValue\n\tMatchValue interface{}\n\n\t// MatchDigest is used with IFDEQ/IFDNE modes for digest-based compare-and-set.\n\t// - IFDEQ: only set if current value's digest equals MatchDigest\n\t// - IFDNE: only set if current value's digest does not equal MatchDigest\n\t//\n\t// The digest is a uint64 xxh3 hash value.\n\t//\n\t// For examples of client-side digest generation, see:\n\t// example/digest-optimistic-locking/\n\tMatchDigest uint64\n\n\t// Zero `TTL` or `Expiration` means that the key has no expiration time.\n\tTTL      time.Duration\n\tExpireAt time.Time\n\n\t// When Get is true, the command returns the old value stored at key, or nil when key did not exist.\n\tGet bool\n\n\t// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,\n\t// otherwise you will receive an error: (error) ERR syntax error.\n\tKeepTTL bool\n}\n\n// SetArgs supports all the options that the SET command supports.\n// It is the alternative to the Set function when you want\n// to have more control over the options.\nfunc (c cmdable) SetArgs(ctx context.Context, key string, value interface{}, a SetArgs) *StatusCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif a.KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\tif !a.ExpireAt.IsZero() {\n\t\targs = append(args, \"exat\", a.ExpireAt.Unix())\n\t}\n\tif a.TTL > 0 {\n\t\tif usePrecise(a.TTL) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, a.TTL))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, a.TTL))\n\t\t}\n\t}\n\n\tif a.Mode != \"\" {\n\t\targs = append(args, a.Mode)\n\n\t\t// Add match value/digest for CAS modes\n\t\tswitch a.Mode {\n\t\tcase \"ifeq\", \"IFEQ\", \"ifne\", \"IFNE\":\n\t\t\tif a.MatchValue != nil {\n\t\t\t\targs = append(args, a.MatchValue)\n\t\t\t}\n\t\tcase \"ifdeq\", \"IFDEQ\", \"ifdne\", \"IFDNE\":\n\t\t\tif a.MatchDigest != 0 {\n\t\t\t\targs = append(args, fmt.Sprintf(\"%016x\", a.MatchDigest))\n\t\t\t}\n\t\t}\n\t}\n\n\tif a.Get {\n\t\targs = append(args, \"get\")\n\t}\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetEx sets the value and expiration of a key.\n//\n// Deprecated: Use Set with expiration instead as of Redis 2.6.12.\nfunc (c cmdable) SetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd {\n\tcmd := NewStatusCmd(ctx, \"setex\", key, formatSec(ctx, expiration), value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetNX sets the value of a key only if the key does not exist.\n//\n// Zero expiration means the key has no expiration time.\n// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,\n// otherwise you will receive an error: (error) ERR syntax error.\nfunc (c cmdable) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd {\n\tvar cmd *BoolCmd\n\tswitch expiration {\n\tcase 0:\n\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"nx\")\n\tcase KeepTTL:\n\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"keepttl\", \"nx\")\n\tdefault:\n\t\tif usePrecise(expiration) {\n\t\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"px\", formatMs(ctx, expiration), \"nx\")\n\t\t} else {\n\t\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"ex\", formatSec(ctx, expiration), \"nx\")\n\t\t}\n\t}\n\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetXX Redis `SET key value [expiration] XX` command.\n//\n// Zero expiration means the key has no expiration time.\n// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,\n// otherwise you will receive an error: (error) ERR syntax error.\nfunc (c cmdable) SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd {\n\tvar cmd *BoolCmd\n\tswitch expiration {\n\tcase 0:\n\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"xx\")\n\tcase KeepTTL:\n\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"keepttl\", \"xx\")\n\tdefault:\n\t\tif usePrecise(expiration) {\n\t\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"px\", formatMs(ctx, expiration), \"xx\")\n\t\t} else {\n\t\t\tcmd = NewBoolCmd(ctx, \"set\", key, value, \"ex\", formatSec(ctx, expiration), \"xx\")\n\t\t}\n\t}\n\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFEQ Redis `SET key value [expiration] IFEQ match-value` command.\n// Compare-and-set: only sets the value if the current value equals matchValue.\n//\n// Returns \"OK\" on success.\n// Returns nil if the operation was aborted due to condition not matching.\n// Zero expiration means the key has no expiration time.\n//\n// NOTE SetIFEQ is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFEQ(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifeq\", matchValue)\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFEQGet Redis `SET key value [expiration] IFEQ match-value GET` command.\n// Compare-and-set with GET: only sets the value if the current value equals matchValue,\n// and returns the previous value.\n//\n// Returns the previous value on success.\n// Returns nil if the operation was aborted due to condition not matching.\n// Zero expiration means the key has no expiration time.\n//\n// NOTE SetIFEQGet is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFEQGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifeq\", matchValue, \"get\")\n\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFNE Redis `SET key value [expiration] IFNE match-value` command.\n// Compare-and-set: only sets the value if the current value does not equal matchValue.\n//\n// Returns \"OK\" on success.\n// Returns nil if the operation was aborted due to condition not matching.\n// Zero expiration means the key has no expiration time.\n//\n// NOTE SetIFNE is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFNE(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StatusCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifne\", matchValue)\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFNEGet Redis `SET key value [expiration] IFNE match-value GET` command.\n// Compare-and-set with GET: only sets the value if the current value does not equal matchValue,\n// and returns the previous value.\n//\n// Returns the previous value on success.\n// Returns nil if the operation was aborted due to condition not matching.\n// Zero expiration means the key has no expiration time.\n//\n// NOTE SetIFNEGet is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFNEGet(ctx context.Context, key string, value interface{}, matchValue interface{}, expiration time.Duration) *StringCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifne\", matchValue, \"get\")\n\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFDEQ sets the value only if the current value's digest equals matchDigest.\n//\n// This is a compare-and-set operation using xxh3 digest for optimistic locking.\n// The matchDigest parameter is a uint64 xxh3 hash value.\n//\n// Returns \"OK\" on success.\n// Returns redis.Nil if the digest doesn't match (value was modified).\n// Zero expiration means the key has no expiration time.\n//\n// For examples of client-side digest generation and usage patterns, see:\n// example/digest-optimistic-locking/\n//\n// Redis 8.4+. See https://redis.io/commands/set/\n//\n// NOTE SetIFNEQ is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFDEQ(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifdeq\", fmt.Sprintf(\"%016x\", matchDigest))\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFDEQGet sets the value only if the current value's digest equals matchDigest,\n// and returns the previous value.\n//\n// This is a compare-and-set operation using xxh3 digest for optimistic locking.\n// The matchDigest parameter is a uint64 xxh3 hash value.\n//\n// Returns the previous value on success.\n// Returns redis.Nil if the digest doesn't match (value was modified).\n// Zero expiration means the key has no expiration time.\n//\n// For examples of client-side digest generation and usage patterns, see:\n// example/digest-optimistic-locking/\n//\n// Redis 8.4+. See https://redis.io/commands/set/\n//\n// NOTE SetIFNEQGet is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFDEQGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifdeq\", fmt.Sprintf(\"%016x\", matchDigest), \"get\")\n\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFDNE sets the value only if the current value's digest does NOT equal matchDigest.\n//\n// This is a compare-and-set operation using xxh3 digest for optimistic locking.\n// The matchDigest parameter is a uint64 xxh3 hash value.\n//\n// Returns \"OK\" on success (digest didn't match, value was set).\n// Returns redis.Nil if the digest matches (value was not modified).\n// Zero expiration means the key has no expiration time.\n//\n// For examples of client-side digest generation and usage patterns, see:\n// example/digest-optimistic-locking/\n//\n// Redis 8.4+. See https://redis.io/commands/set/\n//\n// NOTE SetIFDNE is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFDNE(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StatusCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifdne\", fmt.Sprintf(\"%016x\", matchDigest))\n\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// SetIFDNEGet sets the value only if the current value's digest does NOT equal matchDigest,\n// and returns the previous value.\n//\n// This is a compare-and-set operation using xxh3 digest for optimistic locking.\n// The matchDigest parameter is a uint64 xxh3 hash value.\n//\n// Returns the previous value on success (digest didn't match, value was set).\n// Returns redis.Nil if the digest matches (value was not modified).\n// Zero expiration means the key has no expiration time.\n//\n// For examples of client-side digest generation and usage patterns, see:\n// example/digest-optimistic-locking/\n//\n// Redis 8.4+. See https://redis.io/commands/set/\n//\n// NOTE SetIFDNEGet is still experimental\n// it's signature and behaviour may change\nfunc (c cmdable) SetIFDNEGet(ctx context.Context, key string, value interface{}, matchDigest uint64, expiration time.Duration) *StringCmd {\n\targs := []interface{}{\"set\", key, value}\n\n\tif expiration > 0 {\n\t\tif usePrecise(expiration) {\n\t\t\targs = append(args, \"px\", formatMs(ctx, expiration))\n\t\t} else {\n\t\t\targs = append(args, \"ex\", formatSec(ctx, expiration))\n\t\t}\n\t} else if expiration == KeepTTL {\n\t\targs = append(args, \"keepttl\")\n\t}\n\n\targs = append(args, \"ifdne\", fmt.Sprintf(\"%016x\", matchDigest), \"get\")\n\n\tcmd := NewStringCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"setrange\", key, offset, value)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\nfunc (c cmdable) StrLen(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"strlen\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "timeseries_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/util\"\n)\n\ntype TimeseriesCmdable interface {\n\tTSAdd(ctx context.Context, key string, timestamp interface{}, value float64) *IntCmd\n\tTSAddWithArgs(ctx context.Context, key string, timestamp interface{}, value float64, options *TSOptions) *IntCmd\n\tTSCreate(ctx context.Context, key string) *StatusCmd\n\tTSCreateWithArgs(ctx context.Context, key string, options *TSOptions) *StatusCmd\n\tTSAlter(ctx context.Context, key string, options *TSAlterOptions) *StatusCmd\n\tTSCreateRule(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int) *StatusCmd\n\tTSCreateRuleWithArgs(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int, options *TSCreateRuleOptions) *StatusCmd\n\tTSIncrBy(ctx context.Context, Key string, timestamp float64) *IntCmd\n\tTSIncrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd\n\tTSDecrBy(ctx context.Context, Key string, timestamp float64) *IntCmd\n\tTSDecrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd\n\tTSDel(ctx context.Context, Key string, fromTimestamp int, toTimestamp int) *IntCmd\n\tTSDeleteRule(ctx context.Context, sourceKey string, destKey string) *StatusCmd\n\tTSGet(ctx context.Context, key string) *TSTimestampValueCmd\n\tTSGetWithArgs(ctx context.Context, key string, options *TSGetOptions) *TSTimestampValueCmd\n\tTSInfo(ctx context.Context, key string) *MapStringInterfaceCmd\n\tTSInfoWithArgs(ctx context.Context, key string, options *TSInfoOptions) *MapStringInterfaceCmd\n\tTSMAdd(ctx context.Context, ktvSlices [][]interface{}) *IntSliceCmd\n\tTSQueryIndex(ctx context.Context, filterExpr []string) *StringSliceCmd\n\tTSRevRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd\n\tTSRevRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRevRangeOptions) *TSTimestampValueSliceCmd\n\tTSRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd\n\tTSRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRangeOptions) *TSTimestampValueSliceCmd\n\tTSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd\n\tTSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRangeOptions) *MapStringSliceInterfaceCmd\n\tTSMRevRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd\n\tTSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRevRangeOptions) *MapStringSliceInterfaceCmd\n\tTSMGet(ctx context.Context, filters []string) *MapStringSliceInterfaceCmd\n\tTSMGetWithArgs(ctx context.Context, filters []string, options *TSMGetOptions) *MapStringSliceInterfaceCmd\n}\n\ntype TSOptions struct {\n\tRetention         int\n\tChunkSize         int\n\tEncoding          string\n\tDuplicatePolicy   string\n\tLabels            map[string]string\n\tIgnoreMaxTimeDiff int64\n\tIgnoreMaxValDiff  float64\n}\ntype TSIncrDecrOptions struct {\n\tTimestamp         int64\n\tRetention         int\n\tChunkSize         int\n\tUncompressed      bool\n\tDuplicatePolicy   string\n\tLabels            map[string]string\n\tIgnoreMaxTimeDiff int64\n\tIgnoreMaxValDiff  float64\n}\n\ntype TSAlterOptions struct {\n\tRetention         int\n\tChunkSize         int\n\tDuplicatePolicy   string\n\tLabels            map[string]string\n\tIgnoreMaxTimeDiff int64\n\tIgnoreMaxValDiff  float64\n}\n\ntype TSCreateRuleOptions struct {\n\talignTimestamp int64\n}\n\ntype TSGetOptions struct {\n\tLatest bool\n}\n\ntype TSInfoOptions struct {\n\tDebug bool\n}\ntype Aggregator int\n\nconst (\n\tInvalid = Aggregator(iota)\n\tAvg\n\tSum\n\tMin\n\tMax\n\tRange\n\tCount\n\tFirst\n\tLast\n\tStdP\n\tStdS\n\tVarP\n\tVarS\n\tTwa\n\tCountNaN\n\tCountAll\n)\n\nfunc (a Aggregator) String() string {\n\tswitch a {\n\tcase Invalid:\n\t\treturn \"\"\n\tcase Avg:\n\t\treturn \"AVG\"\n\tcase Sum:\n\t\treturn \"SUM\"\n\tcase Min:\n\t\treturn \"MIN\"\n\tcase Max:\n\t\treturn \"MAX\"\n\tcase Range:\n\t\treturn \"RANGE\"\n\tcase Count:\n\t\treturn \"COUNT\"\n\tcase First:\n\t\treturn \"FIRST\"\n\tcase Last:\n\t\treturn \"LAST\"\n\tcase StdP:\n\t\treturn \"STD.P\"\n\tcase StdS:\n\t\treturn \"STD.S\"\n\tcase VarP:\n\t\treturn \"VAR.P\"\n\tcase VarS:\n\t\treturn \"VAR.S\"\n\tcase Twa:\n\t\treturn \"TWA\"\n\tcase CountNaN:\n\t\treturn \"COUNTNAN\"\n\tcase CountAll:\n\t\treturn \"COUNTALL\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\ntype TSRangeOptions struct {\n\tLatest          bool\n\tFilterByTS      []int\n\tFilterByValue   []int\n\tCount           int\n\tAlign           interface{}\n\tAggregator      Aggregator\n\tBucketDuration  int\n\tBucketTimestamp interface{}\n\tEmpty           bool\n}\n\ntype TSRevRangeOptions struct {\n\tLatest          bool\n\tFilterByTS      []int\n\tFilterByValue   []int\n\tCount           int\n\tAlign           interface{}\n\tAggregator      Aggregator\n\tBucketDuration  int\n\tBucketTimestamp interface{}\n\tEmpty           bool\n}\n\ntype TSMRangeOptions struct {\n\tLatest          bool\n\tFilterByTS      []int\n\tFilterByValue   []int\n\tWithLabels      bool\n\tSelectedLabels  []interface{}\n\tCount           int\n\tAlign           interface{}\n\tAggregator      Aggregator\n\tBucketDuration  int\n\tBucketTimestamp interface{}\n\tEmpty           bool\n\tGroupByLabel    interface{}\n\tReducer         interface{}\n}\n\ntype TSMRevRangeOptions struct {\n\tLatest          bool\n\tFilterByTS      []int\n\tFilterByValue   []int\n\tWithLabels      bool\n\tSelectedLabels  []interface{}\n\tCount           int\n\tAlign           interface{}\n\tAggregator      Aggregator\n\tBucketDuration  int\n\tBucketTimestamp interface{}\n\tEmpty           bool\n\tGroupByLabel    interface{}\n\tReducer         interface{}\n}\n\ntype TSMGetOptions struct {\n\tLatest         bool\n\tWithLabels     bool\n\tSelectedLabels []interface{}\n}\n\n// TSAdd - Adds one or more observations to a t-digest sketch.\n// For more information - https://redis.io/commands/ts.add/\nfunc (c cmdable) TSAdd(ctx context.Context, key string, timestamp interface{}, value float64) *IntCmd {\n\targs := []interface{}{\"TS.ADD\", key, timestamp, value}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSAddWithArgs - Adds one or more observations to a t-digest sketch.\n// This function also allows for specifying additional options such as:\n// Retention, ChunkSize, Encoding, DuplicatePolicy and Labels.\n// For more information - https://redis.io/commands/ts.add/\nfunc (c cmdable) TSAddWithArgs(ctx context.Context, key string, timestamp interface{}, value float64, options *TSOptions) *IntCmd {\n\targs := []interface{}{\"TS.ADD\", key, timestamp, value}\n\tif options != nil {\n\t\tif options.Retention != 0 {\n\t\t\targs = append(args, \"RETENTION\", options.Retention)\n\t\t}\n\t\tif options.ChunkSize != 0 {\n\t\t\targs = append(args, \"CHUNK_SIZE\", options.ChunkSize)\n\t\t}\n\t\tif options.Encoding != \"\" {\n\t\t\targs = append(args, \"ENCODING\", options.Encoding)\n\t\t}\n\n\t\tif options.DuplicatePolicy != \"\" {\n\t\t\targs = append(args, \"DUPLICATE_POLICY\", options.DuplicatePolicy)\n\t\t}\n\t\tif options.Labels != nil {\n\t\t\targs = append(args, \"LABELS\")\n\t\t\tfor label, value := range options.Labels {\n\t\t\t\targs = append(args, label, value)\n\t\t\t}\n\t\t}\n\t\tif options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 {\n\t\t\targs = append(args, \"IGNORE\", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff)\n\t\t}\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSCreate - Creates a new time-series key.\n// For more information - https://redis.io/commands/ts.create/\nfunc (c cmdable) TSCreate(ctx context.Context, key string) *StatusCmd {\n\targs := []interface{}{\"TS.CREATE\", key}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSCreateWithArgs - Creates a new time-series key with additional options.\n// This function allows for specifying additional options such as:\n// Retention, ChunkSize, Encoding, DuplicatePolicy and Labels.\n// For more information - https://redis.io/commands/ts.create/\nfunc (c cmdable) TSCreateWithArgs(ctx context.Context, key string, options *TSOptions) *StatusCmd {\n\targs := []interface{}{\"TS.CREATE\", key}\n\tif options != nil {\n\t\tif options.Retention != 0 {\n\t\t\targs = append(args, \"RETENTION\", options.Retention)\n\t\t}\n\t\tif options.ChunkSize != 0 {\n\t\t\targs = append(args, \"CHUNK_SIZE\", options.ChunkSize)\n\t\t}\n\t\tif options.Encoding != \"\" {\n\t\t\targs = append(args, \"ENCODING\", options.Encoding)\n\t\t}\n\n\t\tif options.DuplicatePolicy != \"\" {\n\t\t\targs = append(args, \"DUPLICATE_POLICY\", options.DuplicatePolicy)\n\t\t}\n\t\tif options.Labels != nil {\n\t\t\targs = append(args, \"LABELS\")\n\t\t\tfor label, value := range options.Labels {\n\t\t\t\targs = append(args, label, value)\n\t\t\t}\n\t\t}\n\t\tif options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 {\n\t\t\targs = append(args, \"IGNORE\", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff)\n\t\t}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSAlter - Alters an existing time-series key with additional options.\n// This function allows for specifying additional options such as:\n// Retention, ChunkSize and DuplicatePolicy.\n// For more information - https://redis.io/commands/ts.alter/\nfunc (c cmdable) TSAlter(ctx context.Context, key string, options *TSAlterOptions) *StatusCmd {\n\targs := []interface{}{\"TS.ALTER\", key}\n\tif options != nil {\n\t\tif options.Retention != 0 {\n\t\t\targs = append(args, \"RETENTION\", options.Retention)\n\t\t}\n\t\tif options.ChunkSize != 0 {\n\t\t\targs = append(args, \"CHUNK_SIZE\", options.ChunkSize)\n\t\t}\n\t\tif options.DuplicatePolicy != \"\" {\n\t\t\targs = append(args, \"DUPLICATE_POLICY\", options.DuplicatePolicy)\n\t\t}\n\t\tif options.Labels != nil {\n\t\t\targs = append(args, \"LABELS\")\n\t\t\tfor label, value := range options.Labels {\n\t\t\t\targs = append(args, label, value)\n\t\t\t}\n\t\t}\n\t\tif options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 {\n\t\t\targs = append(args, \"IGNORE\", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff)\n\t\t}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSCreateRule - Creates a compaction rule from sourceKey to destKey.\n// For more information - https://redis.io/commands/ts.createrule/\nfunc (c cmdable) TSCreateRule(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int) *StatusCmd {\n\targs := []interface{}{\"TS.CREATERULE\", sourceKey, destKey, \"AGGREGATION\", aggregator.String(), bucketDuration}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSCreateRuleWithArgs - Creates a compaction rule from sourceKey to destKey with additional option.\n// This function allows for specifying additional option such as:\n// alignTimestamp.\n// For more information - https://redis.io/commands/ts.createrule/\nfunc (c cmdable) TSCreateRuleWithArgs(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int, options *TSCreateRuleOptions) *StatusCmd {\n\targs := []interface{}{\"TS.CREATERULE\", sourceKey, destKey, \"AGGREGATION\", aggregator.String(), bucketDuration}\n\tif options != nil {\n\t\tif options.alignTimestamp != 0 {\n\t\t\targs = append(args, options.alignTimestamp)\n\t\t}\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSIncrBy - Increments the value of a time-series key by the specified timestamp.\n// For more information - https://redis.io/commands/ts.incrby/\nfunc (c cmdable) TSIncrBy(ctx context.Context, Key string, timestamp float64) *IntCmd {\n\targs := []interface{}{\"TS.INCRBY\", Key, timestamp}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSIncrByWithArgs - Increments the value of a time-series key by the specified timestamp with additional options.\n// This function allows for specifying additional options such as:\n// Timestamp, Retention, ChunkSize, Uncompressed and Labels.\n// For more information - https://redis.io/commands/ts.incrby/\nfunc (c cmdable) TSIncrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd {\n\targs := []interface{}{\"TS.INCRBY\", key, timestamp}\n\tif options != nil {\n\t\tif options.Timestamp != 0 {\n\t\t\targs = append(args, \"TIMESTAMP\", options.Timestamp)\n\t\t}\n\t\tif options.Retention != 0 {\n\t\t\targs = append(args, \"RETENTION\", options.Retention)\n\t\t}\n\t\tif options.ChunkSize != 0 {\n\t\t\targs = append(args, \"CHUNK_SIZE\", options.ChunkSize)\n\t\t}\n\t\tif options.Uncompressed {\n\t\t\targs = append(args, \"UNCOMPRESSED\")\n\t\t}\n\t\tif options.DuplicatePolicy != \"\" {\n\t\t\targs = append(args, \"DUPLICATE_POLICY\", options.DuplicatePolicy)\n\t\t}\n\t\tif options.Labels != nil {\n\t\t\targs = append(args, \"LABELS\")\n\t\t\tfor label, value := range options.Labels {\n\t\t\t\targs = append(args, label, value)\n\t\t\t}\n\t\t}\n\t\tif options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 {\n\t\t\targs = append(args, \"IGNORE\", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff)\n\t\t}\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSDecrBy - Decrements the value of a time-series key by the specified timestamp.\n// For more information - https://redis.io/commands/ts.decrby/\nfunc (c cmdable) TSDecrBy(ctx context.Context, Key string, timestamp float64) *IntCmd {\n\targs := []interface{}{\"TS.DECRBY\", Key, timestamp}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSDecrByWithArgs - Decrements the value of a time-series key by the specified timestamp with additional options.\n// This function allows for specifying additional options such as:\n// Timestamp, Retention, ChunkSize, Uncompressed and Labels.\n// For more information - https://redis.io/commands/ts.decrby/\nfunc (c cmdable) TSDecrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd {\n\targs := []interface{}{\"TS.DECRBY\", key, timestamp}\n\tif options != nil {\n\t\tif options.Timestamp != 0 {\n\t\t\targs = append(args, \"TIMESTAMP\", options.Timestamp)\n\t\t}\n\t\tif options.Retention != 0 {\n\t\t\targs = append(args, \"RETENTION\", options.Retention)\n\t\t}\n\t\tif options.ChunkSize != 0 {\n\t\t\targs = append(args, \"CHUNK_SIZE\", options.ChunkSize)\n\t\t}\n\t\tif options.Uncompressed {\n\t\t\targs = append(args, \"UNCOMPRESSED\")\n\t\t}\n\t\tif options.DuplicatePolicy != \"\" {\n\t\t\targs = append(args, \"DUPLICATE_POLICY\", options.DuplicatePolicy)\n\t\t}\n\t\tif options.Labels != nil {\n\t\t\targs = append(args, \"LABELS\")\n\t\t\tfor label, value := range options.Labels {\n\t\t\t\targs = append(args, label, value)\n\t\t\t}\n\t\t}\n\t\tif options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 {\n\t\t\targs = append(args, \"IGNORE\", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff)\n\t\t}\n\t}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSDel - Deletes a range of samples from a time-series key.\n// For more information - https://redis.io/commands/ts.del/\nfunc (c cmdable) TSDel(ctx context.Context, Key string, fromTimestamp int, toTimestamp int) *IntCmd {\n\targs := []interface{}{\"TS.DEL\", Key, fromTimestamp, toTimestamp}\n\tcmd := NewIntCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSDeleteRule - Deletes a compaction rule from sourceKey to destKey.\n// For more information - https://redis.io/commands/ts.deleterule/\nfunc (c cmdable) TSDeleteRule(ctx context.Context, sourceKey string, destKey string) *StatusCmd {\n\targs := []interface{}{\"TS.DELETERULE\", sourceKey, destKey}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSGetWithArgs - Gets the last sample of a time-series key with additional option.\n// This function allows for specifying additional option such as:\n// Latest.\n// For more information - https://redis.io/commands/ts.get/\nfunc (c cmdable) TSGetWithArgs(ctx context.Context, key string, options *TSGetOptions) *TSTimestampValueCmd {\n\targs := []interface{}{\"TS.GET\", key}\n\tif options != nil {\n\t\tif options.Latest {\n\t\t\targs = append(args, \"LATEST\")\n\t\t}\n\t}\n\tcmd := newTSTimestampValueCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSGet - Gets the last sample of a time-series key.\n// For more information - https://redis.io/commands/ts.get/\nfunc (c cmdable) TSGet(ctx context.Context, key string) *TSTimestampValueCmd {\n\targs := []interface{}{\"TS.GET\", key}\n\tcmd := newTSTimestampValueCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype TSTimestampValue struct {\n\tTimestamp int64\n\tValue     float64\n}\ntype TSTimestampValueCmd struct {\n\tbaseCmd\n\tval TSTimestampValue\n}\n\nfunc newTSTimestampValueCmd(ctx context.Context, args ...interface{}) *TSTimestampValueCmd {\n\treturn &TSTimestampValueCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeTSTimestampValue,\n\t\t},\n\t}\n}\n\nfunc (cmd *TSTimestampValueCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *TSTimestampValueCmd) SetVal(val TSTimestampValue) {\n\tcmd.val = val\n}\n\nfunc (cmd *TSTimestampValueCmd) Result() (TSTimestampValue, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *TSTimestampValueCmd) Val() TSTimestampValue {\n\treturn cmd.val\n}\n\nfunc (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadMapLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = TSTimestampValue{}\n\tfor i := 0; i < n; i++ {\n\t\ttimestamp, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvalue, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val.Timestamp = timestamp\n\t\tcmd.val.Value, err = util.ParseStringToFloat(value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *TSTimestampValueCmd) Clone() Cmder {\n\treturn &TSTimestampValueCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     cmd.val, // TSTimestampValue is a simple struct, can be copied directly\n\t}\n}\n\n// TSInfo - Returns information about a time-series key.\n// For more information - https://redis.io/commands/ts.info/\nfunc (c cmdable) TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd {\n\targs := []interface{}{\"TS.INFO\", key}\n\tcmd := NewMapStringInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSInfoWithArgs - Returns information about a time-series key with additional option.\n// This function allows for specifying additional option such as:\n// Debug.\n// For more information - https://redis.io/commands/ts.info/\nfunc (c cmdable) TSInfoWithArgs(ctx context.Context, key string, options *TSInfoOptions) *MapStringInterfaceCmd {\n\targs := []interface{}{\"TS.INFO\", key}\n\tif options != nil {\n\t\tif options.Debug {\n\t\t\targs = append(args, \"DEBUG\")\n\t\t}\n\t}\n\tcmd := NewMapStringInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSMAdd - Adds multiple samples to multiple time-series keys.\n// It accepts a slice of 'ktv' slices, each containing exactly three elements: key, timestamp, and value.\n// This struct must be provided for this command to work.\n// For more information - https://redis.io/commands/ts.madd/\nfunc (c cmdable) TSMAdd(ctx context.Context, ktvSlices [][]interface{}) *IntSliceCmd {\n\targs := []interface{}{\"TS.MADD\"}\n\tfor _, ktv := range ktvSlices {\n\t\targs = append(args, ktv...)\n\t}\n\tcmd := NewIntSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSQueryIndex - Returns all the keys matching the filter expression.\n// For more information - https://redis.io/commands/ts.queryindex/\nfunc (c cmdable) TSQueryIndex(ctx context.Context, filterExpr []string) *StringSliceCmd {\n\targs := []interface{}{\"TS.QUERYINDEX\"}\n\tfor _, f := range filterExpr {\n\t\targs = append(args, f)\n\t}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSRevRange - Returns a range of samples from a time-series key in reverse order.\n// For more information - https://redis.io/commands/ts.revrange/\nfunc (c cmdable) TSRevRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd {\n\targs := []interface{}{\"TS.REVRANGE\", key, fromTimestamp, toTimestamp}\n\tcmd := newTSTimestampValueSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSRevRangeWithArgs - Returns a range of samples from a time-series key in reverse order with additional options.\n// This function allows for specifying additional options such as:\n// Latest, FilterByTS, FilterByValue, Count, Align, Aggregator,\n// BucketDuration, BucketTimestamp and Empty.\n// For more information - https://redis.io/commands/ts.revrange/\nfunc (c cmdable) TSRevRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRevRangeOptions) *TSTimestampValueSliceCmd {\n\targs := []interface{}{\"TS.REVRANGE\", key, fromTimestamp, toTimestamp}\n\tif options != nil {\n\t\tif options.Latest {\n\t\t\targs = append(args, \"LATEST\")\n\t\t}\n\t\tif options.FilterByTS != nil {\n\t\t\targs = append(args, \"FILTER_BY_TS\")\n\t\t\tfor _, f := range options.FilterByTS {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.FilterByValue != nil {\n\t\t\targs = append(args, \"FILTER_BY_VALUE\")\n\t\t\tfor _, f := range options.FilterByValue {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.Count != 0 {\n\t\t\targs = append(args, \"COUNT\", options.Count)\n\t\t}\n\t\tif options.Align != nil {\n\t\t\targs = append(args, \"ALIGN\", options.Align)\n\t\t}\n\t\tif options.Aggregator != 0 {\n\t\t\targs = append(args, \"AGGREGATION\", options.Aggregator.String())\n\t\t}\n\t\tif options.BucketDuration != 0 {\n\t\t\targs = append(args, options.BucketDuration)\n\t\t}\n\t\tif options.BucketTimestamp != nil {\n\t\t\targs = append(args, \"BUCKETTIMESTAMP\", options.BucketTimestamp)\n\t\t}\n\t\tif options.Empty {\n\t\t\targs = append(args, \"EMPTY\")\n\t\t}\n\t}\n\tcmd := newTSTimestampValueSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSRange - Returns a range of samples from a time-series key.\n// For more information - https://redis.io/commands/ts.range/\nfunc (c cmdable) TSRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd {\n\targs := []interface{}{\"TS.RANGE\", key, fromTimestamp, toTimestamp}\n\tcmd := newTSTimestampValueSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSRangeWithArgs - Returns a range of samples from a time-series key with additional options.\n// This function allows for specifying additional options such as:\n// Latest, FilterByTS, FilterByValue, Count, Align, Aggregator,\n// BucketDuration, BucketTimestamp and Empty.\n// For more information - https://redis.io/commands/ts.range/\nfunc (c cmdable) TSRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRangeOptions) *TSTimestampValueSliceCmd {\n\targs := []interface{}{\"TS.RANGE\", key, fromTimestamp, toTimestamp}\n\tif options != nil {\n\t\tif options.Latest {\n\t\t\targs = append(args, \"LATEST\")\n\t\t}\n\t\tif options.FilterByTS != nil {\n\t\t\targs = append(args, \"FILTER_BY_TS\")\n\t\t\tfor _, f := range options.FilterByTS {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.FilterByValue != nil {\n\t\t\targs = append(args, \"FILTER_BY_VALUE\")\n\t\t\tfor _, f := range options.FilterByValue {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.Count != 0 {\n\t\t\targs = append(args, \"COUNT\", options.Count)\n\t\t}\n\t\tif options.Align != nil {\n\t\t\targs = append(args, \"ALIGN\", options.Align)\n\t\t}\n\t\tif options.Aggregator != 0 {\n\t\t\targs = append(args, \"AGGREGATION\", options.Aggregator.String())\n\t\t}\n\t\tif options.BucketDuration != 0 {\n\t\t\targs = append(args, options.BucketDuration)\n\t\t}\n\t\tif options.BucketTimestamp != nil {\n\t\t\targs = append(args, \"BUCKETTIMESTAMP\", options.BucketTimestamp)\n\t\t}\n\t\tif options.Empty {\n\t\t\targs = append(args, \"EMPTY\")\n\t\t}\n\t}\n\tcmd := newTSTimestampValueSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\ntype TSTimestampValueSliceCmd struct {\n\tbaseCmd\n\tval []TSTimestampValue\n}\n\nfunc newTSTimestampValueSliceCmd(ctx context.Context, args ...interface{}) *TSTimestampValueSliceCmd {\n\treturn &TSTimestampValueSliceCmd{\n\t\tbaseCmd: baseCmd{\n\t\t\tctx:     ctx,\n\t\t\targs:    args,\n\t\t\tcmdType: CmdTypeTSTimestampValueSlice,\n\t\t},\n\t}\n}\n\nfunc (cmd *TSTimestampValueSliceCmd) String() string {\n\treturn cmdString(cmd, cmd.val)\n}\n\nfunc (cmd *TSTimestampValueSliceCmd) SetVal(val []TSTimestampValue) {\n\tcmd.val = val\n}\n\nfunc (cmd *TSTimestampValueSliceCmd) Result() ([]TSTimestampValue, error) {\n\treturn cmd.val, cmd.err\n}\n\nfunc (cmd *TSTimestampValueSliceCmd) Val() []TSTimestampValue {\n\treturn cmd.val\n}\n\nfunc (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) {\n\tn, err := rd.ReadArrayLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.val = make([]TSTimestampValue, n)\n\tfor i := 0; i < n; i++ {\n\t\t_, _ = rd.ReadArrayLen()\n\t\ttimestamp, err := rd.ReadInt()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvalue, err := rd.ReadString()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.val[i].Timestamp = timestamp\n\t\tcmd.val[i].Value, err = util.ParseStringToFloat(value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *TSTimestampValueSliceCmd) Clone() Cmder {\n\tvar val []TSTimestampValue\n\tif cmd.val != nil {\n\t\tval = make([]TSTimestampValue, len(cmd.val))\n\t\tcopy(val, cmd.val)\n\t}\n\treturn &TSTimestampValueSliceCmd{\n\t\tbaseCmd: cmd.cloneBaseCmd(),\n\t\tval:     val,\n\t}\n}\n\n// TSMRange - Returns a range of samples from multiple time-series keys.\n// For more information - https://redis.io/commands/ts.mrange/\nfunc (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd {\n\targs := []interface{}{\"TS.MRANGE\", fromTimestamp, toTimestamp, \"FILTER\"}\n\tfor _, f := range filterExpr {\n\t\targs = append(args, f)\n\t}\n\tcmd := NewMapStringSliceInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSMRangeWithArgs - Returns a range of samples from multiple time-series keys with additional options.\n// This function allows for specifying additional options such as:\n// Latest, FilterByTS, FilterByValue, WithLabels, SelectedLabels,\n// Count, Align, Aggregator, BucketDuration, BucketTimestamp,\n// Empty, GroupByLabel and Reducer.\n// For more information - https://redis.io/commands/ts.mrange/\nfunc (c cmdable) TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRangeOptions) *MapStringSliceInterfaceCmd {\n\targs := []interface{}{\"TS.MRANGE\", fromTimestamp, toTimestamp}\n\tif options != nil {\n\t\tif options.Latest {\n\t\t\targs = append(args, \"LATEST\")\n\t\t}\n\t\tif options.FilterByTS != nil {\n\t\t\targs = append(args, \"FILTER_BY_TS\")\n\t\t\tfor _, f := range options.FilterByTS {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.FilterByValue != nil {\n\t\t\targs = append(args, \"FILTER_BY_VALUE\")\n\t\t\tfor _, f := range options.FilterByValue {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.WithLabels {\n\t\t\targs = append(args, \"WITHLABELS\")\n\t\t}\n\t\tif options.SelectedLabels != nil {\n\t\t\targs = append(args, \"SELECTED_LABELS\")\n\t\t\targs = append(args, options.SelectedLabels...)\n\t\t}\n\t\tif options.Count != 0 {\n\t\t\targs = append(args, \"COUNT\", options.Count)\n\t\t}\n\t\tif options.Align != nil {\n\t\t\targs = append(args, \"ALIGN\", options.Align)\n\t\t}\n\t\tif options.Aggregator != 0 {\n\t\t\targs = append(args, \"AGGREGATION\", options.Aggregator.String())\n\t\t}\n\t\tif options.BucketDuration != 0 {\n\t\t\targs = append(args, options.BucketDuration)\n\t\t}\n\t\tif options.BucketTimestamp != nil {\n\t\t\targs = append(args, \"BUCKETTIMESTAMP\", options.BucketTimestamp)\n\t\t}\n\t\tif options.Empty {\n\t\t\targs = append(args, \"EMPTY\")\n\t\t}\n\t}\n\targs = append(args, \"FILTER\")\n\tfor _, f := range filterExpr {\n\t\targs = append(args, f)\n\t}\n\tif options != nil {\n\t\tif options.GroupByLabel != nil {\n\t\t\targs = append(args, \"GROUPBY\", options.GroupByLabel)\n\t\t}\n\t\tif options.Reducer != nil {\n\t\t\targs = append(args, \"REDUCE\", options.Reducer)\n\t\t}\n\t}\n\tcmd := NewMapStringSliceInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSMRevRange - Returns a range of samples from multiple time-series keys in reverse order.\n// For more information - https://redis.io/commands/ts.mrevrange/\nfunc (c cmdable) TSMRevRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd {\n\targs := []interface{}{\"TS.MREVRANGE\", fromTimestamp, toTimestamp, \"FILTER\"}\n\tfor _, f := range filterExpr {\n\t\targs = append(args, f)\n\t}\n\tcmd := NewMapStringSliceInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSMRevRangeWithArgs - Returns a range of samples from multiple time-series keys in reverse order with additional options.\n// This function allows for specifying additional options such as:\n// Latest, FilterByTS, FilterByValue, WithLabels, SelectedLabels,\n// Count, Align, Aggregator, BucketDuration, BucketTimestamp,\n// Empty, GroupByLabel and Reducer.\n// For more information - https://redis.io/commands/ts.mrevrange/\nfunc (c cmdable) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRevRangeOptions) *MapStringSliceInterfaceCmd {\n\targs := []interface{}{\"TS.MREVRANGE\", fromTimestamp, toTimestamp}\n\tif options != nil {\n\t\tif options.Latest {\n\t\t\targs = append(args, \"LATEST\")\n\t\t}\n\t\tif options.FilterByTS != nil {\n\t\t\targs = append(args, \"FILTER_BY_TS\")\n\t\t\tfor _, f := range options.FilterByTS {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.FilterByValue != nil {\n\t\t\targs = append(args, \"FILTER_BY_VALUE\")\n\t\t\tfor _, f := range options.FilterByValue {\n\t\t\t\targs = append(args, f)\n\t\t\t}\n\t\t}\n\t\tif options.WithLabels {\n\t\t\targs = append(args, \"WITHLABELS\")\n\t\t}\n\t\tif options.SelectedLabels != nil {\n\t\t\targs = append(args, \"SELECTED_LABELS\")\n\t\t\targs = append(args, options.SelectedLabels...)\n\t\t}\n\t\tif options.Count != 0 {\n\t\t\targs = append(args, \"COUNT\", options.Count)\n\t\t}\n\t\tif options.Align != nil {\n\t\t\targs = append(args, \"ALIGN\", options.Align)\n\t\t}\n\t\tif options.Aggregator != 0 {\n\t\t\targs = append(args, \"AGGREGATION\", options.Aggregator.String())\n\t\t}\n\t\tif options.BucketDuration != 0 {\n\t\t\targs = append(args, options.BucketDuration)\n\t\t}\n\t\tif options.BucketTimestamp != nil {\n\t\t\targs = append(args, \"BUCKETTIMESTAMP\", options.BucketTimestamp)\n\t\t}\n\t\tif options.Empty {\n\t\t\targs = append(args, \"EMPTY\")\n\t\t}\n\t}\n\targs = append(args, \"FILTER\")\n\tfor _, f := range filterExpr {\n\t\targs = append(args, f)\n\t}\n\tif options != nil {\n\t\tif options.GroupByLabel != nil {\n\t\t\targs = append(args, \"GROUPBY\", options.GroupByLabel)\n\t\t}\n\t\tif options.Reducer != nil {\n\t\t\targs = append(args, \"REDUCE\", options.Reducer)\n\t\t}\n\t}\n\tcmd := NewMapStringSliceInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSMGet - Returns the last sample of multiple time-series keys.\n// For more information - https://redis.io/commands/ts.mget/\nfunc (c cmdable) TSMGet(ctx context.Context, filters []string) *MapStringSliceInterfaceCmd {\n\targs := []interface{}{\"TS.MGET\", \"FILTER\"}\n\tfor _, f := range filters {\n\t\targs = append(args, f)\n\t}\n\tcmd := NewMapStringSliceInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// TSMGetWithArgs - Returns the last sample of multiple time-series keys with additional options.\n// This function allows for specifying additional options such as:\n// Latest, WithLabels and SelectedLabels.\n// For more information - https://redis.io/commands/ts.mget/\nfunc (c cmdable) TSMGetWithArgs(ctx context.Context, filters []string, options *TSMGetOptions) *MapStringSliceInterfaceCmd {\n\targs := []interface{}{\"TS.MGET\"}\n\tif options != nil {\n\t\tif options.Latest {\n\t\t\targs = append(args, \"LATEST\")\n\t\t}\n\t\tif options.WithLabels {\n\t\t\targs = append(args, \"WITHLABELS\")\n\t\t}\n\t\tif options.SelectedLabels != nil {\n\t\t\targs = append(args, \"SELECTED_LABELS\")\n\t\t\targs = append(args, options.SelectedLabels...)\n\t\t}\n\t}\n\targs = append(args, \"FILTER\")\n\tfor _, f := range filters {\n\t\targs = append(args, f)\n\t}\n\tcmd := NewMapStringSliceInterfaceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "timeseries_commands_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"RedisTimeseries commands\", Label(\"timeseries\"), func() {\n\tctx := context.TODO()\n\n\tsetupRedisClient := func(protocolVersion int) *redis.Client {\n\t\treturn redis.NewClient(&redis.Options{\n\t\t\tAddr:          \"localhost:6379\",\n\t\t\tDB:            0,\n\t\t\tProtocol:      protocolVersion,\n\t\t\tUnstableResp3: true,\n\t\t})\n\t}\n\n\tprotocols := []int{2, 3}\n\tfor _, protocol := range protocols {\n\t\tprotocol := protocol // capture loop variable for each context\n\n\t\tContext(fmt.Sprintf(\"with protocol version %d\", protocol), func() {\n\t\t\tvar client *redis.Client\n\n\t\t\tBeforeEach(func() {\n\t\t\t\tclient = setupRedisClient(protocol)\n\t\t\t\tExpect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())\n\t\t\t})\n\n\t\t\tAfterEach(func() {\n\t\t\t\tif client != nil {\n\t\t\t\t\tclient.FlushDB(ctx)\n\t\t\t\t\tclient.Close()\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should TSCreate and TSCreateWithArgs\", Label(\"timeseries\", \"tscreate\", \"tscreateWithArgs\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(7.4, \"older redis stack has different results for timeseries module\")\n\t\t\t\tresult, err := client.TSCreate(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t// Test TSCreateWithArgs\n\t\t\t\topt := &redis.TSOptions{Retention: 5}\n\t\t\t\tresult, err = client.TSCreateWithArgs(ctx, \"2\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"Redis\": \"Labs\"}}\n\t\t\t\tresult, err = client.TSCreateWithArgs(ctx, \"3\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"Time\": \"Series\"}, Retention: 20}\n\t\t\t\tresult, err = client.TSCreateWithArgs(ctx, \"4\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultInfo, err := client.TSInfo(ctx, \"4\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(resultInfo[\"labels\"].([]interface{})[0]).To(BeEquivalentTo([]interface{}{\"Time\", \"Series\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(resultInfo[\"labels\"].(map[interface{}]interface{})[\"Time\"]).To(BeEquivalentTo(\"Series\"))\n\t\t\t\t}\n\t\t\t\t// Test chunk size\n\t\t\t\topt = &redis.TSOptions{ChunkSize: 128}\n\t\t\t\tresult, err = client.TSCreateWithArgs(ctx, \"ts-cs-1\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultInfo, err = client.TSInfo(ctx, \"ts-cs-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultInfo[\"chunkSize\"]).To(BeEquivalentTo(128))\n\t\t\t\t// Test duplicate policy\n\t\t\t\tduplicate_policies := []string{\"BLOCK\", \"LAST\", \"FIRST\", \"MIN\", \"MAX\"}\n\t\t\t\tfor _, dup := range duplicate_policies {\n\t\t\t\t\tkeyName := \"ts-dup-\" + dup\n\t\t\t\t\topt = &redis.TSOptions{DuplicatePolicy: dup}\n\t\t\t\t\tresult, err = client.TSCreateWithArgs(ctx, keyName, opt).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t\tresultInfo, err = client.TSInfo(ctx, keyName).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(strings.ToUpper(resultInfo[\"duplicatePolicy\"].(string))).To(BeEquivalentTo(dup))\n\t\t\t\t}\n\t\t\t\t// Test insertion filters\n\t\t\t\topt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, DuplicatePolicy: \"LAST\", IgnoreMaxValDiff: 10.0}\n\t\t\t\tresult, err = client.TSCreateWithArgs(ctx, \"ts-if-1\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultAdd, err := client.TSAdd(ctx, \"ts-if-1\", 1000, 1.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1000))\n\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"ts-if-1\", 1010, 11.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1010))\n\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"ts-if-1\", 1013, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1010))\n\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"ts-if-1\", 1020, 11.5).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1020))\n\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"ts-if-1\", 1021, 22.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1021))\n\n\t\t\t\trangePoints, err := client.TSRange(ctx, \"ts-if-1\", 1000, 1021).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(4))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{\n\t\t\t\t\t{Timestamp: 1000, Value: 1.0},\n\t\t\t\t\t{Timestamp: 1010, Value: 11.0},\n\t\t\t\t\t{Timestamp: 1020, Value: 11.5},\n\t\t\t\t\t{Timestamp: 1021, Value: 22.0}}))\n\t\t\t\t// Test insertion filters with other duplicate policy\n\t\t\t\topt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0}\n\t\t\t\tresult, err = client.TSCreateWithArgs(ctx, \"ts-if-2\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultAdd1, err := client.TSAdd(ctx, \"ts-if-1\", 1000, 1.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd1).To(BeEquivalentTo(1000))\n\t\t\t\tresultAdd1, err = client.TSAdd(ctx, \"ts-if-1\", 1010, 11.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd1).To(BeEquivalentTo(1010))\n\t\t\t\tresultAdd1, err = client.TSAdd(ctx, \"ts-if-1\", 1013, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd1).To(BeEquivalentTo(1013))\n\n\t\t\t\trangePoints, err = client.TSRange(ctx, \"ts-if-1\", 1000, 1013).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(3))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{\n\t\t\t\t\t{Timestamp: 1000, Value: 1.0},\n\t\t\t\t\t{Timestamp: 1010, Value: 11.0},\n\t\t\t\t\t{Timestamp: 1013, Value: 10.0}}))\n\t\t\t})\n\t\t\tIt(\"should TSAdd and TSAddWithArgs\", Label(\"timeseries\", \"tsadd\", \"tsaddWithArgs\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(7.4, \"older redis stack has different results for timeseries module\")\n\t\t\t\tresult, err := client.TSAdd(ctx, \"1\", 1, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\t// Test TSAddWithArgs\n\t\t\t\topt := &redis.TSOptions{Retention: 10}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"2\", 2, 3, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(2))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"Redis\": \"Labs\"}}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"3\", 3, 2, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(3))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"Redis\": \"Labs\", \"Time\": \"Series\"}, Retention: 10}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"4\", 4, 2, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(4))\n\t\t\t\tresultInfo, err := client.TSInfo(ctx, \"4\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(resultInfo[\"labels\"].([]interface{})).To(ContainElement([]interface{}{\"Time\", \"Series\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(resultInfo[\"labels\"].(map[interface{}]interface{})[\"Time\"]).To(BeEquivalentTo(\"Series\"))\n\t\t\t\t}\n\t\t\t\t// Test chunk size\n\t\t\t\topt = &redis.TSOptions{ChunkSize: 128}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"ts-cs-1\", 1, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresultInfo, err = client.TSInfo(ctx, \"ts-cs-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultInfo[\"chunkSize\"]).To(BeEquivalentTo(128))\n\t\t\t\t// Test duplicate policy\n\t\t\t\t// LAST\n\t\t\t\topt = &redis.TSOptions{DuplicatePolicy: \"LAST\"}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsal-1\", 1, 5, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsal-1\", 1, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresultGet, err := client.TSGet(ctx, \"tsal-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet.Value).To(BeEquivalentTo(10))\n\t\t\t\t// FIRST\n\t\t\t\topt = &redis.TSOptions{DuplicatePolicy: \"FIRST\"}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsaf-1\", 1, 5, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsaf-1\", 1, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresultGet, err = client.TSGet(ctx, \"tsaf-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet.Value).To(BeEquivalentTo(5))\n\t\t\t\t// MAX\n\t\t\t\topt = &redis.TSOptions{DuplicatePolicy: \"MAX\"}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsam-1\", 1, 5, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsam-1\", 1, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresultGet, err = client.TSGet(ctx, \"tsam-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet.Value).To(BeEquivalentTo(10))\n\t\t\t\t// MIN\n\t\t\t\topt = &redis.TSOptions{DuplicatePolicy: \"MIN\"}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsami-1\", 1, 5, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"tsami-1\", 1, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1))\n\t\t\t\tresultGet, err = client.TSGet(ctx, \"tsami-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet.Value).To(BeEquivalentTo(5))\n\t\t\t\t// Insertion filters\n\t\t\t\topt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: \"LAST\"}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"ts-if-1\", 1000, 1.0, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1000))\n\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"ts-if-1\", 1004, 3.0, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1000))\n\n\t\t\t\trangePoints, err := client.TSRange(ctx, \"ts-if-1\", 1000, 1004).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}}))\n\t\t\t})\n\n\t\t\tIt(\"should TSAlter\", Label(\"timeseries\", \"tsalter\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(7.4, \"older redis stack has different results for timeseries module\")\n\t\t\t\tresult, err := client.TSCreate(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultInfo, err := client.TSInfo(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultInfo[\"retentionTime\"]).To(BeEquivalentTo(0))\n\n\t\t\t\topt := &redis.TSAlterOptions{Retention: 10}\n\t\t\t\tresultAlter, err := client.TSAlter(ctx, \"1\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAlter).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultInfo, err = client.TSInfo(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultInfo[\"retentionTime\"]).To(BeEquivalentTo(10))\n\n\t\t\t\tresultInfo, err = client.TSInfo(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(resultInfo[\"labels\"]).To(BeEquivalentTo([]interface{}{}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(resultInfo[\"labels\"]).To(BeEquivalentTo(map[interface{}]interface{}{}))\n\t\t\t\t}\n\n\t\t\t\topt = &redis.TSAlterOptions{Labels: map[string]string{\"Time\": \"Series\"}}\n\t\t\t\tresultAlter, err = client.TSAlter(ctx, \"1\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAlter).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultInfo, err = client.TSInfo(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(resultInfo[\"labels\"].([]interface{})[0]).To(BeEquivalentTo([]interface{}{\"Time\", \"Series\"}))\n\t\t\t\t\tExpect(resultInfo[\"retentionTime\"]).To(BeEquivalentTo(10))\n\t\t\t\t\tif RedisVersion >= 8 {\n\t\t\t\t\t\tExpect(resultInfo[\"duplicatePolicy\"]).To(BeEquivalentTo(\"block\"))\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Older versions of Redis had a bug where the duplicate policy was not set correctly\n\t\t\t\t\t\tExpect(resultInfo[\"duplicatePolicy\"]).To(BeEquivalentTo(redis.Nil))\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tExpect(resultInfo[\"labels\"].(map[interface{}]interface{})[\"Time\"]).To(BeEquivalentTo(\"Series\"))\n\t\t\t\t\tExpect(resultInfo[\"retentionTime\"]).To(BeEquivalentTo(10))\n\t\t\t\t\tif RedisVersion >= 8 {\n\t\t\t\t\t\tExpect(resultInfo[\"duplicatePolicy\"]).To(BeEquivalentTo(\"block\"))\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Older versions of Redis had a bug where the duplicate policy was not set correctly\n\t\t\t\t\t\tExpect(resultInfo[\"duplicatePolicy\"]).To(BeEquivalentTo(redis.Nil))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\topt = &redis.TSAlterOptions{DuplicatePolicy: \"min\"}\n\t\t\t\tresultAlter, err = client.TSAlter(ctx, \"1\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAlter).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultInfo, err = client.TSInfo(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultInfo[\"duplicatePolicy\"]).To(BeEquivalentTo(\"min\"))\n\t\t\t\t// Test insertion filters\n\t\t\t\tresultAdd, err := client.TSAdd(ctx, \"ts-if-1\", 1000, 1.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1000))\n\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"ts-if-1\", 1010, 11.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1010))\n\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"ts-if-1\", 1013, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1013))\n\n\t\t\t\talterOpt := &redis.TSAlterOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: \"LAST\"}\n\t\t\t\tresultAlter, err = client.TSAlter(ctx, \"ts-if-1\", alterOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAlter).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"ts-if-1\", 1015, 11.5).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(1013))\n\n\t\t\t\trangePoints, err := client.TSRange(ctx, \"ts-if-1\", 1000, 1013).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(3))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{\n\t\t\t\t\t{Timestamp: 1000, Value: 1.0},\n\t\t\t\t\t{Timestamp: 1010, Value: 11.0},\n\t\t\t\t\t{Timestamp: 1013, Value: 10.0}}))\n\t\t\t})\n\n\t\t\tIt(\"should TSCreateRule and TSDeleteRule\", Label(\"timeseries\", \"tscreaterule\", \"tsdeleterule\"), func() {\n\t\t\t\tresult, err := client.TSCreate(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresult, err = client.TSCreate(ctx, \"2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresult, err = client.TSCreateRule(ctx, \"1\", \"2\", redis.Avg, 100).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tfor i := 0; i < 50; i++ {\n\t\t\t\t\tresultAdd, err := client.TSAdd(ctx, \"1\", 100+i*2, 1).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(100 + i*2))\n\t\t\t\t\tresultAdd, err = client.TSAdd(ctx, \"1\", 100+i*2+1, 2).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(100 + i*2 + 1))\n\n\t\t\t\t}\n\t\t\t\tresultAdd, err := client.TSAdd(ctx, \"1\", 100*2, 1.5).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultAdd).To(BeEquivalentTo(100 * 2))\n\t\t\t\tresultGet, err := client.TSGet(ctx, \"2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet.Value).To(BeEquivalentTo(1.5))\n\t\t\t\tExpect(resultGet.Timestamp).To(BeEquivalentTo(100))\n\n\t\t\t\tresultDeleteRule, err := client.TSDeleteRule(ctx, \"1\", \"2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultDeleteRule).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultInfo, err := client.TSInfo(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(resultInfo[\"rules\"]).To(BeEquivalentTo([]interface{}{}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(resultInfo[\"rules\"]).To(BeEquivalentTo(map[interface{}]interface{}{}))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs\", Label(\"timeseries\", \"tsincrby\", \"tsdecrby\", \"tsincrbyWithArgs\", \"tsdecrbyWithArgs\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(7.4, \"older redis stack has different results for timeseries module\")\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSIncrBy(ctx, \"1\", 1).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSGet(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(100))\n\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSDecrBy(ctx, \"1\", 1).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSGet(ctx, \"1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(0))\n\n\t\t\t\topt := &redis.TSIncrDecrOptions{Timestamp: 5}\n\t\t\t\t_, err = client.TSIncrByWithArgs(ctx, \"2\", 1.5, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tresult, err = client.TSGet(ctx, \"2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Timestamp).To(BeEquivalentTo(5))\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(1.5))\n\n\t\t\t\topt = &redis.TSIncrDecrOptions{Timestamp: 7}\n\t\t\t\t_, err = client.TSIncrByWithArgs(ctx, \"2\", 2.25, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tresult, err = client.TSGet(ctx, \"2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Timestamp).To(BeEquivalentTo(7))\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(3.75))\n\n\t\t\t\topt = &redis.TSIncrDecrOptions{Timestamp: 15}\n\t\t\t\t_, err = client.TSDecrByWithArgs(ctx, \"2\", 1.5, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tresult, err = client.TSGet(ctx, \"2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Timestamp).To(BeEquivalentTo(15))\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(2.25))\n\n\t\t\t\t// Test chunk size INCRBY\n\t\t\t\topt = &redis.TSIncrDecrOptions{ChunkSize: 128}\n\t\t\t\t_, err = client.TSIncrByWithArgs(ctx, \"3\", 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tresultInfo, err := client.TSInfo(ctx, \"3\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultInfo[\"chunkSize\"]).To(BeEquivalentTo(128))\n\n\t\t\t\t// Test chunk size DECRBY\n\t\t\t\topt = &redis.TSIncrDecrOptions{ChunkSize: 128}\n\t\t\t\t_, err = client.TSDecrByWithArgs(ctx, \"4\", 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tresultInfo, err = client.TSInfo(ctx, \"4\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultInfo[\"chunkSize\"]).To(BeEquivalentTo(128))\n\n\t\t\t\t// Test insertion filters INCRBY\n\t\t\t\topt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: \"LAST\"}\n\t\t\t\tres, err := client.TSIncrByWithArgs(ctx, \"ts-if-1\", 1.0, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(BeEquivalentTo(1000))\n\n\t\t\t\tres, err = client.TSIncrByWithArgs(ctx, \"ts-if-1\", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(BeEquivalentTo(1000))\n\n\t\t\t\trangePoints, err := client.TSRange(ctx, \"ts-if-1\", 1000, 1004).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}}))\n\n\t\t\t\tres, err = client.TSIncrByWithArgs(ctx, \"ts-if-1\", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(BeEquivalentTo(1000))\n\n\t\t\t\trangePoints, err = client.TSRange(ctx, \"ts-if-1\", 1000, 1004).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 11.1}}))\n\n\t\t\t\t// Test insertion filters DECRBY\n\t\t\t\topt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: \"LAST\"}\n\t\t\t\tres, err = client.TSDecrByWithArgs(ctx, \"ts-if-2\", 1.0, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(BeEquivalentTo(1000))\n\n\t\t\t\tres, err = client.TSDecrByWithArgs(ctx, \"ts-if-2\", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(BeEquivalentTo(1000))\n\n\t\t\t\trangePoints, err = client.TSRange(ctx, \"ts-if-2\", 1000, 1004).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -1.0}}))\n\n\t\t\t\tres, err = client.TSDecrByWithArgs(ctx, \"ts-if-2\", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(res).To(BeEquivalentTo(1000))\n\n\t\t\t\trangePoints, err = client.TSRange(ctx, \"ts-if-2\", 1000, 1004).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangePoints)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -11.1}}))\n\t\t\t})\n\n\t\t\tIt(\"should TSGet\", Label(\"timeseries\", \"tsget\"), func() {\n\t\t\t\topt := &redis.TSOptions{DuplicatePolicy: \"max\"}\n\t\t\t\tresultGet, err := client.TSAddWithArgs(ctx, \"foo\", 2265985, 151, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet).To(BeEquivalentTo(2265985))\n\t\t\t\tresult, err := client.TSGet(ctx, \"foo\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Timestamp).To(BeEquivalentTo(2265985))\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(151))\n\t\t\t})\n\n\t\t\tIt(\"should TSGet Latest\", Label(\"timeseries\", \"tsgetlatest\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tresultGet, err := client.TSCreate(ctx, \"tsgl-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultGet, err = client.TSCreate(ctx, \"tsgl-2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultGet, err = client.TSCreateRule(ctx, \"tsgl-1\", \"tsgl-2\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tExpect(resultGet).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t_, err = client.TSAdd(ctx, \"tsgl-1\", 1, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"tsgl-1\", 2, 3).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"tsgl-1\", 11, 7).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"tsgl-1\", 13, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tresult, errGet := client.TSGet(ctx, \"tsgl-2\").Result()\n\t\t\t\tExpect(errGet).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Timestamp).To(BeEquivalentTo(0))\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(4))\n\t\t\t\tresult, errGet = client.TSGetWithArgs(ctx, \"tsgl-2\", &redis.TSGetOptions{Latest: true}).Result()\n\t\t\t\tExpect(errGet).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Timestamp).To(BeEquivalentTo(10))\n\t\t\t\tExpect(result.Value).To(BeEquivalentTo(8))\n\t\t\t})\n\n\t\t\tIt(\"should TSInfo\", Label(\"timeseries\", \"tsinfo\"), func() {\n\t\t\t\tresultGet, err := client.TSAdd(ctx, \"foo\", 2265985, 151).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet).To(BeEquivalentTo(2265985))\n\t\t\t\tresult, err := client.TSInfo(ctx, \"foo\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result[\"firstTimestamp\"]).To(BeEquivalentTo(2265985))\n\t\t\t})\n\n\t\t\tIt(\"should TSMAdd\", Label(\"timeseries\", \"tsmadd\"), func() {\n\t\t\t\tresultGet, err := client.TSCreate(ctx, \"a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultGet).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tktvSlices := make([][]interface{}, 3)\n\t\t\t\tfor i := 0; i < 3; i++ {\n\t\t\t\t\tktvSlices[i] = make([]interface{}, 3)\n\t\t\t\t\tktvSlices[i][0] = \"a\"\n\t\t\t\t\tfor j := 1; j < 3; j++ {\n\t\t\t\t\t\tktvSlices[i][j] = (i + j) * j\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSMAdd(ctx, ktvSlices).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo([]int64{1, 2, 3}))\n\t\t\t})\n\n\t\t\tIt(\"should TSMGet and TSMGetWithArgs\", Label(\"timeseries\", \"tsmget\", \"tsmgetWithArgs\", \"NonRedisEnterprise\"), func() {\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\"}}\n\t\t\t\tresultCreate, err := client.TSCreateWithArgs(ctx, \"a\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\", \"Taste\": \"That\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"b\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", \"*\", 15).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"b\", \"*\", 25).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tresult, err := client.TSMGet(ctx, []string{\"Test=This\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][1].([]interface{})[1]).To(BeEquivalentTo(\"15\"))\n\t\t\t\t\tExpect(result[\"b\"][1].([]interface{})[1]).To(BeEquivalentTo(\"25\"))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][1].([]interface{})[1]).To(BeEquivalentTo(15))\n\t\t\t\t\tExpect(result[\"b\"][1].([]interface{})[1]).To(BeEquivalentTo(25))\n\t\t\t\t}\n\t\t\t\tmgetOpt := &redis.TSMGetOptions{WithLabels: true}\n\t\t\t\tresult, err = client.TSMGetWithArgs(ctx, []string{\"Test=This\"}, mgetOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"b\"][0]).To(ConsistOf([]interface{}{\"Test\", \"This\"}, []interface{}{\"Taste\", \"That\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"b\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{\"Test\": \"This\", \"Taste\": \"That\"}))\n\t\t\t\t}\n\n\t\t\t\tresultCreate, err = client.TSCreate(ctx, \"c\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"is_compaction\": \"true\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"d\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultCreateRule, err := client.TSCreateRule(ctx, \"c\", \"d\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreateRule).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 1, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 2, 3).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 11, 7).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 13, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tresult, err = client.TSMGet(ctx, []string{\"is_compaction=true\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"d\"][1]).To(BeEquivalentTo([]interface{}{int64(0), \"4\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"d\"][1]).To(BeEquivalentTo([]interface{}{int64(0), 4.0}))\n\t\t\t\t}\n\t\t\t\tmgetOpt = &redis.TSMGetOptions{Latest: true}\n\t\t\t\tresult, err = client.TSMGetWithArgs(ctx, []string{\"is_compaction=true\"}, mgetOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"d\"][1]).To(BeEquivalentTo([]interface{}{int64(10), \"8\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"d\"][1]).To(BeEquivalentTo([]interface{}{int64(10), 8.0}))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should TSQueryIndex\", Label(\"timeseries\", \"tsqueryindex\"), func() {\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\"}}\n\t\t\t\tresultCreate, err := client.TSCreateWithArgs(ctx, \"a\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\", \"Taste\": \"That\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"b\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresult, err := client.TSQueryIndex(ctx, []string{\"Test=This\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\tresult, err = client.TSQueryIndex(ctx, []string{\"Taste=That\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t})\n\n\t\t\tIt(\"should TSDel and TSRange\", Label(\"timeseries\", \"tsdel\", \"tsrange\"), func() {\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSAdd(ctx, \"a\", i, float64(i%7)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t\tresultDelete, err := client.TSDel(ctx, \"a\", 0, 21).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultDelete).To(BeEquivalentTo(22))\n\n\t\t\t\tresultRange, err := client.TSRange(ctx, \"a\", 0, 21).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange).To(BeEquivalentTo([]redis.TSTimestampValue{}))\n\n\t\t\t\tresultRange, err = client.TSRange(ctx, \"a\", 22, 22).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 22, Value: 1}))\n\t\t\t})\n\n\t\t\tIt(\"should TSRange, TSRangeWithArgs\", Label(\"timeseries\", \"tsrange\", \"tsrangeWithArgs\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSAdd(ctx, \"a\", i, float64(i%7)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSRange(ctx, \"a\", 0, 200).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(100))\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\tclient.TSAdd(ctx, \"a\", i+200, float64(i%7))\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSRange(ctx, \"a\", 0, 500).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(200))\n\t\t\t\tfts := make([]int, 0)\n\t\t\t\tfor i := 10; i < 20; i++ {\n\t\t\t\t\tfts = append(fts, i)\n\t\t\t\t}\n\t\t\t\topt := &redis.TSRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"a\", 0, 500, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\topt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: \"+\"}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"a\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 10}, {Timestamp: 10, Value: 1}}))\n\t\t\t\topt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: \"5\"}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"a\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 5}, {Timestamp: 5, Value: 6}}))\n\t\t\t\topt = &redis.TSRangeOptions{Aggregator: redis.Twa, BucketDuration: 10}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"a\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 2.55}, {Timestamp: 10, Value: 3}}))\n\t\t\t\t// Test Range Latest\n\t\t\t\tresultCreate, err := client.TSCreate(ctx, \"t1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultCreate, err = client.TSCreate(ctx, \"t2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultRule, err := client.TSCreateRule(ctx, \"t1\", \"t2\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRule).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t_, errAdd := client.TSAdd(ctx, \"t1\", 1, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t1\", 2, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t1\", 11, 7).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t1\", 13, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\tresultRange, err := client.TSRange(ctx, \"t1\", 0, 20).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 1, Value: 1}))\n\n\t\t\t\topt = &redis.TSRangeOptions{Latest: true}\n\t\t\t\tresultRange, err = client.TSRangeWithArgs(ctx, \"t2\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4}))\n\t\t\t\t// Test Bucket Timestamp\n\t\t\t\tresultCreate, err = client.TSCreate(ctx, \"t3\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 15, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 17, 4).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 51, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 73, 5).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 75, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\n\t\t\t\topt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10}\n\t\t\t\tresultRange, err = client.TSRangeWithArgs(ctx, \"t3\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(3))\n\n\t\t\t\topt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: \"+\"}\n\t\t\t\tresultRange, err = client.TSRangeWithArgs(ctx, \"t3\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 20, Value: 4}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(3))\n\t\t\t\t// Test Empty\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 15, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 17, 4).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 51, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 73, 5).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 75, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\n\t\t\t\topt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10}\n\t\t\t\tresultRange, err = client.TSRangeWithArgs(ctx, \"t4\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(3))\n\n\t\t\t\topt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true}\n\t\t\t\tresultRange, err = client.TSRangeWithArgs(ctx, \"t4\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(7))\n\t\t\t})\n\n\t\t\tIt(\"should TSRevRange, TSRevRangeWithArgs\", Label(\"timeseries\", \"tsrevrange\", \"tsrevrangeWithArgs\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSAdd(ctx, \"a\", i, float64(i%7)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSRange(ctx, \"a\", 0, 200).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(100))\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\tclient.TSAdd(ctx, \"a\", i+200, float64(i%7))\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSRange(ctx, \"a\", 0, 500).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(200))\n\n\t\t\t\topt := &redis.TSRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10}\n\t\t\t\tresult, err = client.TSRevRangeWithArgs(ctx, \"a\", 0, 500, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(20))\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Count: 10}\n\t\t\t\tresult, err = client.TSRevRangeWithArgs(ctx, \"a\", 0, 500, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(10))\n\n\t\t\t\tfts := make([]int, 0)\n\t\t\t\tfor i := 10; i < 20; i++ {\n\t\t\t\t\tfts = append(fts, i)\n\t\t\t\t}\n\t\t\t\topt = &redis.TSRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}}\n\t\t\t\tresult, err = client.TSRevRangeWithArgs(ctx, \"a\", 0, 500, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: \"+\"}\n\t\t\t\tresult, err = client.TSRevRangeWithArgs(ctx, \"a\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 1}, {Timestamp: 0, Value: 10}}))\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: \"1\"}\n\t\t\t\tresult, err = client.TSRevRangeWithArgs(ctx, \"a\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1, Value: 10}, {Timestamp: 0, Value: 1}}))\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Aggregator: redis.Twa, BucketDuration: 10}\n\t\t\t\tresult, err = client.TSRevRangeWithArgs(ctx, \"a\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 3}, {Timestamp: 0, Value: 2.55}}))\n\t\t\t\t// Test Range Latest\n\t\t\t\tresultCreate, err := client.TSCreate(ctx, \"t1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultCreate, err = client.TSCreate(ctx, \"t2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultRule, err := client.TSCreateRule(ctx, \"t1\", \"t2\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRule).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t_, errAdd := client.TSAdd(ctx, \"t1\", 1, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t1\", 2, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t1\", 11, 7).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t1\", 13, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\tresultRange, err := client.TSRange(ctx, \"t2\", 0, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4}))\n\t\t\t\topt = &redis.TSRevRangeOptions{Latest: true}\n\t\t\t\tresultRange, err = client.TSRevRangeWithArgs(ctx, \"t2\", 0, 10, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 8}))\n\t\t\t\tresultRange, err = client.TSRevRangeWithArgs(ctx, \"t2\", 0, 9, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4}))\n\t\t\t\t// Test Bucket Timestamp\n\t\t\t\tresultCreate, err = client.TSCreate(ctx, \"t3\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 15, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 17, 4).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 51, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 73, 5).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t3\", 75, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10}\n\t\t\t\tresultRange, err = client.TSRevRangeWithArgs(ctx, \"t3\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(3))\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: \"+\"}\n\t\t\t\tresultRange, err = client.TSRevRangeWithArgs(ctx, \"t3\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 80, Value: 5}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(3))\n\t\t\t\t// Test Empty\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 15, 1).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 17, 4).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 51, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 73, 5).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\t\t\t\t_, errAdd = client.TSAdd(ctx, \"t4\", 75, 3).Result()\n\t\t\t\tExpect(errAdd).NotTo(HaveOccurred())\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10}\n\t\t\t\tresultRange, err = client.TSRevRangeWithArgs(ctx, \"t4\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(3))\n\n\t\t\t\topt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true}\n\t\t\t\tresultRange, err = client.TSRevRangeWithArgs(ctx, \"t4\", 0, 100, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5}))\n\t\t\t\tExpect(len(resultRange)).To(BeEquivalentTo(7))\n\t\t\t})\n\n\t\t\tIt(\"should TSMRange and TSMRangeWithArgs\", Label(\"timeseries\", \"tsmrange\", \"tsmrangeWithArgs\"), func() {\n\t\t\t\tcreateOpt := &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\", \"team\": \"ny\"}}\n\t\t\t\tresultCreate, err := client.TSCreateWithArgs(ctx, \"a\", createOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tcreateOpt = &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\", \"Taste\": \"That\", \"team\": \"sf\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"b\", createOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSAdd(ctx, \"a\", i, float64(i%7)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\t_, err = client.TSAdd(ctx, \"b\", i, float64(i%11)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\n\t\t\t\tresult, err := client.TSMRange(ctx, 0, 200, []string{\"Test=This\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(len(result[\"a\"][1].([]interface{}))).To(BeEquivalentTo(100))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(len(result[\"a\"][2].([]interface{}))).To(BeEquivalentTo(100))\n\t\t\t\t}\n\t\t\t\t// Test Count\n\t\t\t\tmrangeOpt := &redis.TSMRangeOptions{Count: 10}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(len(result[\"a\"][1].([]interface{}))).To(BeEquivalentTo(10))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(len(result[\"a\"][2].([]interface{}))).To(BeEquivalentTo(10))\n\t\t\t\t}\n\t\t\t\t// Test Aggregation and BucketDuration\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSAdd(ctx, \"a\", i+200, float64(i%7)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Avg, BucketDuration: 10}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 500, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(len(result[\"a\"][1].([]interface{}))).To(BeEquivalentTo(20))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(len(result[\"a\"][2].([]interface{}))).To(BeEquivalentTo(20))\n\t\t\t\t}\n\t\t\t\t// Test WithLabels\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo([]interface{}{}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{}))\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{WithLabels: true}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(ConsistOf([]interface{}{[]interface{}{\"Test\", \"This\"}, []interface{}{\"team\", \"ny\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{\"Test\": \"This\", \"team\": \"ny\"}))\n\t\t\t\t}\n\t\t\t\t// Test SelectedLabels\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{SelectedLabels: []interface{}{\"team\"}}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{\"team\", \"ny\"}))\n\t\t\t\t\tExpect(result[\"b\"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{\"team\", \"sf\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{\"team\": \"ny\"}))\n\t\t\t\t\tExpect(result[\"b\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{\"team\": \"sf\"}))\n\t\t\t\t}\n\t\t\t\t// Test FilterBy\n\t\t\t\tfts := make([]int, 0)\n\t\t\t\tfor i := 10; i < 20; i++ {\n\t\t\t\t\tfts = append(fts, i)\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][1].([]interface{})).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), \"1\"}, []interface{}{int64(16), \"2\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), 1.0}, []interface{}{int64(16), 2.0}}))\n\t\t\t\t}\n\t\t\t\t// Test GroupBy\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{GroupByLabel: \"Test\", Reducer: \"sum\"}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"Test=This\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), \"0\"}, []interface{}{int64(1), \"2\"}, []interface{}{int64(2), \"4\"}, []interface{}{int64(3), \"6\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"Test=This\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(3), 6.0}}))\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{GroupByLabel: \"Test\", Reducer: \"max\"}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"Test=This\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), \"0\"}, []interface{}{int64(1), \"1\"}, []interface{}{int64(2), \"2\"}, []interface{}{int64(3), \"3\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"Test=This\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}}))\n\t\t\t\t}\n\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{GroupByLabel: \"team\", Reducer: \"min\"}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"team=ny\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), \"0\"}, []interface{}{int64(1), \"1\"}, []interface{}{int64(2), \"2\"}, []interface{}{int64(3), \"3\"}}))\n\t\t\t\t\tExpect(result[\"team=sf\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), \"0\"}, []interface{}{int64(1), \"1\"}, []interface{}{int64(2), \"2\"}, []interface{}{int64(3), \"3\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"team=ny\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}}))\n\t\t\t\t\tExpect(result[\"team=sf\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}}))\n\t\t\t\t}\n\t\t\t\t// Test Align\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: \"-\"}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{\"team=ny\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), \"10\"}, []interface{}{int64(10), \"1\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 10.0}, []interface{}{int64(10), 1.0}}))\n\t\t\t\t}\n\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 5}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{\"team=ny\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), \"5\"}, []interface{}{int64(5), \"6\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 5.0}, []interface{}{int64(5), 6.0}}))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should TSMRangeWithArgs Latest\", Label(\"timeseries\", \"tsmrangeWithArgs\", \"tsmrangelatest\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tresultCreate, err := client.TSCreate(ctx, \"a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"is_compaction\": \"true\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"b\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultCreate, err = client.TSCreate(ctx, \"c\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"is_compaction\": \"true\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"d\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultCreateRule, err := client.TSCreateRule(ctx, \"a\", \"b\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreateRule).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultCreateRule, err = client.TSCreateRule(ctx, \"c\", \"d\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreateRule).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 1, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 2, 3).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 11, 7).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 13, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 1, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 2, 3).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 11, 7).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 13, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tmrangeOpt := &redis.TSMRangeOptions{Latest: true}\n\t\t\t\tresult, err := client.TSMRangeWithArgs(ctx, 0, 10, []string{\"is_compaction=true\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"b\"][1]).To(ConsistOf([]interface{}{int64(0), \"4\"}, []interface{}{int64(10), \"8\"}))\n\t\t\t\t\tExpect(result[\"d\"][1]).To(ConsistOf([]interface{}{int64(0), \"4\"}, []interface{}{int64(10), \"8\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"b\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}}))\n\t\t\t\t\tExpect(result[\"d\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}}))\n\t\t\t\t}\n\t\t\t})\n\t\t\tIt(\"should TSMRevRange and TSMRevRangeWithArgs\", Label(\"timeseries\", \"tsmrevrange\", \"tsmrevrangeWithArgs\"), func() {\n\t\t\t\tcreateOpt := &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\", \"team\": \"ny\"}}\n\t\t\t\tresultCreate, err := client.TSCreateWithArgs(ctx, \"a\", createOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tcreateOpt = &redis.TSOptions{Labels: map[string]string{\"Test\": \"This\", \"Taste\": \"That\", \"team\": \"sf\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"b\", createOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSAdd(ctx, \"a\", i, float64(i%7)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\t_, err = client.TSAdd(ctx, \"b\", i, float64(i%11)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSMRevRange(ctx, 0, 200, []string{\"Test=This\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(len(result[\"a\"][1].([]interface{}))).To(BeEquivalentTo(100))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(len(result[\"a\"][2].([]interface{}))).To(BeEquivalentTo(100))\n\t\t\t\t}\n\t\t\t\t// Test Count\n\t\t\t\tmrangeOpt := &redis.TSMRevRangeOptions{Count: 10}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(len(result[\"a\"][1].([]interface{}))).To(BeEquivalentTo(10))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(len(result[\"a\"][2].([]interface{}))).To(BeEquivalentTo(10))\n\t\t\t\t}\n\t\t\t\t// Test Aggregation and BucketDuration\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\t_, err := client.TSAdd(ctx, \"a\", i+200, float64(i%7)).Result()\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 500, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(len(result[\"a\"][1].([]interface{}))).To(BeEquivalentTo(20))\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo([]interface{}{}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(len(result[\"a\"][2].([]interface{}))).To(BeEquivalentTo(20))\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{}))\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{WithLabels: true}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(ConsistOf([]interface{}{[]interface{}{\"Test\", \"This\"}, []interface{}{\"team\", \"ny\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{\"Test\": \"This\", \"team\": \"ny\"}))\n\t\t\t\t}\n\t\t\t\t// Test SelectedLabels\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{SelectedLabels: []interface{}{\"team\"}}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{\"team\", \"ny\"}))\n\t\t\t\t\tExpect(result[\"b\"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{\"team\", \"sf\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{\"team\": \"ny\"}))\n\t\t\t\t\tExpect(result[\"b\"][0]).To(BeEquivalentTo(map[interface{}]interface{}{\"team\": \"sf\"}))\n\t\t\t\t}\n\t\t\t\t// Test FilterBy\n\t\t\t\tfts := make([]int, 0)\n\t\t\t\tfor i := 10; i < 20; i++ {\n\t\t\t\t\tfts = append(fts, i)\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][1].([]interface{})).To(ConsistOf([]interface{}{int64(16), \"2\"}, []interface{}{int64(15), \"1\"}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(16), 2.0}, []interface{}{int64(15), 1.0}}))\n\t\t\t\t}\n\t\t\t\t// Test GroupBy\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: \"Test\", Reducer: \"sum\"}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"Test=This\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), \"6\"}, []interface{}{int64(2), \"4\"}, []interface{}{int64(1), \"2\"}, []interface{}{int64(0), \"0\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"Test=This\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 6.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(0), 0.0}}))\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: \"Test\", Reducer: \"max\"}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"Test=This\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), \"3\"}, []interface{}{int64(2), \"2\"}, []interface{}{int64(1), \"1\"}, []interface{}{int64(0), \"0\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"Test=This\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}}))\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: \"team\", Reducer: \"min\"}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{\"Test=This\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"team=ny\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), \"3\"}, []interface{}{int64(2), \"2\"}, []interface{}{int64(1), \"1\"}, []interface{}{int64(0), \"0\"}}))\n\t\t\t\t\tExpect(result[\"team=sf\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), \"3\"}, []interface{}{int64(2), \"2\"}, []interface{}{int64(1), \"1\"}, []interface{}{int64(0), \"0\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"team=ny\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}}))\n\t\t\t\t\tExpect(result[\"team=sf\"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}}))\n\t\t\t\t}\n\t\t\t\t// Test Align\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: \"-\"}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{\"team=ny\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), \"1\"}, []interface{}{int64(0), \"10\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 1.0}, []interface{}{int64(0), 10.0}}))\n\t\t\t\t}\n\t\t\t\tmrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 1}\n\t\t\t\tresult, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{\"team=ny\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"a\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), \"10\"}, []interface{}{int64(0), \"1\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"a\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), 10.0}, []interface{}{int64(0), 1.0}}))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should TSMRevRangeWithArgs Latest\", Label(\"timeseries\", \"tsmrevrangeWithArgs\", \"tsmrevrangelatest\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tresultCreate, err := client.TSCreate(ctx, \"a\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"is_compaction\": \"true\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"b\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultCreate, err = client.TSCreate(ctx, \"c\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\t\t\t\topt = &redis.TSOptions{Labels: map[string]string{\"is_compaction\": \"true\"}}\n\t\t\t\tresultCreate, err = client.TSCreateWithArgs(ctx, \"d\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreate).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\tresultCreateRule, err := client.TSCreateRule(ctx, \"a\", \"b\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreateRule).To(BeEquivalentTo(\"OK\"))\n\t\t\t\tresultCreateRule, err = client.TSCreateRule(ctx, \"c\", \"d\", redis.Sum, 10).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(resultCreateRule).To(BeEquivalentTo(\"OK\"))\n\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 1, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 2, 3).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 11, 7).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"a\", 13, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 1, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 2, 3).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 11, 7).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"c\", 13, 1).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tmrangeOpt := &redis.TSMRevRangeOptions{Latest: true}\n\t\t\t\tresult, err := client.TSMRevRangeWithArgs(ctx, 0, 10, []string{\"is_compaction=true\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tif client.Options().Protocol == 2 {\n\t\t\t\t\tExpect(result[\"b\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), \"8\"}, []interface{}{int64(0), \"4\"}}))\n\t\t\t\t\tExpect(result[\"d\"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), \"8\"}, []interface{}{int64(0), \"4\"}}))\n\t\t\t\t} else {\n\t\t\t\t\tExpect(result[\"b\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}}))\n\t\t\t\t\tExpect(result[\"d\"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}}))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// NaN Value Support Tests\n\t\t\tIt(\"should support NaN values in TSAdd and TSAddWithArgs\", Label(\"timeseries\", \"tsadd\", \"nan\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN support requires Redis 8.6+\")\n\n\t\t\t\t// Test basic NaN insertion with TSAdd\n\t\t\t\tresult, err := client.TSAdd(ctx, \"nan-test-1\", 1000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(1000))\n\n\t\t\t\t// Test NaN insertion with TSAddWithArgs\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"sensor\": \"broken\"}}\n\t\t\t\tresult, err = client.TSAddWithArgs(ctx, \"nan-test-2\", 2000, math.NaN(), opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result).To(BeEquivalentTo(2000))\n\n\t\t\t\t// Verify NaN value can be retrieved\n\t\t\t\tgetResult, err := client.TSGet(ctx, \"nan-test-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(getResult.Timestamp).To(BeEquivalentTo(1000))\n\t\t\t\tExpect(math.IsNaN(getResult.Value)).To(BeTrue())\n\t\t\t})\n\n\t\t\tIt(\"should support NaN values in TSMAdd\", Label(\"timeseries\", \"tsmadd\", \"nan\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN support requires Redis 8.6+\")\n\n\t\t\t\t// Create time series\n\t\t\t\t_, err := client.TSCreate(ctx, \"nan-madd-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSCreate(ctx, \"nan-madd-2\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Add multiple values including NaN\n\t\t\t\tktvSlices := [][]interface{}{\n\t\t\t\t\t{\"nan-madd-1\", 1000, 10.5},\n\t\t\t\t\t{\"nan-madd-1\", 2000, math.NaN()},\n\t\t\t\t\t{\"nan-madd-2\", 1000, math.NaN()},\n\t\t\t\t\t{\"nan-madd-2\", 2000, 20.3},\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSMAdd(ctx, ktvSlices).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(4))\n\n\t\t\t\t// Verify NaN values in range query\n\t\t\t\trangeResult, err := client.TSRange(ctx, \"nan-madd-1\", 0, 3000).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangeResult)).To(BeEquivalentTo(2))\n\t\t\t\tExpect(rangeResult[0].Value).To(BeEquivalentTo(10.5))\n\t\t\t\tExpect(math.IsNaN(rangeResult[1].Value)).To(BeTrue())\n\t\t\t})\n\n\t\t\tIt(\"should retrieve NaN values with TSGet and TSMGet\", Label(\"timeseries\", \"tsget\", \"tsmget\", \"nan\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN support requires Redis 8.6+\")\n\n\t\t\t\t// Add NaN values to multiple time series\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"type\": \"sensor\"}}\n\t\t\t\t_, err := client.TSAddWithArgs(ctx, \"sensor-1\", 1000, math.NaN(), opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAddWithArgs(ctx, \"sensor-2\", 1000, 42.5, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Test TSGet with NaN\n\t\t\t\tgetResult, err := client.TSGet(ctx, \"sensor-1\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(math.IsNaN(getResult.Value)).To(BeTrue())\n\n\t\t\t\t// Test TSMGet with NaN values\n\t\t\t\tmgetResult, err := client.TSMGet(ctx, []string{\"type=sensor\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(mgetResult)).To(BeEquivalentTo(2))\n\n\t\t\t\t// One should be NaN, one should be 42.5\n\t\t\t\tfoundNaN := false\n\t\t\t\tfoundValue := false\n\t\t\t\tfor _, v := range mgetResult {\n\t\t\t\t\ttsVal := v[1].([]interface{})\n\t\t\t\t\tif len(tsVal) == 2 {\n\t\t\t\t\t\tval := tsVal[1]\n\t\t\t\t\t\tif strVal, ok := val.(string); ok {\n\t\t\t\t\t\t\t// RESP2 returns NaN as string\n\t\t\t\t\t\t\tif strVal == \"nan\" || strVal == \"NaN\" || strVal == \"-nan\" {\n\t\t\t\t\t\t\t\tfoundNaN = true\n\t\t\t\t\t\t\t} else if parsedVal, err := strconv.ParseFloat(strVal, 64); err == nil && parsedVal == 42.5 {\n\t\t\t\t\t\t\t\tfoundValue = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if floatVal, ok := val.(float64); ok {\n\t\t\t\t\t\t\t// RESP3 returns NaN as float64\n\t\t\t\t\t\t\tif math.IsNaN(floatVal) {\n\t\t\t\t\t\t\t\tfoundNaN = true\n\t\t\t\t\t\t\t} else if floatVal == 42.5 {\n\t\t\t\t\t\t\t\tfoundValue = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(foundNaN && foundValue).To(BeTrue())\n\t\t\t})\n\n\t\t\tIt(\"should support NaN values in TSRange and TSRevRange\", Label(\"timeseries\", \"tsrange\", \"tsrevrange\", \"nan\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN support requires Redis 8.6+\")\n\n\t\t\t\t// Create time series with mixed NaN and regular values\n\t\t\t\t_, err := client.TSCreate(ctx, \"mixed-values\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Add mixed values\n\t\t\t\t_, err = client.TSAdd(ctx, \"mixed-values\", 1000, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"mixed-values\", 2000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"mixed-values\", 3000, 20.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"mixed-values\", 4000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"mixed-values\", 5000, 30.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Test TSRange\n\t\t\t\trangeResult, err := client.TSRange(ctx, \"mixed-values\", 0, 6000).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(rangeResult)).To(BeEquivalentTo(5))\n\t\t\t\tExpect(rangeResult[0].Value).To(BeEquivalentTo(10.0))\n\t\t\t\tExpect(math.IsNaN(rangeResult[1].Value)).To(BeTrue())\n\t\t\t\tExpect(rangeResult[2].Value).To(BeEquivalentTo(20.0))\n\t\t\t\tExpect(math.IsNaN(rangeResult[3].Value)).To(BeTrue())\n\t\t\t\tExpect(rangeResult[4].Value).To(BeEquivalentTo(30.0))\n\n\t\t\t\t// Test TSRevRange\n\t\t\t\trevRangeResult, err := client.TSRevRange(ctx, \"mixed-values\", 0, 6000).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(revRangeResult)).To(BeEquivalentTo(5))\n\t\t\t\tExpect(revRangeResult[0].Value).To(BeEquivalentTo(30.0))\n\t\t\t\tExpect(math.IsNaN(revRangeResult[1].Value)).To(BeTrue())\n\t\t\t\tExpect(revRangeResult[2].Value).To(BeEquivalentTo(20.0))\n\t\t\t\tExpect(math.IsNaN(revRangeResult[3].Value)).To(BeTrue())\n\t\t\t\tExpect(revRangeResult[4].Value).To(BeEquivalentTo(10.0))\n\t\t\t})\n\n\t\t\tIt(\"should support CountNaN and CountAll aggregators\", Label(\"timeseries\", \"aggregator\", \"nan\", \"countnan\", \"countall\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN aggregators require Redis 8.6+\")\n\n\t\t\t\t// Create time series with mixed NaN and regular values\n\t\t\t\t_, err := client.TSCreate(ctx, \"agg-test\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Add values: 3 regular values and 2 NaN values\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-test\", 1000, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-test\", 2000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-test\", 3000, 20.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-test\", 4000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-test\", 5000, 30.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Test CountNaN aggregator\n\t\t\t\topt := &redis.TSRangeOptions{\n\t\t\t\t\tAggregator:     redis.CountNaN,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSRangeWithArgs(ctx, \"agg-test\", 0, 10000, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(result[0].Value).To(BeEquivalentTo(2.0)) // 2 NaN values\n\n\t\t\t\t// Test CountAll aggregator\n\t\t\t\topt = &redis.TSRangeOptions{\n\t\t\t\t\tAggregator:     redis.CountAll,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"agg-test\", 0, 10000, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(result[0].Value).To(BeEquivalentTo(5.0)) // 5 total values\n\t\t\t})\n\n\t\t\tIt(\"should ignore NaN values in existing aggregators\", Label(\"timeseries\", \"aggregator\", \"nan\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN support requires Redis 8.6+\")\n\n\t\t\t\t// Create time series with mixed NaN and regular values\n\t\t\t\t_, err := client.TSCreate(ctx, \"agg-ignore-nan\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Add values: 10, NaN, 20, NaN, 30\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-ignore-nan\", 1000, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-ignore-nan\", 2000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-ignore-nan\", 3000, 20.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-ignore-nan\", 4000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"agg-ignore-nan\", 5000, 30.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Test AVG aggregator (should ignore NaN: (10+20+30)/3 = 20)\n\t\t\t\topt := &redis.TSRangeOptions{\n\t\t\t\t\tAggregator:     redis.Avg,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSRangeWithArgs(ctx, \"agg-ignore-nan\", 0, 10000, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(result[0].Value).To(BeEquivalentTo(20.0))\n\n\t\t\t\t// Test SUM aggregator (should ignore NaN: 10+20+30 = 60)\n\t\t\t\topt = &redis.TSRangeOptions{\n\t\t\t\t\tAggregator:     redis.Sum,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"agg-ignore-nan\", 0, 10000, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(result[0].Value).To(BeEquivalentTo(60.0))\n\n\t\t\t\t// Test MIN aggregator (should ignore NaN: min = 10)\n\t\t\t\topt = &redis.TSRangeOptions{\n\t\t\t\t\tAggregator:     redis.Min,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"agg-ignore-nan\", 0, 10000, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(result[0].Value).To(BeEquivalentTo(10.0))\n\n\t\t\t\t// Test MAX aggregator (should ignore NaN: max = 30)\n\t\t\t\topt = &redis.TSRangeOptions{\n\t\t\t\t\tAggregator:     redis.Max,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"agg-ignore-nan\", 0, 10000, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(result[0].Value).To(BeEquivalentTo(30.0))\n\n\t\t\t\t// Test COUNT aggregator (should ignore NaN: count = 3)\n\t\t\t\topt = &redis.TSRangeOptions{\n\t\t\t\t\tAggregator:     redis.Count,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSRangeWithArgs(ctx, \"agg-ignore-nan\", 0, 10000, opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(1))\n\t\t\t\tExpect(result[0].Value).To(BeEquivalentTo(3.0))\n\t\t\t})\n\n\t\t\tIt(\"should support NaN values in TSMRange and TSMRevRange\", Label(\"timeseries\", \"tsmrange\", \"tsmrevrange\", \"nan\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN support requires Redis 8.6+\")\n\n\t\t\t\t// Create multiple time series with NaN values\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"location\": \"sensor-room\"}}\n\t\t\t\t_, err := client.TSCreateWithArgs(ctx, \"mrange-1\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSCreateWithArgs(ctx, \"mrange-2\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Add mixed values to both series\n\t\t\t\t_, err = client.TSAdd(ctx, \"mrange-1\", 1000, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"mrange-1\", 2000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"mrange-2\", 1000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"mrange-2\", 2000, 20.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Test TSMRange\n\t\t\t\tmrangeResult, err := client.TSMRange(ctx, 0, 3000, []string{\"location=sensor-room\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(mrangeResult)).To(BeEquivalentTo(2))\n\n\t\t\t\t// Test TSMRevRange\n\t\t\t\tmrevrangeResult, err := client.TSMRevRange(ctx, 0, 3000, []string{\"location=sensor-room\"}).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(mrevrangeResult)).To(BeEquivalentTo(2))\n\t\t\t})\n\n\t\t\tIt(\"should support NaN with CountNaN and CountAll in TSMRange\", Label(\"timeseries\", \"tsmrange\", \"aggregator\", \"nan\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN aggregators require Redis 8.6+\")\n\n\t\t\t\t// Create multiple time series with NaN values\n\t\t\t\topt := &redis.TSOptions{Labels: map[string]string{\"device\": \"temp-sensor\"}}\n\t\t\t\t_, err := client.TSCreateWithArgs(ctx, \"multi-agg-1\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSCreateWithArgs(ctx, \"multi-agg-2\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Add values with NaN\n\t\t\t\t_, err = client.TSAdd(ctx, \"multi-agg-1\", 1000, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"multi-agg-1\", 2000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"multi-agg-1\", 3000, 20.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t_, err = client.TSAdd(ctx, \"multi-agg-2\", 1000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"multi-agg-2\", 2000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t_, err = client.TSAdd(ctx, \"multi-agg-2\", 3000, 30.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Test CountNaN with TSMRange\n\t\t\t\tmrangeOpt := &redis.TSMRangeOptions{\n\t\t\t\t\tAggregator:     redis.CountNaN,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err := client.TSMRangeWithArgs(ctx, 0, 10000, []string{\"device=temp-sensor\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\n\t\t\t\t// Test CountAll with TSMRange\n\t\t\t\tmrangeOpt = &redis.TSMRangeOptions{\n\t\t\t\t\tAggregator:     redis.CountAll,\n\t\t\t\t\tBucketDuration: 10000,\n\t\t\t\t}\n\t\t\t\tresult, err = client.TSMRangeWithArgs(ctx, 0, 10000, []string{\"device=temp-sensor\"}, mrangeOpt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(len(result)).To(BeEquivalentTo(2))\n\t\t\t})\n\n\t\t\tIt(\"should handle duplicate policy with NaN values\", Label(\"timeseries\", \"nan\", \"duplicatepolicy\", \"NonRedisEnterprise\"), func() {\n\t\t\t\tSkipBeforeRedisVersion(8.6, \"NaN support requires Redis 8.6+\")\n\n\t\t\t\t// Test BLOCK duplicate policy with NaN (should work - just blocks duplicates)\n\t\t\t\topt := &redis.TSOptions{DuplicatePolicy: \"BLOCK\"}\n\t\t\t\t_, err := client.TSCreateWithArgs(ctx, \"dup-block\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t_, err = client.TSAdd(ctx, \"dup-block\", 1000, math.NaN()).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tgetResult, err := client.TSGet(ctx, \"dup-block\").Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(math.IsNaN(getResult.Value)).To(BeTrue())\n\n\t\t\t\t// Trying to add another value at the same timestamp should be blocked\n\t\t\t\t_, err = client.TSAdd(ctx, \"dup-block\", 1000, 20.0).Result()\n\t\t\t\tExpect(err).To(HaveOccurred())\n\n\t\t\t\t// Test that MIN/MAX/SUM policies error when mixing NaN and non-NaN\n\t\t\t\topt = &redis.TSOptions{DuplicatePolicy: \"MIN\"}\n\t\t\t\t_, err = client.TSCreateWithArgs(ctx, \"dup-min\", opt).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t_, err = client.TSAdd(ctx, \"dup-min\", 1000, 10.0).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Adding NaN to existing non-NaN value should error for MIN policy\n\t\t\t\t_, err = client.TSAdd(ctx, \"dup-min\", 1000, math.NaN()).Result()\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t})\n\n\t\t\tIt(\"should verify Aggregator.String() returns correct values for CountNaN and CountAll\", Label(\"timeseries\", \"aggregator\", \"unit\"), func() {\n\t\t\t\t// Unit test for aggregator string representation\n\t\t\t\tExpect(redis.CountNaN.String()).To(Equal(\"COUNTNAN\"))\n\t\t\t\tExpect(redis.CountAll.String()).To(Equal(\"COUNTALL\"))\n\n\t\t\t\t// Verify other aggregators still work\n\t\t\t\tExpect(redis.Avg.String()).To(Equal(\"AVG\"))\n\t\t\t\tExpect(redis.Sum.String()).To(Equal(\"SUM\"))\n\t\t\t\tExpect(redis.Count.String()).To(Equal(\"COUNT\"))\n\t\t\t})\n\t\t})\n\t}\n})\n"
  },
  {
    "path": "tls_cert_auth_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc init() {\n\t// Initialize RedisVersion from environment variable for regular Go tests\n\t// (Ginkgo tests initialize this in BeforeSuite)\n\tif version := os.Getenv(\"REDIS_VERSION\"); version != \"\" {\n\t\tif v, err := strconv.ParseFloat(strings.Trim(version, \"\\\"\"), 64); err == nil && v > 0 {\n\t\t\tRedisVersion = v\n\t\t}\n\t}\n}\n\n// skipBeforeRedisVersion checks if Redis version is below the specified version and skips the test if so\nfunc skipBeforeRedisVersion(t *testing.T, version float64, msg string) {\n\tt.Helper()\n\tif RedisVersion < version {\n\t\tt.Skipf(\"Skipping test: Redis version %.1f < %.1f: %s\", RedisVersion, version, msg)\n\t}\n}\n\n// TestTLSCertificateAuthentication tests that Redis automatically authenticates\n// a user based on the CN field in the client's TLS certificate.\n//\n// This test requires:\n// 1. Redis 8.6+ configured with: tls-auth-clients-user CN\n// 2. A client certificate with CN matching the Redis ACL username\n// 3. The Docker image generates testcertuser.{crt,key} when TLS_CLIENT_CNS=testcertuser\n//\n// The test flow:\n// 1. Create a Redis ACL user with a specific username (testcertuser)\n// 2. Load the pre-generated client certificate with that username in the CN field\n// 3. Connect using TLS with that certificate\n// 4. Verify that Redis automatically authenticates as that user (no AUTH command needed)\nfunc TestTLSCertificateAuthentication(t *testing.T) {\n\tskipBeforeRedisVersion(t, 8.6, \"tls-auth-clients-user CN requires Redis 8.6+\")\n\n\tctx := context.Background()\n\ttestUsername := \"testcertuser\"\n\ttlsCertDir := \"dockers/standalone/tls\"\n\n\t// Step 1: Create a non-TLS client to set up the ACL user\n\tsetupClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\", // Non-TLS port\n\t})\n\tdefer setupClient.Close()\n\n\t// Verify connection\n\tif err := setupClient.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"Redis not available: %v\", err)\n\t}\n\n\t// Clean up any existing test user\n\tsetupClient.ACLDelUser(ctx, testUsername)\n\n\t// Step 2: Create ACL user with specific permissions\n\t// The user can read/write keys but has limited command access\n\terr := setupClient.ACLSetUser(ctx,\n\t\ttestUsername,\n\t\t\"on\",          // Enable the user\n\t\t\"nopass\",      // No password required (will use cert auth)\n\t\t\"~*\",          // Can access all keys\n\t\t\"+get\",        // Allow GET command\n\t\t\"+set\",        // Allow SET command\n\t\t\"+ping\",       // Allow PING command\n\t\t\"+acl|whoami\", // Allow ACL WHOAMI command\n\t).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create ACL user: %v\", err)\n\t}\n\tdefer setupClient.ACLDelUser(ctx, testUsername) // Cleanup\n\n\t// Verify user was created\n\tusers, err := setupClient.ACLUsers(ctx).Result()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to list ACL users: %v\", err)\n\t}\n\tt.Logf(\"ACL users: %v\", users)\n\n\t// Step 3: Load CA certificate for server verification\n\tcaCertPEM, err := os.ReadFile(tlsCertDir + \"/ca.crt\")\n\tif err != nil {\n\t\tt.Fatalf(\"CA cert not found: %v\", err)\n\t}\n\n\t// Step 4: Load the pre-generated client certificate with CN=testcertuser\n\t// This certificate is generated by the Docker image when TLS_CLIENT_CNS=testcertuser\n\tclientCert, err := tls.LoadX509KeyPair(\n\t\ttlsCertDir+\"/\"+testUsername+\".crt\",\n\t\ttlsCertDir+\"/\"+testUsername+\".key\",\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)\", err, testUsername)\n\t}\n\n\t// Step 5: Create TLS config with the client certificate\n\tcaCertPool := x509.NewCertPool()\n\tcaCertPool.AppendCertsFromPEM(caCertPEM)\n\n\ttlsConfig := &tls.Config{\n\t\tRootCAs:            caCertPool,\n\t\tCertificates:       []tls.Certificate{clientCert},\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // Using self-signed certs\n\t}\n\n\t// Step 6: Connect with TLS using the certificate\n\t// NOTE: This test requires Redis to be configured with:\n\t//   tls-auth-clients-user CN\n\t// Without this config, the certificate CN won't be used for authentication\n\ttlsClient := redis.NewClient(&redis.Options{\n\t\tAddr:      \"localhost:6666\", // TLS port\n\t\tTLSConfig: tlsConfig,\n\t\t// NO Username/Password - authentication should happen via certificate!\n\t})\n\tdefer tlsClient.Close()\n\n\t// Step 7: Verify we're authenticated as the correct user\n\twhoami, err := tlsClient.ACLWhoAmI(ctx).Result()\n\tif err != nil {\n\t\tt.Fatalf(\"ACL WHOAMI failed: %v (this test requires Redis to be configured with: tls-auth-clients-user CN)\", err)\n\t}\n\n\tif whoami != testUsername {\n\t\tt.Fatalf(\"Expected to be authenticated as %q, but got %q. Ensure Redis is configured with: tls-auth-clients-user CN\", testUsername, whoami)\n\t}\n\tt.Logf(\"✅ Successfully authenticated as %q using certificate CN\", whoami)\n\n\t// Step 8: Test that we can execute allowed commands\n\terr = tlsClient.Set(ctx, \"test_cert_auth_key\", \"test_value\", 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"SET command failed (should be allowed): %v\", err)\n\t}\n\n\tval, err := tlsClient.Get(ctx, \"test_cert_auth_key\").Result()\n\tif err != nil {\n\t\tt.Fatalf(\"GET command failed (should be allowed): %v\", err)\n\t}\n\tif val != \"test_value\" {\n\t\tt.Errorf(\"Expected 'test_value', got %q\", val)\n\t}\n\n\t// Step 9: Test that we CANNOT execute disallowed commands\n\t// The user doesn't have +del permission, so this should fail\n\terr = tlsClient.Del(ctx, \"test_cert_auth_key\").Err()\n\tif err == nil {\n\t\tt.Error(\"DEL command succeeded but should have failed (user doesn't have +del permission)\")\n\t} else {\n\t\tt.Logf(\"✅ DEL command correctly denied: %v\", err)\n\t}\n\n\t// Cleanup\n\tsetupClient.Del(ctx, \"test_cert_auth_key\")\n\n\tt.Log(\"✅ TLS certificate authentication test passed\")\n}\n\n// TestTLSCertificateAuthenticationNoUser tests that when a certificate CN\n// doesn't match any existing ACL user, Redis falls back to the default user.\n//\n// This test:\n// 1. Ensures the testcertuser ACL user does NOT exist\n// 2. Connects with a certificate that has CN=testcertuser\n// 3. Verifies that Redis authenticates as \"default\" (fallback behavior)\nfunc TestTLSCertificateAuthenticationNoUser(t *testing.T) {\n\tskipBeforeRedisVersion(t, 8.6, \"tls-auth-clients-user CN requires Redis 8.6+\")\n\n\tctx := context.Background()\n\ttestUsername := \"testcertuser\"\n\ttlsCertDir := \"dockers/standalone/tls\"\n\n\t// Step 1: Create a non-TLS client to ensure the user does NOT exist\n\tsetupClient := redis.NewClient(&redis.Options{\n\t\tAddr: \"localhost:6379\", // Non-TLS port\n\t})\n\tdefer setupClient.Close()\n\n\t// Verify connection\n\tif err := setupClient.Ping(ctx).Err(); err != nil {\n\t\tt.Fatalf(\"Redis not available: %v\", err)\n\t}\n\n\t// Delete the test user if it exists - we want to test fallback behavior\n\tsetupClient.ACLDelUser(ctx, testUsername)\n\n\t// Verify user does not exist\n\tusers, err := setupClient.ACLUsers(ctx).Result()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to list ACL users: %v\", err)\n\t}\n\tfor _, u := range users {\n\t\tif u == testUsername {\n\t\t\tt.Fatalf(\"User %q should not exist for this test\", testUsername)\n\t\t}\n\t}\n\tt.Logf(\"ACL users (should not contain %s): %v\", testUsername, users)\n\n\t// Step 2: Load CA certificate for server verification\n\tcaCertPEM, err := os.ReadFile(tlsCertDir + \"/ca.crt\")\n\tif err != nil {\n\t\tt.Fatalf(\"CA cert not found: %v\", err)\n\t}\n\n\t// Step 3: Load the client certificate with CN=testcertuser\n\t// Even though the user doesn't exist, we still use this certificate\n\tclientCert, err := tls.LoadX509KeyPair(\n\t\ttlsCertDir+\"/\"+testUsername+\".crt\",\n\t\ttlsCertDir+\"/\"+testUsername+\".key\",\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)\", err, testUsername)\n\t}\n\n\t// Step 4: Create TLS config with the client certificate\n\tcaCertPool := x509.NewCertPool()\n\tcaCertPool.AppendCertsFromPEM(caCertPEM)\n\n\ttlsConfig := &tls.Config{\n\t\tRootCAs:            caCertPool,\n\t\tCertificates:       []tls.Certificate{clientCert},\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // Using self-signed certs\n\t}\n\n\t// Step 5: Connect with TLS using the certificate\n\ttlsClient := redis.NewClient(&redis.Options{\n\t\tAddr:      \"localhost:6666\", // TLS port\n\t\tTLSConfig: tlsConfig,\n\t})\n\tdefer tlsClient.Close()\n\n\t// Step 6: Verify we're authenticated as \"default\" (fallback)\n\twhoami, err := tlsClient.ACLWhoAmI(ctx).Result()\n\tif err != nil {\n\t\tt.Fatalf(\"ACL WHOAMI failed: %v (Redis may not be configured for certificate-based authentication)\", err)\n\t}\n\n\t// When the CN user doesn't exist, Redis should fall back to \"default\"\n\tif whoami != \"default\" {\n\t\tt.Fatalf(\"Expected to be authenticated as %q (fallback), but got %q\", \"default\", whoami)\n\t}\n\tt.Logf(\"✅ Correctly fell back to %q user (CN user %q does not exist)\", whoami, testUsername)\n\n\t// Step 7: Verify we can execute commands as default user\n\terr = tlsClient.Set(ctx, \"test_cert_auth_fallback_key\", \"fallback_value\", 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"SET command failed: %v\", err)\n\t}\n\n\tval, err := tlsClient.Get(ctx, \"test_cert_auth_fallback_key\").Result()\n\tif err != nil {\n\t\tt.Fatalf(\"GET command failed: %v\", err)\n\t}\n\tif val != \"fallback_value\" {\n\t\tt.Errorf(\"Expected 'fallback_value', got %q\", val)\n\t}\n\n\t// Cleanup\n\ttlsClient.Del(ctx, \"test_cert_auth_fallback_key\")\n\n\tt.Log(\"✅ TLS certificate authentication fallback test passed\")\n}\n"
  },
  {
    "path": "tls_cluster_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"TLS Cluster\", Label(\"NonRedisEnterprise\"), func() {\n\tvar tlsConfig *tls.Config\n\tvar clusterAddrs []string\n\n\tBeforeEach(func() {\n\t\tvar err error\n\t\tcertDir := \"dockers/osscluster-tls/tls\"\n\t\ttlsConfig, err = loadClusterTLSConfig(certDir)\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t// TLS cluster ports from docker-compose.yml\n\t\t// TLS_PORT=5430 to avoid conflict with sentinel-cluster (which uses 4430-4432)\n\t\tclusterAddrs = []string{\n\t\t\t\"localhost:5430\",\n\t\t\t\"localhost:5431\",\n\t\t\t\"localhost:5432\",\n\t\t\t\"localhost:5433\",\n\t\t\t\"localhost:5434\",\n\t\t\t\"localhost:5435\",\n\t\t}\n\t})\n\n\tDescribe(\"Cluster Client with TLS\", func() {\n\t\tIt(\"should connect to TLS cluster\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should perform basic operations over TLS cluster\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\t// SET\n\t\t\terr := client.Set(ctx, \"tls_cluster_key\", \"tls_cluster_value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// GET\n\t\t\tval, err := client.Get(ctx, \"tls_cluster_key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"tls_cluster_value\"))\n\n\t\t\t// DEL\n\t\t\tdeleted, err := client.Del(ctx, \"tls_cluster_key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(deleted).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should support pipelining over TLS cluster\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\t// Use hash tags to ensure keys go to same slot\n\t\t\tpipe := client.Pipeline()\n\t\t\tsetCmd := pipe.Set(ctx, \"{tls}pipe_key1\", \"value1\", 0)\n\t\t\tgetCmd := pipe.Get(ctx, \"{tls}pipe_key1\")\n\t\t\tdelCmd := pipe.Del(ctx, \"{tls}pipe_key1\")\n\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(setCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getCmd.Val()).To(Equal(\"value1\"))\n\t\t\tExpect(delCmd.Val()).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should support cluster commands over TLS\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\t// CLUSTER INFO\n\t\t\tinfo, err := client.ClusterInfo(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(info).To(ContainSubstring(\"cluster_state:ok\"))\n\n\t\t\t// CLUSTER NODES\n\t\t\tnodes, err := client.ClusterNodes(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(nodes).NotTo(BeEmpty())\n\n\t\t\t// CLUSTER SLOTS\n\t\t\tslots, err := client.ClusterSlots(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(slots)).To(BeNumerically(\">\", 0))\n\t\t})\n\n\t\tIt(\"should support pub/sub over TLS cluster\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\tpubsub := client.Subscribe(ctx, \"tls_cluster_channel\")\n\t\t\tdefer pubsub.Close()\n\n\t\t\t// Wait for subscription confirmation\n\t\t\t_, err := pubsub.Receive(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Publish a message\n\t\t\terr = client.Publish(ctx, \"tls_cluster_channel\", \"tls_cluster_message\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Receive the message\n\t\t\tmsg, err := pubsub.ReceiveMessage(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msg.Channel).To(Equal(\"tls_cluster_channel\"))\n\t\t\tExpect(msg.Payload).To(Equal(\"tls_cluster_message\"))\n\t\t})\n\n\t\tIt(\"should handle cluster redirects over TLS\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\t// Set multiple keys that will be distributed across cluster\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tkey := fmt.Sprintf(\"tls_redirect_key_%d\", i)\n\t\t\t\terr := client.Set(ctx, key, fmt.Sprintf(\"value_%d\", i), 0).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\t// Verify all keys can be retrieved\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tkey := fmt.Sprintf(\"tls_redirect_key_%d\", i)\n\t\t\t\tval, err := client.Get(ctx, key).Result()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(val).To(Equal(fmt.Sprintf(\"value_%d\", i)))\n\t\t\t}\n\n\t\t\t// Cleanup\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tkey := fmt.Sprintf(\"tls_redirect_key_%d\", i)\n\t\t\t\tclient.Del(ctx, key)\n\t\t\t}\n\t\t})\n\n\t\tIt(\"should connect with InsecureSkipVerify to TLS cluster\", func() {\n\t\t\tctx := context.Background()\n\t\t\tinsecureTLSConfig := &tls.Config{\n\t\t\t\tInsecureSkipVerify: true,\n\t\t\t}\n\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: insecureTLSConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should support ForEachShard over TLS cluster\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\tvar shardCount int64\n\t\t\terr := client.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {\n\t\t\t\tatomic.AddInt64(&shardCount, 1)\n\t\t\t\treturn shard.Ping(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(atomic.LoadInt64(&shardCount)).To(BeNumerically(\">\", 0))\n\t\t})\n\n\t\tIt(\"should support ForEachMaster over TLS cluster\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\t\tAddrs:     clusterAddrs,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Wait for cluster to be ready\n\t\t\tEventually(func() error {\n\t\t\t\treturn client.Ping(ctx).Err()\n\t\t\t}, 30*time.Second, 1*time.Second).Should(Succeed())\n\n\t\t\tvar masterCount int64\n\t\t\terr := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {\n\t\t\t\tatomic.AddInt64(&masterCount, 1)\n\t\t\t\treturn master.Ping(ctx).Err()\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(atomic.LoadInt64(&masterCount)).To(BeNumerically(\">\", 0))\n\t\t})\n\t})\n\n\tDescribe(\"TLS Cluster URL Parsing\", func() {\n\t\tIt(\"should parse rediss:// URL for cluster\", func() {\n\t\t\topt, err := redis.ParseClusterURL(\"rediss://localhost:16800?addr=localhost:16801&addr=localhost:16802\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(opt.Addrs).To(ContainElements(\"localhost:16800\", \"localhost:16801\", \"localhost:16802\"))\n\t\t\tExpect(opt.TLSConfig).NotTo(BeNil())\n\t\t\tExpect(opt.TLSConfig.ServerName).To(Equal(\"localhost\"))\n\t\t})\n\t})\n})\n\n"
  },
  {
    "path": "tls_standalone_test.go",
    "content": "package redis_test\n\nimport (\n\t\"crypto/tls\"\n\t\"testing\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// TestTLSStandalone tests TLS connection to standalone Redis\nfunc TestTLSStandalone(t *testing.T) {\n\t// Use InsecureSkipVerify for testing with self-signed certificates\n\ttlsConfig := &tls.Config{\n\t\tInsecureSkipVerify: true,\n\t}\n\n\tclient := redis.NewClient(&redis.Options{\n\t\tAddr:      \"localhost:6666\",\n\t\tTLSConfig: tlsConfig,\n\t})\n\tdefer client.Close()\n\n\t// Test PING\n\tval, err := client.Ping(ctx).Result()\n\tif err != nil {\n\t\tt.Fatalf(\"PING failed: %v\", err)\n\t}\n\tif val != \"PONG\" {\n\t\tt.Fatalf(\"Expected PONG, got %s\", val)\n\t}\n\n\t// Test SET/GET\n\terr = client.Set(ctx, \"tls_test_key\", \"tls_test_value\", 0).Err()\n\tif err != nil {\n\t\tt.Fatalf(\"SET failed: %v\", err)\n\t}\n\n\tval, err = client.Get(ctx, \"tls_test_key\").Result()\n\tif err != nil {\n\t\tt.Fatalf(\"GET failed: %v\", err)\n\t}\n\tif val != \"tls_test_value\" {\n\t\tt.Fatalf(\"Expected tls_test_value, got %s\", val)\n\t}\n\n\t// Cleanup\n\tclient.Del(ctx, \"tls_test_key\")\n\n\tt.Log(\"✅ TLS standalone test passed\")\n}\n\n// TestTLSRedissURL tests rediss:// URL scheme\nfunc TestTLSRedissURL(t *testing.T) {\n\topt, err := redis.ParseURL(\"rediss://localhost:6666\")\n\tif err != nil {\n\t\tt.Fatalf(\"ParseURL failed: %v\", err)\n\t}\n\n\t// Override TLS config to skip verification for self-signed certs\n\topt.TLSConfig = &tls.Config{\n\t\tInsecureSkipVerify: true,\n\t}\n\n\tclient := redis.NewClient(opt)\n\tdefer client.Close()\n\n\tval, err := client.Ping(ctx).Result()\n\tif err != nil {\n\t\tt.Fatalf(\"PING failed: %v\", err)\n\t}\n\tif val != \"PONG\" {\n\t\tt.Fatalf(\"Expected PONG, got %s\", val)\n\t}\n\n\tt.Log(\"✅ TLS rediss:// URL test passed\")\n}\n\n"
  },
  {
    "path": "tls_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// loadTLSConfig loads TLS certificates from the docker test environment\nfunc loadTLSConfig(certDir string) (*tls.Config, error) {\n\t// Load CA cert\n\tcaCert, err := os.ReadFile(filepath.Join(certDir, \"ca.crt\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcaCertPool := x509.NewCertPool()\n\tcaCertPool.AppendCertsFromPEM(caCert)\n\n\t// Load client cert and key\n\tcert, err := tls.LoadX509KeyPair(\n\t\tfilepath.Join(certDir, \"client.crt\"),\n\t\tfilepath.Join(certDir, \"client.key\"),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &tls.Config{\n\t\tRootCAs:      caCertPool,\n\t\tCertificates: []tls.Certificate{cert},\n\t\tServerName:   \"localhost\",\n\t\tInsecureSkipVerify: true,\n\t}, nil\n}\n\nvar _ = Describe(\"TLS\", Label(\"NonRedisEnterprise\"), func() {\n\tvar tlsConfig *tls.Config\n\tvar tlsPort = \"6666\" // TLS port from docker-compose.yml\n\n\tBeforeEach(func() {\n\t\tvar err error\n\t\t// Load TLS config from docker test certificates\n\t\ttlsConfig, err = loadTLSConfig(\"dockers/standalone/tls\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n\n\tDescribe(\"Standalone Redis with TLS\", func() {\n\t\tIt(\"should connect with TLS using Options\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should connect with TLS using rediss:// URL\", func() {\n\t\t\tctx := context.Background()\n\t\t\topt, err := redis.ParseURL(\"rediss://localhost:\" + tlsPort)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Override TLS config with our certificates\n\t\t\topt.TLSConfig = tlsConfig\n\n\t\t\tclient := redis.NewClient(opt)\n\t\t\tdefer client.Close()\n\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should perform basic operations over TLS\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// SET\n\t\t\terr := client.Set(ctx, \"tls_test_key\", \"tls_test_value\", 0).Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// GET\n\t\t\tval, err := client.Get(ctx, \"tls_test_key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"tls_test_value\"))\n\n\t\t\t// DEL\n\t\t\tdeleted, err := client.Del(ctx, \"tls_test_key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(deleted).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should support pipelining over TLS\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\tpipe := client.Pipeline()\n\t\t\tsetCmd := pipe.Set(ctx, \"tls_pipe_key1\", \"value1\", 0)\n\t\t\tgetCmd := pipe.Get(ctx, \"tls_pipe_key1\")\n\t\t\tdelCmd := pipe.Del(ctx, \"tls_pipe_key1\")\n\n\t\t\t_, err := pipe.Exec(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tExpect(setCmd.Err()).NotTo(HaveOccurred())\n\t\t\tExpect(getCmd.Val()).To(Equal(\"value1\"))\n\t\t\tExpect(delCmd.Val()).To(Equal(int64(1)))\n\t\t})\n\n\t\tIt(\"should support transactions over TLS\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Set(ctx, \"tls_tx_key\", \"tx_value\", 0)\n\t\t\t\t\tpipe.Get(ctx, \"tls_tx_key\")\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn err\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tval, err := client.Get(ctx, \"tls_tx_key\").Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"tx_value\"))\n\n\t\t\t// Cleanup\n\t\t\tclient.Del(ctx, \"tls_tx_key\")\n\t\t})\n\n\t\tIt(\"should support pub/sub over TLS\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\tpubsub := client.Subscribe(ctx, \"tls_test_channel\")\n\t\t\tdefer pubsub.Close()\n\n\t\t\t// Wait for subscription confirmation\n\t\t\t_, err := pubsub.Receive(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Publish a message\n\t\t\terr = client.Publish(ctx, \"tls_test_channel\", \"tls_test_message\").Err()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t// Receive the message\n\t\t\tmsg, err := pubsub.ReceiveMessage(ctx)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(msg.Channel).To(Equal(\"tls_test_channel\"))\n\t\t\tExpect(msg.Payload).To(Equal(\"tls_test_message\"))\n\t\t})\n\n\t\tIt(\"should fail to connect without TLS to TLS port\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr: \"localhost:\" + tlsPort,\n\t\t\t\t// No TLS config\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\terr := client.Ping(ctx).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should fail to connect with invalid certificates\", func() {\n\t\t\tctx := context.Background()\n\t\t\tinvalidTLSConfig := &tls.Config{\n\t\t\t\tInsecureSkipVerify: false,\n\t\t\t\tServerName:         \"invalid.example.com\",\n\t\t\t}\n\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: invalidTLSConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\terr := client.Ping(ctx).Err()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\n\t\tIt(\"should connect with InsecureSkipVerify\", func() {\n\t\t\tctx := context.Background()\n\t\t\tinsecureTLSConfig := &tls.Config{\n\t\t\t\tInsecureSkipVerify: true,\n\t\t\t}\n\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: insecureTLSConfig,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should support connection pooling over TLS\", func() {\n\t\t\tctx := context.Background()\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfig,\n\t\t\t\tPoolSize:  10,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\t// Perform multiple operations to use different connections\n\t\t\tfor i := 0; i < 20; i++ {\n\t\t\t\terr := client.Ping(ctx).Err()\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}\n\n\t\t\tstats := client.PoolStats()\n\t\t\tExpect(stats.Hits).To(BeNumerically(\">\", 0))\n\t\t})\n\t})\n\n\tDescribe(\"TLS Configuration Options\", func() {\n\t\tIt(\"should respect MinVersion setting\", func() {\n\t\t\tctx := context.Background()\n\t\t\ttlsConfigWithMinVersion := &tls.Config{\n\t\t\t\tRootCAs:      tlsConfig.RootCAs,\n\t\t\t\tCertificates: tlsConfig.Certificates,\n\t\t\t\tServerName:   \"localhost\",\n\t\tInsecureSkipVerify: true,\n\t\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t\t}\n\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfigWithMinVersion,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\n\t\tIt(\"should work with custom cipher suites\", func() {\n\t\t\tctx := context.Background()\n\t\t\ttlsConfigWithCiphers := &tls.Config{\n\t\t\t\tRootCAs:      tlsConfig.RootCAs,\n\t\t\t\tCertificates: tlsConfig.Certificates,\n\t\t\t\tServerName:   \"localhost\",\n\t\tInsecureSkipVerify: true,\n\t\t\t\tCipherSuites: []uint16{\n\t\t\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\t\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tclient := redis.NewClient(&redis.Options{\n\t\t\t\tAddr:      \"localhost:\" + tlsPort,\n\t\t\t\tTLSConfig: tlsConfigWithCiphers,\n\t\t\t})\n\t\t\tdefer client.Close()\n\n\t\t\tval, err := client.Ping(ctx).Result()\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(val).To(Equal(\"PONG\"))\n\t\t})\n\t})\n})\n\n"
  },
  {
    "path": "tx.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\n// TxFailedErr transaction redis failed.\nconst TxFailedErr = proto.RedisError(\"redis: transaction failed\")\n\n// Tx implements Redis transactions as described in\n// https://redis.io/docs/latest/develop/using-commands/transactions. It's NOT safe for concurrent use\n// by multiple goroutines, because Exec resets list of watched keys.\n//\n// If you don't need WATCH, use Pipeline instead.\ntype Tx struct {\n\tbaseClient\n\tcmdable\n\tstatefulCmdable\n}\n\nfunc (c *Client) newTx() *Tx {\n\ttx := Tx{\n\t\tbaseClient: baseClient{\n\t\t\topt:           c.cloneOpt(), // Clone options under optLock to avoid race with initConn\n\t\t\tconnPool:      pool.NewStickyConnPool(c.connPool),\n\t\t\thooksMixin:    c.hooksMixin.clone(),\n\t\t\tpushProcessor: c.pushProcessor, // Copy push processor from parent client\n\t\t},\n\t}\n\ttx.init()\n\treturn &tx\n}\n\nfunc (c *Tx) init() {\n\tc.cmdable = c.Process\n\tc.statefulCmdable = c.Process\n\n\tc.initHooks(hooks{\n\t\tdial:       c.baseClient.dial,\n\t\tprocess:    c.baseClient.process,\n\t\tpipeline:   c.baseClient.processPipeline,\n\t\ttxPipeline: c.baseClient.processTxPipeline,\n\t})\n}\n\nfunc (c *Tx) Process(ctx context.Context, cmd Cmder) error {\n\terr := c.processHook(ctx, cmd)\n\tcmd.SetErr(err)\n\treturn err\n}\n\n// Watch prepares a transaction and marks the keys to be watched\n// for conditional execution if there are any keys.\n//\n// The transaction is automatically closed when fn exits.\nfunc (c *Client) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {\n\ttx := c.newTx()\n\tdefer tx.Close(ctx)\n\tif len(keys) > 0 {\n\t\tif err := tx.Watch(ctx, keys...).Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn fn(tx)\n}\n\n// Close closes the transaction, releasing any open resources.\nfunc (c *Tx) Close(ctx context.Context) error {\n\t_ = c.Unwatch(ctx).Err()\n\treturn c.baseClient.Close()\n}\n\n// Watch marks the keys to be watched for conditional execution\n// of a transaction.\nfunc (c *Tx) Watch(ctx context.Context, keys ...string) *StatusCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"watch\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Unwatch flushes all the previously watched keys for a transaction.\nfunc (c *Tx) Unwatch(ctx context.Context, keys ...string) *StatusCmd {\n\targs := make([]interface{}, 1+len(keys))\n\targs[0] = \"unwatch\"\n\tfor i, key := range keys {\n\t\targs[1+i] = key\n\t}\n\tcmd := NewStatusCmd(ctx, args...)\n\t_ = c.Process(ctx, cmd)\n\treturn cmd\n}\n\n// Pipeline creates a pipeline. Usually it is more convenient to use Pipelined.\nfunc (c *Tx) Pipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: func(ctx context.Context, cmds []Cmder) error {\n\t\t\treturn c.processPipelineHook(ctx, cmds)\n\t\t},\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\n// Pipelined executes commands queued in the fn outside of the transaction.\n// Use TxPipelined if you need transactional behavior.\nfunc (c *Tx) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.Pipeline().Pipelined(ctx, fn)\n}\n\n// TxPipelined executes commands queued in the fn in the transaction.\n//\n// When using WATCH, EXEC will execute commands only if the watched keys\n// were not modified, allowing for a check-and-set mechanism.\n//\n// Exec always returns list of commands. If transaction fails\n// TxFailedErr is returned. Otherwise Exec returns an error of the first\n// failed command or nil.\nfunc (c *Tx) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {\n\treturn c.TxPipeline().Pipelined(ctx, fn)\n}\n\n// TxPipeline creates a pipeline. Usually it is more convenient to use TxPipelined.\nfunc (c *Tx) TxPipeline() Pipeliner {\n\tpipe := Pipeline{\n\t\texec: func(ctx context.Context, cmds []Cmder) error {\n\t\t\tcmds = wrapMultiExec(ctx, cmds)\n\t\t\treturn c.processTxPipelineHook(ctx, cmds)\n\t\t},\n\t}\n\tpipe.init()\n\treturn &pipe\n}\n\nfunc wrapMultiExec(ctx context.Context, cmds []Cmder) []Cmder {\n\tif len(cmds) == 0 {\n\t\tpanic(\"not reached\")\n\t}\n\tcmdsCopy := make([]Cmder, len(cmds)+2)\n\tcmdsCopy[0] = NewStatusCmd(ctx, \"multi\")\n\tcopy(cmdsCopy[1:], cmds)\n\tcmdsCopy[len(cmdsCopy)-1] = NewSliceCmd(ctx, \"exec\")\n\treturn cmdsCopy\n}\n"
  },
  {
    "path": "tx_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"sync\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"Tx\", func() {\n\tvar client *redis.Client\n\n\tBeforeEach(func() {\n\t\tclient = redis.NewClient(redisOptions())\n\t\tExpect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tAfterEach(func() {\n\t\tExpect(client.Close()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should Watch\", func() {\n\t\tvar incr func(string) error\n\n\t\t// Transactionally increments key using GET and SET commands.\n\t\tincr = func(key string) error {\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\tn, err := tx.Get(ctx, key).Int64()\n\t\t\t\tif err != nil && err != redis.Nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Set(ctx, key, strconv.FormatInt(n+1, 10), 0)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn err\n\t\t\t}, key)\n\t\t\tif err == redis.TxFailedErr {\n\t\t\t\treturn incr(key)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tvar wg sync.WaitGroup\n\t\tfor i := 0; i < 100; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer GinkgoRecover()\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\terr := incr(\"key\")\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\tn, err := client.Get(ctx, \"key\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(n).To(Equal(int64(100)))\n\t})\n\n\tIt(\"should discard\", Label(\"NonRedisEnterprise\"), func() {\n\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\tcmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tpipe.Set(ctx, \"key1\", \"hello1\", 0)\n\t\t\t\tpipe.Discard()\n\t\t\t\tpipe.Set(ctx, \"key2\", \"hello2\", 0)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(cmds).To(HaveLen(1))\n\t\t\treturn err\n\t\t}, \"key1\", \"key2\")\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tget := client.Get(ctx, \"key1\")\n\t\tExpect(get.Err()).To(Equal(redis.Nil))\n\t\tExpect(get.Val()).To(Equal(\"\"))\n\n\t\tget = client.Get(ctx, \"key2\")\n\t\tExpect(get.Err()).NotTo(HaveOccurred())\n\t\tExpect(get.Val()).To(Equal(\"hello2\"))\n\t})\n\n\tIt(\"returns no error when there are no commands\", func() {\n\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t_, err := tx.TxPipelined(ctx, func(redis.Pipeliner) error { return nil })\n\t\t\treturn err\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tv, err := client.Ping(ctx).Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(v).To(Equal(\"PONG\"))\n\t})\n\n\tIt(\"should exec bulks\", func() {\n\t\tconst N = 20000\n\n\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\tcmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\tfor i := 0; i < N; i++ {\n\t\t\t\t\tpipe.Incr(ctx, \"key\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(len(cmds)).To(Equal(N))\n\t\t\tfor _, cmd := range cmds {\n\t\t\t\tExpect(cmd.Err()).NotTo(HaveOccurred())\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tnum, err := client.Get(ctx, \"key\").Int64()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t\tExpect(num).To(Equal(int64(N)))\n\t})\n\n\tIt(\"should recover from bad connection\", func() {\n\t\t// Put bad connection in the pool.\n\t\tcn, err := client.Pool().Get(context.Background())\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tcn.SetNetConn(&badConn{})\n\t\tclient.Pool().Put(ctx, cn)\n\n\t\tdo := func() error {\n\t\t\terr := client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\t\t_, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {\n\t\t\t\t\tpipe.Ping(ctx)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t\treturn err\n\t\t\t})\n\t\t\treturn err\n\t\t}\n\n\t\terr = do()\n\t\tExpect(err).NotTo(HaveOccurred())\n\t})\n})\n"
  },
  {
    "path": "unit_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n)\n\n// mockCmdable is a mock implementation of cmdable that records the last command.\n// This is used for unit testing command construction without requiring a Redis server.\ntype mockCmdable struct {\n\tlastCmd   Cmder\n\treturnErr error\n}\n\nfunc (m *mockCmdable) call(_ context.Context, cmd Cmder) error {\n\tm.lastCmd = cmd\n\tif m.returnErr != nil {\n\t\tcmd.SetErr(m.returnErr)\n\t}\n\treturn m.returnErr\n}\n\nfunc (m *mockCmdable) asCmdable() cmdable {\n\treturn func(ctx context.Context, cmd Cmder) error {\n\t\treturn m.call(ctx, cmd)\n\t}\n}\n"
  },
  {
    "path": "universal.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\n// UniversalOptions information is required by UniversalClient to establish\n// connections.\ntype UniversalOptions struct {\n\t// Either a single address or a seed list of host:port addresses\n\t// of cluster/sentinel nodes.\n\tAddrs []string\n\n\t// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.\n\tClientName string\n\n\t// Database to be selected after connecting to the server.\n\t// Only single-node and failover clients.\n\tDB int\n\n\t// Common options.\n\n\tDialer    func(ctx context.Context, network, addr string) (net.Conn, error)\n\tOnConnect func(ctx context.Context, cn *Conn) error\n\n\tProtocol int\n\tUsername string\n\tPassword string\n\t// CredentialsProvider allows the username and password to be updated\n\t// before reconnecting. It should return the current username and password.\n\tCredentialsProvider func() (username string, password string)\n\n\t// CredentialsProviderContext is an enhanced parameter of CredentialsProvider,\n\t// done to maintain API compatibility. In the future,\n\t// there might be a merge between CredentialsProviderContext and CredentialsProvider.\n\t// There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider.\n\tCredentialsProviderContext func(ctx context.Context) (username string, password string, err error)\n\n\t// StreamingCredentialsProvider is used to retrieve the credentials\n\t// for the connection from an external source. Those credentials may change\n\t// during the connection lifetime. This is useful for managed identity\n\t// scenarios where the credentials are retrieved from an external source.\n\t//\n\t// Currently, this is a placeholder for the future implementation.\n\tStreamingCredentialsProvider auth.StreamingCredentialsProvider\n\n\tSentinelUsername string\n\tSentinelPassword string\n\n\tMaxRetries      int\n\tMinRetryBackoff time.Duration\n\tMaxRetryBackoff time.Duration\n\n\tDialTimeout time.Duration\n\n\t// DialerRetries is the maximum number of retry attempts when dialing fails.\n\t//\n\t// default: 5\n\tDialerRetries int\n\n\t// DialerRetryTimeout is the backoff duration between retry attempts.\n\t//\n\t// default: 100 milliseconds\n\tDialerRetryTimeout time.Duration\n\n\tReadTimeout           time.Duration\n\tWriteTimeout          time.Duration\n\tContextTimeoutEnabled bool\n\n\t// ReadBufferSize is the size of the bufio.Reader buffer for each connection.\n\t// Larger buffers can improve performance for commands that return large responses.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tReadBufferSize int\n\n\t// WriteBufferSize is the size of the bufio.Writer buffer for each connection.\n\t// Larger buffers can improve performance for large pipelines and commands with many arguments.\n\t// Smaller buffers can improve memory usage for larger pools.\n\t//\n\t// default: 32KiB (32768 bytes)\n\tWriteBufferSize int\n\n\t// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).\n\tPoolFIFO bool\n\n\tPoolSize int\n\n\t// MaxConcurrentDials is the maximum number of concurrent connection creation goroutines.\n\t// If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize.\n\tMaxConcurrentDials int\n\n\tPoolTimeout           time.Duration\n\tMinIdleConns          int\n\tMaxIdleConns          int\n\tMaxActiveConns        int\n\tConnMaxIdleTime       time.Duration\n\tConnMaxLifetime       time.Duration\n\tConnMaxLifetimeJitter time.Duration\n\n\tTLSConfig *tls.Config\n\n\t// Only cluster clients.\n\n\tMaxRedirects   int\n\tReadOnly       bool\n\tRouteByLatency bool\n\tRouteRandomly  bool\n\n\t// MasterName is the sentinel master name.\n\t// Only for failover clients.\n\tMasterName string\n\n\t// DisableIndentity - Disable set-lib on connect.\n\t//\n\t// default: false\n\t//\n\t// Deprecated: Use DisableIdentity instead.\n\tDisableIndentity bool\n\n\t// DisableIdentity is used to disable CLIENT SETINFO command on connect.\n\t//\n\t// default: false\n\tDisableIdentity bool\n\n\tIdentitySuffix string\n\n\t// FailingTimeoutSeconds is the timeout in seconds for marking a cluster node as failing.\n\t// When a node is marked as failing, it will be avoided for this duration.\n\t// Only applies to cluster clients. Default is 15 seconds.\n\tFailingTimeoutSeconds int\n\n\tUnstableResp3 bool\n\n\t// PushNotificationProcessor is the processor for handling push notifications.\n\t// If nil, a default processor will be created for RESP3 connections.\n\tPushNotificationProcessor push.NotificationProcessor\n\n\t// IsClusterMode can be used when only one Addrs is provided (e.g. Elasticache supports setting up cluster mode with configuration endpoint).\n\tIsClusterMode bool\n\n\t// MaintNotificationsConfig provides configuration for maintnotifications upgrades.\n\tMaintNotificationsConfig *maintnotifications.Config\n}\n\n// Cluster returns cluster options created from the universal options.\nfunc (o *UniversalOptions) Cluster() *ClusterOptions {\n\tif len(o.Addrs) == 0 {\n\t\to.Addrs = []string{\"127.0.0.1:6379\"}\n\t}\n\n\treturn &ClusterOptions{\n\t\tAddrs:      o.Addrs,\n\t\tClientName: o.ClientName,\n\t\tDialer:     o.Dialer,\n\t\tOnConnect:  o.OnConnect,\n\n\t\tProtocol:                     o.Protocol,\n\t\tUsername:                     o.Username,\n\t\tPassword:                     o.Password,\n\t\tCredentialsProvider:          o.CredentialsProvider,\n\t\tCredentialsProviderContext:   o.CredentialsProviderContext,\n\t\tStreamingCredentialsProvider: o.StreamingCredentialsProvider,\n\n\t\tMaxRedirects:   o.MaxRedirects,\n\t\tReadOnly:       o.ReadOnly,\n\t\tRouteByLatency: o.RouteByLatency,\n\t\tRouteRandomly:  o.RouteRandomly,\n\n\t\tMaxRetries:      o.MaxRetries,\n\t\tMinRetryBackoff: o.MinRetryBackoff,\n\t\tMaxRetryBackoff: o.MaxRetryBackoff,\n\n\t\tDialTimeout:        o.DialTimeout,\n\t\tDialerRetries:      o.DialerRetries,\n\t\tDialerRetryTimeout: o.DialerRetryTimeout,\n\t\tReadTimeout:        o.ReadTimeout,\n\t\tWriteTimeout:       o.WriteTimeout,\n\n\t\tContextTimeoutEnabled: o.ContextTimeoutEnabled,\n\n\t\tReadBufferSize:  o.ReadBufferSize,\n\t\tWriteBufferSize: o.WriteBufferSize,\n\n\t\tPoolFIFO:              o.PoolFIFO,\n\t\tPoolSize:              o.PoolSize,\n\t\tMaxConcurrentDials:    o.MaxConcurrentDials,\n\t\tPoolTimeout:           o.PoolTimeout,\n\t\tMinIdleConns:          o.MinIdleConns,\n\t\tMaxIdleConns:          o.MaxIdleConns,\n\t\tMaxActiveConns:        o.MaxActiveConns,\n\t\tConnMaxIdleTime:       o.ConnMaxIdleTime,\n\t\tConnMaxLifetime:       o.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter,\n\n\t\tTLSConfig: o.TLSConfig,\n\n\t\tDisableIdentity:           o.DisableIdentity,\n\t\tDisableIndentity:          o.DisableIndentity,\n\t\tIdentitySuffix:            o.IdentitySuffix,\n\t\tFailingTimeoutSeconds:     o.FailingTimeoutSeconds,\n\t\tUnstableResp3:             o.UnstableResp3,\n\t\tPushNotificationProcessor: o.PushNotificationProcessor,\n\t\tMaintNotificationsConfig:  o.MaintNotificationsConfig,\n\t}\n}\n\n// Failover returns failover options created from the universal options.\nfunc (o *UniversalOptions) Failover() *FailoverOptions {\n\tif len(o.Addrs) == 0 {\n\t\to.Addrs = []string{\"127.0.0.1:26379\"}\n\t}\n\n\treturn &FailoverOptions{\n\t\tSentinelAddrs: o.Addrs,\n\t\tMasterName:    o.MasterName,\n\t\tClientName:    o.ClientName,\n\n\t\tDialer:    o.Dialer,\n\t\tOnConnect: o.OnConnect,\n\n\t\tDB:                           o.DB,\n\t\tProtocol:                     o.Protocol,\n\t\tUsername:                     o.Username,\n\t\tPassword:                     o.Password,\n\t\tCredentialsProvider:          o.CredentialsProvider,\n\t\tCredentialsProviderContext:   o.CredentialsProviderContext,\n\t\tStreamingCredentialsProvider: o.StreamingCredentialsProvider,\n\n\t\tSentinelUsername: o.SentinelUsername,\n\t\tSentinelPassword: o.SentinelPassword,\n\n\t\tRouteByLatency: o.RouteByLatency,\n\t\tRouteRandomly:  o.RouteRandomly,\n\n\t\tMaxRetries:      o.MaxRetries,\n\t\tMinRetryBackoff: o.MinRetryBackoff,\n\t\tMaxRetryBackoff: o.MaxRetryBackoff,\n\n\t\tDialTimeout:        o.DialTimeout,\n\t\tDialerRetries:      o.DialerRetries,\n\t\tDialerRetryTimeout: o.DialerRetryTimeout,\n\t\tReadTimeout:        o.ReadTimeout,\n\t\tWriteTimeout:       o.WriteTimeout,\n\n\t\tContextTimeoutEnabled: o.ContextTimeoutEnabled,\n\n\t\tReadBufferSize:  o.ReadBufferSize,\n\t\tWriteBufferSize: o.WriteBufferSize,\n\n\t\tPoolFIFO:              o.PoolFIFO,\n\t\tPoolSize:              o.PoolSize,\n\t\tMaxConcurrentDials:    o.MaxConcurrentDials,\n\t\tPoolTimeout:           o.PoolTimeout,\n\t\tMinIdleConns:          o.MinIdleConns,\n\t\tMaxIdleConns:          o.MaxIdleConns,\n\t\tMaxActiveConns:        o.MaxActiveConns,\n\t\tConnMaxIdleTime:       o.ConnMaxIdleTime,\n\t\tConnMaxLifetime:       o.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter,\n\n\t\tTLSConfig: o.TLSConfig,\n\n\t\tReplicaOnly: o.ReadOnly,\n\n\t\tDisableIdentity:           o.DisableIdentity,\n\t\tDisableIndentity:          o.DisableIndentity,\n\t\tIdentitySuffix:            o.IdentitySuffix,\n\t\tUnstableResp3:             o.UnstableResp3,\n\t\tPushNotificationProcessor: o.PushNotificationProcessor,\n\t\t// Note: MaintNotificationsConfig not supported for FailoverOptions\n\t}\n}\n\n// Simple returns basic options created from the universal options.\nfunc (o *UniversalOptions) Simple() *Options {\n\taddr := \"127.0.0.1:6379\"\n\tif len(o.Addrs) > 0 {\n\t\taddr = o.Addrs[0]\n\t}\n\n\treturn &Options{\n\t\tAddr:       addr,\n\t\tClientName: o.ClientName,\n\t\tDialer:     o.Dialer,\n\t\tOnConnect:  o.OnConnect,\n\n\t\tDB:                           o.DB,\n\t\tProtocol:                     o.Protocol,\n\t\tUsername:                     o.Username,\n\t\tPassword:                     o.Password,\n\t\tCredentialsProvider:          o.CredentialsProvider,\n\t\tCredentialsProviderContext:   o.CredentialsProviderContext,\n\t\tStreamingCredentialsProvider: o.StreamingCredentialsProvider,\n\n\t\tMaxRetries:      o.MaxRetries,\n\t\tMinRetryBackoff: o.MinRetryBackoff,\n\t\tMaxRetryBackoff: o.MaxRetryBackoff,\n\n\t\tDialTimeout:        o.DialTimeout,\n\t\tDialerRetries:      o.DialerRetries,\n\t\tDialerRetryTimeout: o.DialerRetryTimeout,\n\t\tReadTimeout:        o.ReadTimeout,\n\t\tWriteTimeout:       o.WriteTimeout,\n\n\t\tContextTimeoutEnabled: o.ContextTimeoutEnabled,\n\n\t\tReadBufferSize:  o.ReadBufferSize,\n\t\tWriteBufferSize: o.WriteBufferSize,\n\n\t\tPoolFIFO:              o.PoolFIFO,\n\t\tPoolSize:              o.PoolSize,\n\t\tMaxConcurrentDials:    o.MaxConcurrentDials,\n\t\tPoolTimeout:           o.PoolTimeout,\n\t\tMinIdleConns:          o.MinIdleConns,\n\t\tMaxIdleConns:          o.MaxIdleConns,\n\t\tMaxActiveConns:        o.MaxActiveConns,\n\t\tConnMaxIdleTime:       o.ConnMaxIdleTime,\n\t\tConnMaxLifetime:       o.ConnMaxLifetime,\n\t\tConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter,\n\n\t\tTLSConfig: o.TLSConfig,\n\n\t\tDisableIdentity:           o.DisableIdentity,\n\t\tDisableIndentity:          o.DisableIndentity,\n\t\tIdentitySuffix:            o.IdentitySuffix,\n\t\tUnstableResp3:             o.UnstableResp3,\n\t\tPushNotificationProcessor: o.PushNotificationProcessor,\n\t\tMaintNotificationsConfig:  o.MaintNotificationsConfig,\n\t}\n}\n\n// --------------------------------------------------------------------\n\n// UniversalClient is an abstract client which - based on the provided options -\n// represents either a ClusterClient, a FailoverClient, or a single-node Client.\n// This can be useful for testing cluster-specific applications locally or having different\n// clients in different environments.\ntype UniversalClient interface {\n\tCmdable\n\tAddHook(Hook)\n\tWatch(ctx context.Context, fn func(*Tx) error, keys ...string) error\n\tDo(ctx context.Context, args ...interface{}) *Cmd\n\tProcess(ctx context.Context, cmd Cmder) error\n\tSubscribe(ctx context.Context, channels ...string) *PubSub\n\tPSubscribe(ctx context.Context, channels ...string) *PubSub\n\tSSubscribe(ctx context.Context, channels ...string) *PubSub\n\tClose() error\n\tPoolStats() *PoolStats\n}\n\nvar (\n\t_ UniversalClient = (*Client)(nil)\n\t_ UniversalClient = (*ClusterClient)(nil)\n\t_ UniversalClient = (*Ring)(nil)\n)\n\n// NewUniversalClient returns a new multi client. The type of the returned client depends\n// on the following conditions:\n//\n//  1. If the MasterName option is specified with RouteByLatency, RouteRandomly or IsClusterMode,\n//     a FailoverClusterClient is returned.\n//  2. If the MasterName option is specified without RouteByLatency, RouteRandomly or IsClusterMode,\n//     a sentinel-backed FailoverClient is returned.\n//  3. If the number of Addrs is two or more, or IsClusterMode option is specified,\n//     a ClusterClient is returned.\n//  4. Otherwise, a single-node Client is returned.\nfunc NewUniversalClient(opts *UniversalOptions) UniversalClient {\n\tif opts == nil {\n\t\tpanic(\"redis: NewUniversalClient nil options\")\n\t}\n\n\tswitch {\n\tcase opts.MasterName != \"\" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode):\n\t\treturn NewFailoverClusterClient(opts.Failover())\n\tcase opts.MasterName != \"\":\n\t\treturn NewFailoverClient(opts.Failover())\n\tcase len(opts.Addrs) > 1 || opts.IsClusterMode:\n\t\treturn NewClusterClient(opts.Cluster())\n\tdefault:\n\t\treturn NewClient(opts.Simple())\n\t}\n}\n"
  },
  {
    "path": "universal_test.go",
    "content": "package redis_test\n\nimport (\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ = Describe(\"UniversalClient\", func() {\n\tvar client redis.UniversalClient\n\n\tAfterEach(func() {\n\t\tif client != nil {\n\t\t\tExpect(client.Close()).To(Succeed())\n\t\t}\n\t})\n\n\tIt(\"should connect to failover servers\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tMasterName: sentinelName,\n\t\t\tAddrs:      sentinelAddrs,\n\t\t})\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should connect to failover cluster\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tMasterName:    sentinelName,\n\t\t\tRouteRandomly: true,\n\t\t\tAddrs:         sentinelAddrs,\n\t\t})\n\t\t_, ok := client.(*redis.ClusterClient)\n\t\tExpect(ok).To(BeTrue(), \"expected a ClusterClient\")\n\t})\n\n\tIt(\"should connect to simple servers\", func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tAddrs: []string{redisAddr},\n\t\t})\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"should connect to clusters\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tAddrs: cluster.addrs(),\n\t\t})\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t})\n\n\tIt(\"connect to clusters with UniversalClient and UnstableResp3\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tAddrs:         cluster.addrs(),\n\t\t\tProtocol:      3,\n\t\t\tUnstableResp3: true,\n\t\t})\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t\ta := func() { client.FTInfo(ctx, \"all\").Result() }\n\t\tExpect(a).ToNot(Panic())\n\t})\n\n\tIt(\"connect to clusters with ClusterClient and UnstableResp3\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\tAddrs:         cluster.addrs(),\n\t\t\tProtocol:      3,\n\t\t\tUnstableResp3: true,\n\t\t})\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\t\ta := func() { client.FTInfo(ctx, \"all\").Result() }\n\t\tExpect(a).ToNot(Panic())\n\t})\n\n\tIt(\"should connect to failover servers on slaves when readonly Options is ok\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tMasterName: sentinelName,\n\t\t\tAddrs:      sentinelAddrs,\n\t\t\tReadOnly:   true,\n\t\t})\n\t\tExpect(client.Ping(ctx).Err()).NotTo(HaveOccurred())\n\n\t\troleCmd := client.Do(ctx, \"ROLE\")\n\t\trole, err := roleCmd.Result()\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\troleSlice, ok := role.([]interface{})\n\t\tExpect(ok).To(BeTrue())\n\t\tExpect(roleSlice[0]).To(Equal(\"slave\"))\n\n\t\terr = client.Set(ctx, \"somekey\", \"somevalue\", 0).Err()\n\t\tExpect(err).To(HaveOccurred())\n\t})\n\n\tIt(\"should connect to clusters if IsClusterMode is set even if only a single address is provided\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tAddrs:         []string{cluster.addrs()[0]},\n\t\t\tIsClusterMode: true,\n\t\t})\n\t\t_, ok := client.(*redis.ClusterClient)\n\t\tExpect(ok).To(BeTrue(), \"expected a ClusterClient\")\n\t})\n\n\tIt(\"should return all slots after instantiating UniversalClient with IsClusterMode\", Label(\"NonRedisEnterprise\"), func() {\n\t\tclient = redis.NewUniversalClient(&redis.UniversalOptions{\n\t\t\tAddrs:         []string{cluster.addrs()[0]},\n\t\t\tIsClusterMode: true,\n\t\t})\n\t\tExpect(client.ClusterSlots(ctx).Val()).To(HaveLen(3))\n\t})\n})\n"
  },
  {
    "path": "vectorset_commands.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strconv\"\n)\n\n// note: the APIs is experimental and may be subject to change.\ntype VectorSetCmdable interface {\n\tVAdd(ctx context.Context, key, element string, val Vector) *BoolCmd\n\tVAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd\n\tVCard(ctx context.Context, key string) *IntCmd\n\tVDim(ctx context.Context, key string) *IntCmd\n\tVEmb(ctx context.Context, key, element string, raw bool) *SliceCmd\n\tVGetAttr(ctx context.Context, key, element string) *StringCmd\n\tVInfo(ctx context.Context, key string) *MapStringInterfaceCmd\n\tVLinks(ctx context.Context, key, element string) *StringSliceCmd\n\tVLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd\n\tVRandMember(ctx context.Context, key string) *StringCmd\n\tVRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd\n\tVRem(ctx context.Context, key, element string) *BoolCmd\n\tVSetAttr(ctx context.Context, key, element string, attr interface{}) *BoolCmd\n\tVClearAttributes(ctx context.Context, key, element string) *BoolCmd\n\tVSim(ctx context.Context, key string, val Vector) *StringSliceCmd\n\tVSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd\n\tVSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd\n\tVSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreSliceCmd\n\tVRange(ctx context.Context, key, start, end string, count int64) *StringSliceCmd\n}\n\ntype Vector interface {\n\tValue() []any\n}\n\nconst (\n\tvectorFormatFP32   string = \"FP32\"\n\tvectorFormatValues string = \"Values\"\n)\n\ntype VectorFP32 struct {\n\tVal []byte\n}\n\nfunc (v *VectorFP32) Value() []any {\n\treturn []any{vectorFormatFP32, v.Val}\n}\n\nvar _ Vector = (*VectorFP32)(nil)\n\ntype VectorValues struct {\n\tVal []float64\n}\n\nfunc (v *VectorValues) Value() []any {\n\tres := make([]any, 2+len(v.Val))\n\tres[0] = vectorFormatValues\n\tres[1] = len(v.Val)\n\tfor i, v := range v.Val {\n\t\tres[2+i] = v\n\t}\n\treturn res\n}\n\nvar _ Vector = (*VectorValues)(nil)\n\ntype VectorRef struct {\n\tName string // the name of the referent vector\n}\n\nfunc (v *VectorRef) Value() []any {\n\treturn []any{\"ele\", v.Name}\n}\n\nvar _ Vector = (*VectorRef)(nil)\n\ntype VectorScore struct {\n\tName  string\n\tScore float64\n}\n\n// `VADD key (FP32 | VALUES num) vector element`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd {\n\treturn c.VAddWithArgs(ctx, key, element, val, &VAddArgs{})\n}\n\ntype VAddArgs struct {\n\t// the REDUCE option must be passed immediately after the key\n\tReduce int64\n\tCas    bool\n\n\t// The NoQuant, Q8 and Bin options are mutually exclusive.\n\tNoQuant bool\n\tQ8      bool\n\tBin     bool\n\n\tEF      int64\n\tSetAttr string\n\tM       int64\n}\n\nfunc (v VAddArgs) reduce() int64 {\n\treturn v.Reduce\n}\n\nfunc (v VAddArgs) appendArgs(args []any) []any {\n\tif v.Cas {\n\t\targs = append(args, \"cas\")\n\t}\n\n\tif v.NoQuant {\n\t\targs = append(args, \"noquant\")\n\t} else if v.Q8 {\n\t\targs = append(args, \"q8\")\n\t} else if v.Bin {\n\t\targs = append(args, \"bin\")\n\t}\n\n\tif v.EF > 0 {\n\t\targs = append(args, \"ef\", strconv.FormatInt(v.EF, 10))\n\t}\n\tif len(v.SetAttr) > 0 {\n\t\targs = append(args, \"setattr\", v.SetAttr)\n\t}\n\tif v.M > 0 {\n\t\targs = append(args, \"m\", strconv.FormatInt(v.M, 10))\n\t}\n\treturn args\n}\n\n// `VADD key [REDUCE dim] (FP32 | VALUES num) vector element [CAS] [NOQUANT | Q8 | BIN] [EF build-exploration-factor] [SETATTR attributes] [M numlinks]`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd {\n\tif addArgs == nil {\n\t\taddArgs = &VAddArgs{}\n\t}\n\targs := []any{\"vadd\", key}\n\tif addArgs.reduce() > 0 {\n\t\targs = append(args, \"reduce\", addArgs.reduce())\n\t}\n\targs = append(args, val.Value()...)\n\targs = append(args, element)\n\targs = addArgs.appendArgs(args)\n\tcmd := NewBoolCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VCARD key`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VCard(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"vcard\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VDIM key`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VDim(ctx context.Context, key string) *IntCmd {\n\tcmd := NewIntCmd(ctx, \"vdim\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VEMB key element [RAW]`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd {\n\targs := []any{\"vemb\", key, element}\n\tif raw {\n\t\targs = append(args, \"raw\")\n\t}\n\tcmd := NewSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VGETATTR key element`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VGetAttr(ctx context.Context, key, element string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"vgetattr\", key, element)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VINFO key`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VInfo(ctx context.Context, key string) *MapStringInterfaceCmd {\n\tcmd := NewMapStringInterfaceCmd(ctx, \"vinfo\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VLINKS key element`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"vlinks\", key, element)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VLINKS key element WITHSCORES`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd {\n\tcmd := NewVectorInfoSliceCmd(ctx, \"vlinks\", key, element, \"withscores\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VRANDMEMBER key`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VRandMember(ctx context.Context, key string) *StringCmd {\n\tcmd := NewStringCmd(ctx, \"vrandmember\", key)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VRANDMEMBER key [count]`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd {\n\tcmd := NewStringSliceCmd(ctx, \"vrandmember\", key, count)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VREM key element`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VRem(ctx context.Context, key, element string) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"vrem\", key, element)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VSETATTR key element \"{ JSON obj }\"`\n// The `attr` must be something that can be marshaled to JSON (using encoding/JSON) unless\n// the argument is a string or []byte when we assume that it can be passed directly as JSON.\n//\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VSetAttr(ctx context.Context, key, element string, attr interface{}) *BoolCmd {\n\tvar attrStr string\n\tvar err error\n\tswitch v := attr.(type) {\n\tcase string:\n\t\tattrStr = v\n\tcase []byte:\n\t\tattrStr = string(v)\n\tdefault:\n\t\tvar bytes []byte\n\t\tbytes, err = json.Marshal(v)\n\t\tif err != nil {\n\t\t\t// If marshalling fails, create the command and set the error; this command won't be executed.\n\t\t\tcmd := NewBoolCmd(ctx, \"vsetattr\", key, element, \"\")\n\t\t\tcmd.SetErr(err)\n\t\t\treturn cmd\n\t\t}\n\t\tattrStr = string(bytes)\n\t}\n\tcmd := NewBoolCmd(ctx, \"vsetattr\", key, element, attrStr)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VClearAttributes` clear attributes on a vector set element.\n// The implementation of `VClearAttributes` is execute command `VSETATTR key element \"\"`.\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VClearAttributes(ctx context.Context, key, element string) *BoolCmd {\n\tcmd := NewBoolCmd(ctx, \"vsetattr\", key, element, \"\")\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VSIM key (ELE | FP32 | VALUES num) (vector | element)`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VSim(ctx context.Context, key string, val Vector) *StringSliceCmd {\n\treturn c.VSimWithArgs(ctx, key, val, &VSimArgs{})\n}\n\n// `VSIM key (ELE | FP32 | VALUES num) (vector | element) WITHSCORES`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd {\n\treturn c.VSimWithArgsWithScores(ctx, key, val, &VSimArgs{})\n}\n\ntype VSimArgs struct {\n\tCount    int64\n\tEF       int64\n\tFilter   string\n\tFilterEF int64\n\tTruth    bool\n\tNoThread bool\n\tEpsilon  float64\n}\n\nfunc (v VSimArgs) appendArgs(args []any) []any {\n\tif v.Count > 0 {\n\t\targs = append(args, \"count\", v.Count)\n\t}\n\tif v.EF > 0 {\n\t\targs = append(args, \"ef\", v.EF)\n\t}\n\tif len(v.Filter) > 0 {\n\t\targs = append(args, \"filter\", v.Filter)\n\t}\n\tif v.FilterEF > 0 {\n\t\targs = append(args, \"filter-ef\", v.FilterEF)\n\t}\n\tif v.Truth {\n\t\targs = append(args, \"truth\")\n\t}\n\tif v.NoThread {\n\t\targs = append(args, \"nothread\")\n\t}\n\tif v.Epsilon > 0 {\n\t\targs = append(args, \"Epsilon\", v.Epsilon)\n\t}\n\treturn args\n}\n\n// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] [EPSILON delta]\n// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd {\n\tif simArgs == nil {\n\t\tsimArgs = &VSimArgs{}\n\t}\n\targs := []any{\"vsim\", key}\n\targs = append(args, val.Value()...)\n\targs = simArgs.appendArgs(args)\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] [EPSILON delta]\n// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]`\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd {\n\tif simArgs == nil {\n\t\tsimArgs = &VSimArgs{}\n\t}\n\targs := []any{\"vsim\", key}\n\targs = append(args, val.Value()...)\n\targs = append(args, \"withscores\")\n\targs = simArgs.appendArgs(args)\n\tcmd := NewVectorInfoSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n\n// `VRANGE key start end count`\n// a negative count means to return all the elements in the vector set.\n// note: the API is experimental and may be subject to change.\nfunc (c cmdable) VRange(ctx context.Context, key, start, end string, count int64) *StringSliceCmd {\n\targs := []any{\"vrange\", key, start, end, count}\n\tcmd := NewStringSliceCmd(ctx, args...)\n\t_ = c(ctx, cmd)\n\treturn cmd\n}\n"
  },
  {
    "path": "vectorset_commands_integration_test.go",
    "content": "package redis_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"time\"\n\n\t. \"github.com/bsm/ginkgo/v2\"\n\t. \"github.com/bsm/gomega\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n)\n\nfunc expectNil(err error) {\n\tExpect(err).NotTo(HaveOccurred())\n}\n\nfunc expectTrue(t bool) {\n\texpectEqual(t, true)\n}\n\nfunc expectEqual[T any, U any](a T, b U) {\n\tExpect(a).To(BeEquivalentTo(b))\n}\n\nfunc generateRandomVector(dim int) redis.VectorValues {\n\trand.Seed(time.Now().UnixNano())\n\tv := make([]float64, dim)\n\tfor i := range v {\n\t\tv[i] = float64(rand.Intn(1000)) + rand.Float64()\n\t}\n\treturn redis.VectorValues{Val: v}\n}\n\nvar _ = Describe(\"Redis VectorSet commands\", Label(\"vectorset\"), func() {\n\tctx := context.TODO()\n\n\tsetupRedisClient := func(protocolVersion int) *redis.Client {\n\t\treturn redis.NewClient(&redis.Options{\n\t\t\tAddr:          \"localhost:6379\",\n\t\t\tDB:            0,\n\t\t\tProtocol:      protocolVersion,\n\t\t\tUnstableResp3: true,\n\t\t})\n\t}\n\n\tprotocols := []int{2, 3}\n\tfor _, protocol := range protocols {\n\t\tprotocol := protocol\n\n\t\tContext(fmt.Sprintf(\"with protocol version %d\", protocol), func() {\n\t\t\tvar client *redis.Client\n\n\t\t\tBeforeEach(func() {\n\t\t\t\tclient = setupRedisClient(protocol)\n\t\t\t\tExpect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred())\n\t\t\t})\n\n\t\t\tAfterEach(func() {\n\t\t\t\tif client != nil {\n\t\t\t\t\tclient.FlushDB(ctx)\n\t\t\t\t\tclient.Close()\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"basic\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.0, \"Redis 8.0 introduces support for VectorSet\")\n\t\t\t\tvecName := \"basic\"\n\t\t\t\tval := &redis.VectorValues{\n\t\t\t\t\tVal: []float64{1.5, 2.4, 3.3, 4.2},\n\t\t\t\t}\n\t\t\t\tok, err := client.VAdd(ctx, vecName, \"k1\", val).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\n\t\t\t\tfp32 := \"\\x8f\\xc2\\xf9\\x3e\\xcb\\xbe\\xe9\\xbe\\xb0\\x1e\\xca\\x3f\\x5e\\x06\\x9e\\x3f\"\n\t\t\t\tval2 := &redis.VectorFP32{\n\t\t\t\t\tVal: []byte(fp32),\n\t\t\t\t}\n\t\t\t\tok, err = client.VAdd(ctx, vecName, \"k2\", val2).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\n\t\t\t\tdim, err := client.VDim(ctx, vecName).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(dim, 4)\n\n\t\t\t\tcount, err := client.VCard(ctx, vecName).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(count, 2)\n\n\t\t\t\tok, err = client.VRem(ctx, vecName, \"k1\").Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\n\t\t\t\tcount, err = client.VCard(ctx, vecName).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(count, 1)\n\t\t\t})\n\n\t\t\tIt(\"basic similarity\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.0, \"Redis 8.0 introduces support for VectorSet\")\n\t\t\t\tvecName := \"basic_similarity\"\n\n\t\t\t\tok, err := client.VAdd(ctx, vecName, \"k1\", &redis.VectorValues{\n\t\t\t\t\tVal: []float64{1, 0, 0, 0},\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\t\t\t\tok, err = client.VAdd(ctx, vecName, \"k2\", &redis.VectorValues{\n\t\t\t\t\tVal: []float64{0.99, 0.01, 0, 0},\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\t\t\t\tok, err = client.VAdd(ctx, vecName, \"k3\", &redis.VectorValues{\n\t\t\t\t\tVal: []float64{0.1, 1, -1, 0.5},\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\n\t\t\t\tsim, err := client.VSimWithScores(ctx, vecName, &redis.VectorValues{\n\t\t\t\t\tVal: []float64{1, 0, 0, 0},\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 3)\n\t\t\t\tsimMap := make(map[string]float64)\n\t\t\t\tfor _, vi := range sim {\n\t\t\t\t\tsimMap[vi.Name] = vi.Score\n\t\t\t\t}\n\t\t\t\texpectTrue(simMap[\"k1\"] > 0.99)\n\t\t\t\texpectTrue(simMap[\"k2\"] > 0.99)\n\t\t\t\texpectTrue(simMap[\"k3\"] < 0.8)\n\t\t\t})\n\n\t\t\tIt(\"dimension operation\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.0, \"Redis 8.0 introduces support for VectorSet\")\n\t\t\t\tvecName := \"dimension_op\"\n\t\t\t\toriginalDim := 100\n\t\t\t\treducedDim := 50\n\n\t\t\t\tv1 := generateRandomVector(originalDim)\n\t\t\t\tok, err := client.VAddWithArgs(ctx, vecName, \"k1\", &v1, &redis.VAddArgs{\n\t\t\t\t\tReduce: int64(reducedDim),\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\n\t\t\t\tinfo, err := client.VInfo(ctx, vecName).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\tdim := info[\"vector-dim\"].(int64)\n\t\t\t\toriDim := info[\"projection-input-dim\"].(int64)\n\t\t\t\texpectEqual(dim, reducedDim)\n\t\t\t\texpectEqual(oriDim, originalDim)\n\n\t\t\t\twrongDim := 80\n\t\t\t\twrongV := generateRandomVector(wrongDim)\n\t\t\t\t_, err = client.VAddWithArgs(ctx, vecName, \"kw\", &wrongV, &redis.VAddArgs{\n\t\t\t\t\tReduce: int64(reducedDim),\n\t\t\t\t}).Result()\n\t\t\t\texpectTrue(err != nil)\n\n\t\t\t\tv2 := generateRandomVector(originalDim)\n\t\t\t\tok, err = client.VAddWithArgs(ctx, vecName, \"k2\", &v2, &redis.VAddArgs{\n\t\t\t\t\tReduce: int64(reducedDim),\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\t\t\t})\n\n\t\t\tIt(\"remove\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.0, \"Redis 8.0 introduces support for VectorSet\")\n\t\t\t\tvecName := \"remove\"\n\t\t\t\tv1 := generateRandomVector(5)\n\t\t\t\tok, err := client.VAdd(ctx, vecName, \"k1\", &v1).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\n\t\t\t\texist, err := client.Exists(ctx, vecName).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(exist, 1)\n\n\t\t\t\tok, err = client.VRem(ctx, vecName, \"k1\").Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectTrue(ok)\n\n\t\t\t\texist, err = client.Exists(ctx, vecName).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(exist, 0)\n\t\t\t})\n\n\t\t\tIt(\"all operations\", func() {\n\t\t\t\tSkipBeforeRedisVersion(8.0, \"Redis 8.0 introduces support for VectorSet\")\n\t\t\t\tvecName := \"commands\"\n\t\t\t\tvals := []struct {\n\t\t\t\t\tname string\n\t\t\t\t\tv    redis.VectorValues\n\t\t\t\t\tattr string\n\t\t\t\t}{\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"k0\",\n\t\t\t\t\t\tv:    redis.VectorValues{Val: []float64{1, 0, 0, 0}},\n\t\t\t\t\t\tattr: `{\"age\": 25, \"name\": \"Alice\", \"active\": true, \"scores\": [85, 90, 95], \"city\": \"New York\"}`,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"k1\",\n\t\t\t\t\t\tv:    redis.VectorValues{Val: []float64{0, 1, 0, 0}},\n\t\t\t\t\t\tattr: `{\"age\": 30, \"name\": \"Bob\", \"active\": false, \"scores\": [70, 75, 80], \"city\": \"Boston\"}`,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"k2\",\n\t\t\t\t\t\tv:    redis.VectorValues{Val: []float64{0, 0, 1, 0}},\n\t\t\t\t\t\tattr: `{\"age\": 35, \"name\": \"Charlie\", \"scores\": [60, 65, 70], \"city\": \"Seattle\"}`,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"k3\",\n\t\t\t\t\t\tv:    redis.VectorValues{Val: []float64{0, 0, 0, 1}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"k4\",\n\t\t\t\t\t\tv:    redis.VectorValues{Val: []float64{0.5, 0.5, 0, 0}},\n\t\t\t\t\t\tattr: `invalid json`,\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\t// If the key doesn't exist, return null error\n\t\t\t\t_, err := client.VRandMember(ctx, vecName).Result()\n\t\t\t\texpectEqual(err.Error(), proto.Nil.Error())\n\n\t\t\t\t// If the key doesn't exist, return an empty array\n\t\t\t\tres, err := client.VRandMemberCount(ctx, vecName, 3).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(res), 0)\n\n\t\t\t\tfor _, v := range vals {\n\t\t\t\t\tok, err := client.VAdd(ctx, vecName, v.name, &v.v).Result()\n\t\t\t\t\texpectNil(err)\n\t\t\t\t\texpectTrue(ok)\n\t\t\t\t\tif len(v.attr) > 0 {\n\t\t\t\t\t\tok, err = client.VSetAttr(ctx, vecName, v.name, v.attr).Result()\n\t\t\t\t\t\texpectNil(err)\n\t\t\t\t\t\texpectTrue(ok)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// VGetAttr\n\t\t\t\tattr, err := client.VGetAttr(ctx, vecName, vals[1].name).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(attr, vals[1].attr)\n\n\t\t\t\t// VRandMember\n\t\t\t\t_, err = client.VRandMember(ctx, vecName).Result()\n\t\t\t\texpectNil(err)\n\n\t\t\t\tres, err = client.VRandMemberCount(ctx, vecName, 3).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(res), 3)\n\n\t\t\t\tres, err = client.VRandMemberCount(ctx, vecName, 10).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(res), len(vals))\n\n\t\t\t\tif RedisVersion >= 8.4 {\n\t\t\t\t\tres, err = client.VRange(ctx, vecName, \"[k1\", \"[k2\", -1).Result()\n\t\t\t\t\texpectNil(err)\n\t\t\t\t\texpectEqual(len(res), 2)\n\n\t\t\t\t\tres, err = client.VRange(ctx, vecName, \"-\", \"[k2\", -1).Result()\n\t\t\t\t\texpectNil(err)\n\t\t\t\t\texpectEqual(len(res), 3)\n\n\t\t\t\t\tres, err = client.VRange(ctx, vecName, \"(k1\", \"+\", -1).Result()\n\t\t\t\t\texpectNil(err)\n\t\t\t\t\texpectEqual(len(res), 3)\n\n\t\t\t\t\tres, err = client.VRange(ctx, vecName, \"[k1\", \"+\", 2).Result()\n\t\t\t\t\texpectNil(err)\n\t\t\t\t\texpectEqual(len(res), 2)\n\t\t\t\t}\n\n\t\t\t\t// test equality\n\t\t\t\tsim, err := client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.age == 25`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 1)\n\t\t\t\texpectEqual(sim[0], vals[0].name)\n\n\t\t\t\t// test greater than\n\t\t\t\tsim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.age > 25`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 2)\n\n\t\t\t\t// test less than or equal\n\t\t\t\tsim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.age <= 30`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 2)\n\n\t\t\t\t// test string equality\n\t\t\t\tsim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.name == \"Alice\"`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 1)\n\t\t\t\texpectEqual(sim[0], vals[0].name)\n\n\t\t\t\t// test string inequality\n\t\t\t\tsim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.name != \"Alice\"`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 2)\n\n\t\t\t\t// test bool\n\t\t\t\tsim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.active`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 1)\n\t\t\t\texpectEqual(sim[0], vals[0].name)\n\n\t\t\t\t// test logical add\n\t\t\t\tsim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.age > 20 and .age < 30`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 1)\n\t\t\t\texpectEqual(sim[0], vals[0].name)\n\n\t\t\t\t// test logical or\n\t\t\t\tsim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{\n\t\t\t\t\tFilter: `.age < 30 or .age > 35`,\n\t\t\t\t}).Result()\n\t\t\t\texpectNil(err)\n\t\t\t\texpectEqual(len(sim), 1)\n\t\t\t\texpectEqual(sim[0], vals[0].name)\n\t\t\t})\n\t\t})\n\t}\n})\n"
  },
  {
    "path": "vectorset_commands_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestVectorFP32_Value(t *testing.T) {\n\tv := &VectorFP32{Val: []byte{1, 2, 3}}\n\tgot := v.Value()\n\twant := []any{\"FP32\", []byte{1, 2, 3}}\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"VectorFP32.Value() = %v, want %v\", got, want)\n\t}\n}\n\nfunc TestVectorValues_Value(t *testing.T) {\n\tv := &VectorValues{Val: []float64{1.1, 2.2}}\n\tgot := v.Value()\n\twant := []any{\"Values\", 2, 1.1, 2.2}\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"VectorValues.Value() = %v, want %v\", got, want)\n\t}\n}\n\nfunc TestVectorRef_Value(t *testing.T) {\n\tv := &VectorRef{Name: \"foo\"}\n\tgot := v.Value()\n\twant := []any{\"ele\", \"foo\"}\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"VectorRef.Value() = %v, want %v\", got, want)\n\t}\n}\n\nfunc TestVAdd(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\tc.VAdd(context.Background(), \"k\", \"e\", vec)\n\tcmd, ok := m.lastCmd.(*BoolCmd)\n\tif !ok {\n\t\tt.Fatalf(\"expected BoolCmd, got %T\", m.lastCmd)\n\t}\n\tif cmd.args[0] != \"vadd\" || cmd.args[1] != \"k\" || cmd.args[len(cmd.args)-1] != \"e\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVAddWithArgs_AllOptions(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\targs := &VAddArgs{Reduce: 3, Cas: true, NoQuant: true, EF: 5, SetAttr: \"attr\", M: 2}\n\tc.VAddWithArgs(context.Background(), \"k\", \"e\", vec, args)\n\tcmd := m.lastCmd.(*BoolCmd)\n\tfound := map[string]bool{}\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok {\n\t\t\tfound[s] = true\n\t\t}\n\t}\n\tfor _, want := range []string{\"reduce\", \"cas\", \"noquant\", \"ef\", \"setattr\", \"m\"} {\n\t\tif !found[want] {\n\t\t\tt.Errorf(\"missing arg: %s\", want)\n\t\t}\n\t}\n}\n\nfunc TestVCard(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VCard(context.Background(), \"k\")\n\tcmd := m.lastCmd.(*IntCmd)\n\tif cmd.args[0] != \"vcard\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVDim(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VDim(context.Background(), \"k\")\n\tcmd := m.lastCmd.(*IntCmd)\n\tif cmd.args[0] != \"vdim\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVEmb(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VEmb(context.Background(), \"k\", \"e\", true)\n\tcmd := m.lastCmd.(*SliceCmd)\n\tif cmd.args[0] != \"vemb\" || cmd.args[1] != \"k\" || cmd.args[2] != \"e\" || cmd.args[3] != \"raw\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVGetAttr(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VGetAttr(context.Background(), \"k\", \"e\")\n\tcmd := m.lastCmd.(*StringCmd)\n\tif cmd.args[0] != \"vgetattr\" || cmd.args[1] != \"k\" || cmd.args[2] != \"e\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVInfo(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VInfo(context.Background(), \"k\")\n\tcmd := m.lastCmd.(*MapStringInterfaceCmd)\n\tif cmd.args[0] != \"vinfo\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVLinks(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VLinks(context.Background(), \"k\", \"e\")\n\tcmd := m.lastCmd.(*StringSliceCmd)\n\tif cmd.args[0] != \"vlinks\" || cmd.args[1] != \"k\" || cmd.args[2] != \"e\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVLinksWithScores(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VLinksWithScores(context.Background(), \"k\", \"e\")\n\tcmd := m.lastCmd.(*VectorScoreSliceCmd)\n\tif cmd.args[0] != \"vlinks\" || cmd.args[1] != \"k\" || cmd.args[2] != \"e\" || cmd.args[3] != \"withscores\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVRandMember(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VRandMember(context.Background(), \"k\")\n\tcmd := m.lastCmd.(*StringCmd)\n\tif cmd.args[0] != \"vrandmember\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVRandMemberCount(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VRandMemberCount(context.Background(), \"k\", 5)\n\tcmd := m.lastCmd.(*StringSliceCmd)\n\tif cmd.args[0] != \"vrandmember\" || cmd.args[1] != \"k\" || cmd.args[2] != 5 {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVRem(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VRem(context.Background(), \"k\", \"e\")\n\tcmd := m.lastCmd.(*BoolCmd)\n\tif cmd.args[0] != \"vrem\" || cmd.args[1] != \"k\" || cmd.args[2] != \"e\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVSetAttr_String(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VSetAttr(context.Background(), \"k\", \"e\", \"foo\")\n\tcmd := m.lastCmd.(*BoolCmd)\n\tif cmd.args[0] != \"vsetattr\" || cmd.args[1] != \"k\" || cmd.args[2] != \"e\" || cmd.args[3] != \"foo\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVSetAttr_Bytes(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VSetAttr(context.Background(), \"k\", \"e\", []byte(\"bar\"))\n\tcmd := m.lastCmd.(*BoolCmd)\n\tif cmd.args[3] != \"bar\" {\n\t\tt.Errorf(\"expected 'bar', got %v\", cmd.args[3])\n\t}\n}\n\nfunc TestVSetAttr_MarshalStruct(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tval := struct{ A int }{A: 1}\n\tc.VSetAttr(context.Background(), \"k\", \"e\", val)\n\tcmd := m.lastCmd.(*BoolCmd)\n\twant, _ := json.Marshal(val)\n\tif cmd.args[3] != string(want) {\n\t\tt.Errorf(\"expected marshalled struct, got %v\", cmd.args[3])\n\t}\n}\n\nfunc TestVSetAttr_MarshalError(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tbad := func() {}\n\tcmd := c.VSetAttr(context.Background(), \"k\", \"e\", bad)\n\tif cmd.Err() == nil {\n\t\tt.Error(\"expected error for non-marshallable value\")\n\t}\n}\n\nfunc TestVClearAttributes(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VClearAttributes(context.Background(), \"k\", \"e\")\n\tcmd := m.lastCmd.(*BoolCmd)\n\tif cmd.args[0] != \"vsetattr\" || cmd.args[3] != \"\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVSim(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\tc.VSim(context.Background(), \"k\", vec)\n\tcmd := m.lastCmd.(*StringSliceCmd)\n\tif cmd.args[0] != \"vsim\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVSimWithScores(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\tc.VSimWithScores(context.Background(), \"k\", vec)\n\tcmd := m.lastCmd.(*VectorScoreSliceCmd)\n\tif cmd.args[0] != \"vsim\" || cmd.args[1] != \"k\" || cmd.args[len(cmd.args)-1] != \"withscores\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVSimWithArgs_AllOptions(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\targs := &VSimArgs{Count: 2, EF: 3, Filter: \"f\", FilterEF: 4, Truth: true, NoThread: true}\n\tc.VSimWithArgs(context.Background(), \"k\", vec, args)\n\tcmd := m.lastCmd.(*StringSliceCmd)\n\tfound := map[string]bool{}\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok {\n\t\t\tfound[s] = true\n\t\t}\n\t}\n\tfor _, want := range []string{\"count\", \"ef\", \"filter\", \"filter-ef\", \"truth\", \"nothread\"} {\n\t\tif !found[want] {\n\t\t\tt.Errorf(\"missing arg: %s\", want)\n\t\t}\n\t}\n}\n\nfunc TestVSimWithArgsWithScores_AllOptions(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\targs := &VSimArgs{Count: 2, EF: 3, Filter: \"f\", FilterEF: 4, Truth: true, NoThread: true}\n\tc.VSimWithArgsWithScores(context.Background(), \"k\", vec, args)\n\tcmd := m.lastCmd.(*VectorScoreSliceCmd)\n\tfound := map[string]bool{}\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok {\n\t\t\tfound[s] = true\n\t\t}\n\t}\n\tfor _, want := range []string{\"count\", \"ef\", \"filter\", \"filter-ef\", \"truth\", \"nothread\", \"withscores\"} {\n\t\tif !found[want] {\n\t\t\tt.Errorf(\"missing arg: %s\", want)\n\t\t}\n\t}\n}\n\n// Additional tests for missing coverage\n\nfunc TestVectorValues_EmptySlice(t *testing.T) {\n\tv := &VectorValues{Val: []float64{}}\n\tgot := v.Value()\n\twant := []any{\"Values\", 0}\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"VectorValues.Value() with empty slice = %v, want %v\", got, want)\n\t}\n}\n\nfunc TestVEmb_WithoutRaw(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tc.VEmb(context.Background(), \"k\", \"e\", false)\n\tcmd := m.lastCmd.(*SliceCmd)\n\tif cmd.args[0] != \"vemb\" || cmd.args[1] != \"k\" || cmd.args[2] != \"e\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n\tif len(cmd.args) != 3 {\n\t\tt.Errorf(\"expected 3 args when raw=false, got %d\", len(cmd.args))\n\t}\n}\n\nfunc TestVAddWithArgs_Q8Option(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\targs := &VAddArgs{Q8: true}\n\tc.VAddWithArgs(context.Background(), \"k\", \"e\", vec, args)\n\tcmd := m.lastCmd.(*BoolCmd)\n\tfound := false\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok && s == \"q8\" {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"missing q8 arg\")\n\t}\n}\n\nfunc TestVAddWithArgs_BinOption(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\targs := &VAddArgs{Bin: true}\n\tc.VAddWithArgs(context.Background(), \"k\", \"e\", vec, args)\n\tcmd := m.lastCmd.(*BoolCmd)\n\tfound := false\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok && s == \"bin\" {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"missing bin arg\")\n\t}\n}\n\nfunc TestVAddWithArgs_NilArgs(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\tc.VAddWithArgs(context.Background(), \"k\", \"e\", vec, nil)\n\tcmd := m.lastCmd.(*BoolCmd)\n\tif cmd.args[0] != \"vadd\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVSimWithArgs_NilArgs(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\tc.VSimWithArgs(context.Background(), \"k\", vec, nil)\n\tcmd := m.lastCmd.(*StringSliceCmd)\n\tif cmd.args[0] != \"vsim\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n}\n\nfunc TestVSimWithArgsWithScores_NilArgs(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\tc.VSimWithArgsWithScores(context.Background(), \"k\", vec, nil)\n\tcmd := m.lastCmd.(*VectorScoreSliceCmd)\n\tif cmd.args[0] != \"vsim\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n\t// Should still have withscores\n\tfound := false\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok && s == \"withscores\" {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"missing withscores arg\")\n\t}\n}\n\nfunc TestVAdd_WithVectorFP32(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorFP32{Val: []byte{1, 2, 3, 4}}\n\tc.VAdd(context.Background(), \"k\", \"e\", vec)\n\tcmd := m.lastCmd.(*BoolCmd)\n\tif cmd.args[0] != \"vadd\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n\t// Check that FP32 format is used\n\tfound := false\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok && s == \"FP32\" {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"missing FP32 format in args\")\n\t}\n}\n\nfunc TestVAdd_WithVectorRef(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorRef{Name: \"ref-vector\"}\n\tc.VAdd(context.Background(), \"k\", \"e\", vec)\n\tcmd := m.lastCmd.(*BoolCmd)\n\tif cmd.args[0] != \"vadd\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n\t// Check that ele format is used\n\tfound := false\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok && s == \"ele\" {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"missing ele format in args\")\n\t}\n}\n\nfunc TestVSim_WithVectorFP32(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorFP32{Val: []byte{1, 2, 3, 4}}\n\tc.VSim(context.Background(), \"k\", vec)\n\tcmd := m.lastCmd.(*StringSliceCmd)\n\tif cmd.args[0] != \"vsim\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n\t// Check that FP32 format is used\n\tfound := false\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok && s == \"FP32\" {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"missing FP32 format in args\")\n\t}\n}\n\nfunc TestVSim_WithVectorRef(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorRef{Name: \"ref-vector\"}\n\tc.VSim(context.Background(), \"k\", vec)\n\tcmd := m.lastCmd.(*StringSliceCmd)\n\tif cmd.args[0] != \"vsim\" || cmd.args[1] != \"k\" {\n\t\tt.Errorf(\"unexpected args: %v\", cmd.args)\n\t}\n\t// Check that ele format is used\n\tfound := false\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok && s == \"ele\" {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"missing ele format in args\")\n\t}\n}\n\nfunc TestVAddWithArgs_ReduceOption(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\targs := &VAddArgs{Reduce: 128}\n\tc.VAddWithArgs(context.Background(), \"k\", \"e\", vec, args)\n\tcmd := m.lastCmd.(*BoolCmd)\n\t// Check that reduce appears early in args (after key)\n\tif cmd.args[0] != \"vadd\" || cmd.args[1] != \"k\" || cmd.args[2] != \"reduce\" {\n\t\tt.Errorf(\"unexpected args order: %v\", cmd.args)\n\t}\n}\n\nfunc TestVAddWithArgs_ZeroValues(t *testing.T) {\n\tm := &mockCmdable{}\n\tc := m.asCmdable()\n\tvec := &VectorValues{Val: []float64{1, 2}}\n\targs := &VAddArgs{Reduce: 0, EF: 0, M: 0} // Zero values should not appear in args\n\tc.VAddWithArgs(context.Background(), \"k\", \"e\", vec, args)\n\tcmd := m.lastCmd.(*BoolCmd)\n\t// Check that zero values don't appear\n\tfor _, a := range cmd.args {\n\t\tif s, ok := a.(string); ok {\n\t\t\tif s == \"reduce\" || s == \"ef\" || s == \"m\" {\n\t\t\t\tt.Errorf(\"zero value option should not appear in args: %s\", s)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestVSimArgs_IndividualOptions(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\targs *VSimArgs\n\t\twant string\n\t}{\n\t\t{\"Count\", &VSimArgs{Count: 5}, \"count\"},\n\t\t{\"EF\", &VSimArgs{EF: 10}, \"ef\"},\n\t\t{\"Filter\", &VSimArgs{Filter: \"test\"}, \"filter\"},\n\t\t{\"FilterEF\", &VSimArgs{FilterEF: 15}, \"filter-ef\"},\n\t\t{\"Truth\", &VSimArgs{Truth: true}, \"truth\"},\n\t\t{\"NoThread\", &VSimArgs{NoThread: true}, \"nothread\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &mockCmdable{}\n\t\t\tc := m.asCmdable()\n\t\t\tvec := &VectorValues{Val: []float64{1, 2}}\n\t\t\tc.VSimWithArgs(context.Background(), \"k\", vec, tt.args)\n\t\t\tcmd := m.lastCmd.(*StringSliceCmd)\n\t\t\tfound := false\n\t\t\tfor _, a := range cmd.args {\n\t\t\t\tif s, ok := a.(string); ok && s == tt.want {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tt.Errorf(\"missing arg: %s\", tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "version.go",
    "content": "package redis\n\n// Version is the current release version.\nfunc Version() string {\n\treturn \"9.18.0\"\n}\n"
  }
]