├── .circleci ├── config.yml └── generate_config.sh ├── .github ├── release-drafter-config.yml └── workflows │ ├── build.yml │ ├── codeql.yml │ ├── release-drafter.yml │ └── tag-subpkg.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── binary.go ├── binary_test.go ├── cache.go ├── cache_test.go ├── client.go ├── client_test.go ├── cluster.go ├── cluster_test.go ├── cmds.go ├── codecov.yml ├── docker-compose.yml ├── dockertest.sh ├── go.mod ├── go.sum ├── hack └── cmds │ ├── commands.json │ ├── commands_ai.json │ ├── commands_bloom.json │ ├── commands_cell.json │ ├── commands_gears.json │ ├── commands_gears2.json │ ├── commands_graph.json │ ├── commands_json.json │ ├── commands_search.json │ ├── commands_sentinel.json │ ├── commands_timeseries.json │ ├── commands_vector_sets.json │ └── gen.go ├── helper.go ├── helper_test.go ├── internal ├── cmds │ ├── builder.go │ ├── builder_put.go │ ├── builder_test.go │ ├── cmds.go │ ├── cmds_test.go │ ├── gen_bf.go │ ├── gen_bf_test.go │ ├── gen_bitmap.go │ ├── gen_bitmap_test.go │ ├── gen_cf.go │ ├── gen_cf_test.go │ ├── gen_cl.go │ ├── gen_cl_test.go │ ├── gen_cluster.go │ ├── gen_cluster_test.go │ ├── gen_cms.go │ ├── gen_cms_test.go │ ├── gen_connection.go │ ├── gen_connection_test.go │ ├── gen_gears.go │ ├── gen_gears_test.go │ ├── gen_generic.go │ ├── gen_generic_test.go │ ├── gen_geo.go │ ├── gen_geo_test.go │ ├── gen_graph.go │ ├── gen_graph_test.go │ ├── gen_hash.go │ ├── gen_hash_test.go │ ├── gen_hyperloglog.go │ ├── gen_hyperloglog_test.go │ ├── gen_inference.go │ ├── gen_inference_test.go │ ├── gen_json.go │ ├── gen_json_test.go │ ├── gen_list.go │ ├── gen_list_test.go │ ├── gen_model.go │ ├── gen_model_test.go │ ├── gen_pubsub.go │ ├── gen_pubsub_test.go │ ├── gen_script.go │ ├── gen_script_test.go │ ├── gen_scripting.go │ ├── gen_scripting_test.go │ ├── gen_search.go │ ├── gen_search_test.go │ ├── gen_sentinel.go │ ├── gen_sentinel_test.go │ ├── gen_server.go │ ├── gen_server_test.go │ ├── gen_set.go │ ├── gen_set_test.go │ ├── gen_sorted_set.go │ ├── gen_sorted_set_test.go │ ├── gen_stream.go │ ├── gen_stream_test.go │ ├── gen_string.go │ ├── gen_string_test.go │ ├── gen_suggestion.go │ ├── gen_suggestion_test.go │ ├── gen_tdigest.go │ ├── gen_tdigest_test.go │ ├── gen_tensor.go │ ├── gen_tensor_test.go │ ├── gen_timeseries.go │ ├── gen_timeseries_test.go │ ├── gen_topk.go │ ├── gen_topk_test.go │ ├── gen_transactions.go │ ├── gen_transactions_test.go │ ├── gen_triggers_and_functions.go │ ├── gen_triggers_and_functions_test.go │ ├── gen_vector_set.go │ ├── gen_vector_set_test.go │ ├── iter.go │ ├── iter_test.go │ ├── slot.go │ └── slot_test.go └── util │ ├── parallel.go │ ├── parallel_test.go │ ├── parser.go │ ├── parser_test.go │ ├── pool.go │ ├── pool_test.go │ ├── rand.1.22.go │ └── rand_test.go ├── lru.go ├── lru_test.go ├── lua.go ├── lua_test.go ├── message.go ├── message_test.go ├── mock ├── README.md ├── client.go ├── client_test.go ├── go.mod ├── go.sum ├── match.go ├── match_test.go ├── result.go └── result_test.go ├── mux.go ├── mux_test.go ├── om ├── README.md ├── conv.go ├── cursor.go ├── cursor_test.go ├── go.mod ├── go.sum ├── hash.go ├── hash_test.go ├── json.go ├── json_test.go ├── repo.go ├── repo_test.go ├── schema.go └── schema_test.go ├── pipe.go ├── pipe_test.go ├── pool.go ├── pool_test.go ├── pubsub.go ├── pubsub_test.go ├── redis_test.go ├── resp.go ├── resp_test.go ├── retry.go ├── retry_test.go ├── ring.go ├── ring_test.go ├── rueidis.go ├── rueidis_test.go ├── rueidisaside ├── README.md ├── aside.go ├── aside_test.go ├── go.mod ├── go.sum ├── typed_aside.go └── typed_aside_test.go ├── rueidiscompat ├── README.md ├── adapter.go ├── adapter_test.go ├── command.go ├── command_test.go ├── go.mod ├── go.sum ├── hscan.go ├── hscan_test.go ├── pipeline.go ├── pipeline_test.go ├── pubsub.go ├── pubsub_test.go ├── script.go ├── script_test.go ├── structmap.go ├── tx.go ├── tx_test.go ├── util.go └── util_test.go ├── rueidishook ├── README.md ├── go.mod ├── go.sum ├── hook.go └── hook_test.go ├── rueidislimiter ├── README.md ├── go.mod ├── go.sum ├── limit.go ├── limiter.go ├── limiter_test.go └── syncp.go ├── rueidislock ├── README.md ├── lock.go └── lock_test.go ├── rueidisotel ├── README.md ├── go.mod ├── go.sum ├── metrics.go ├── metrics_test.go ├── trace.go └── trace_test.go ├── rueidisprob ├── README.md ├── bloomfilter.go ├── bloomfilter_test.go ├── countingbloomfilter.go ├── countingbloomfilter_test.go ├── go.mod ├── go.sum ├── index.go ├── slidingbloomfilter.go ├── slidingbloomfilter_test.go └── synp.go ├── sentinel.go ├── sentinel_test.go ├── singleflight.go ├── singleflight_test.go ├── standalone.go ├── standalone_test.go ├── syncp.go ├── url.go └── url_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | setup: true 3 | orbs: 4 | go: circleci/go@1.7.3 5 | continuation: circleci/continuation@0.1.2 6 | jobs: 7 | setup: 8 | executor: continuation/default 9 | steps: 10 | - checkout 11 | - run: 12 | name: Generate config 13 | # Generate dynamic configuration based on go.mod files 14 | command: | 15 | ./.circleci/generate_config.sh > generated_config.yml 16 | - continuation/continue: 17 | configuration_path: generated_config.yml # use newly generated config to continue 18 | 19 | # our single workflow, that triggers the setup job defined above 20 | workflows: 21 | setup: 22 | jobs: 23 | - setup 24 | -------------------------------------------------------------------------------- /.circleci/generate_config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Find all directories containing go.mod files 4 | modules=$(find . -maxdepth 2 -type f -name "go.mod" | xargs -n 1 dirname | sort -u) 5 | 6 | # Start the generated config with the version 7 | cat << EOF 8 | version: 2.1 9 | orbs: 10 | go: circleci/go@1.7.3 11 | jobs: 12 | build: 13 | machine: 14 | image: ubuntu-2204:current 15 | resource_class: large 16 | parallelism: 3 17 | steps: 18 | - checkout 19 | - go/install: 20 | version: 1.22.0 21 | EOF 22 | 23 | # Loop through each module and generate job configurations 24 | for module in $modules; do 25 | module_name=$(basename "$module") 26 | if [ "$module_name" = "." ]; then 27 | cat << EOF 28 | - run: # test ./go.mod 29 | name: Test $module_name 30 | command: | 31 | list=\$(go list ./... | circleci tests split --split-by=timings) 32 | echo "Test Packages: \$list" 33 | for n in {1..5}; do 34 | ./dockertest.sh \$list && break 35 | done 36 | no_output_timeout: 15m 37 | EOF 38 | else 39 | cat << EOF 40 | - run: # test ./$module_name/go.mod 41 | name: Test $module_name 42 | command: | 43 | cd "\$CIRCLE_WORKING_DIRECTORY/$module_name" 44 | list=\$(go list ./... | circleci tests split --split-by=timings) 45 | echo "Test Packages: \$list" 46 | for n in {1..5}; do 47 | ../dockertest.sh \$list && break 48 | done 49 | no_output_timeout: 15m 50 | EOF 51 | fi 52 | done 53 | 54 | cat << EOF 55 | - store_test_results: 56 | path: . 57 | - run: curl -Os https://uploader.codecov.io/latest/linux/codecov && chmod +x codecov && ./codecov -t ${CODECOV_TOKEN} 58 | EOF 59 | -------------------------------------------------------------------------------- /.github/release-drafter-config.yml: -------------------------------------------------------------------------------- 1 | name-template: '$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | autolabeler: 4 | - label: 'maintenance' 5 | files: 6 | - '*.md' 7 | - '.github/*' 8 | - label: 'bug' 9 | branch: 10 | - '/bug-.+' 11 | - label: 'maintenance' 12 | branch: 13 | - '/maintenance-.+' 14 | - label: 'feature' 15 | branch: 16 | - '/feature-.+' 17 | categories: 18 | - title: 'Breaking Changes' 19 | labels: 20 | - 'breakingchange' 21 | - title: '🧪 Experimental Features' 22 | labels: 23 | - 'experimental' 24 | - title: '🚀 New Features' 25 | labels: 26 | - 'feature' 27 | - 'enhancement' 28 | - title: '🐛 Bug Fixes' 29 | labels: 30 | - 'fix' 31 | - 'bugfix' 32 | - 'bug' 33 | - 'BUG' 34 | - title: '🧰 Maintenance' 35 | labels: 36 | - 'maintenance' 37 | - 'docs' 38 | - 'documentation' 39 | change-template: '- $TITLE (#$NUMBER)' 40 | exclude-labels: 41 | - 'skip-changelog' 42 | template: | 43 | # Changes 44 | 45 | $CHANGES 46 | 47 | ## Contributors 48 | We'd like to thank all the contributors who worked on this release! 49 | 50 | $CONTRIBUTORS 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go Modules Test 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | # https://docs.github.com/en/actions/learn-github-actions/expressions 9 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context 10 | concurrency: 11 | # Use github.run_id on main branch 12 | # Use github.event.pull_request.number on pull requests, so it's unique per pull request 13 | # Use github.ref on other branches, so it's unique per branch 14 | group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | validate-modules: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Validate Required Modules 23 | run: | 24 | # Find all module directories: rueidis* (except rueidislock), om, and mock 25 | # These directories should contain go.mod files 26 | modules=$(find . -maxdepth 1 -type d \( -name "rueidis*" -o -name "om" -o -name "mock" \) | grep -v "./rueidislock") 27 | 28 | # Check each module directory 29 | for module in $modules; do 30 | if [ ! -f "$module/go.mod" ]; then 31 | echo "Error: Module directory '$module' is missing go.mod" 32 | exit 1 33 | fi 34 | done 35 | 36 | prepare-matrix: 37 | needs: validate-modules 38 | runs-on: ubuntu-latest 39 | outputs: 40 | matrix: ${{ steps.set-matrix.outputs.matrix }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | - id: set-matrix 44 | run: | 45 | echo "matrix=$(find . -maxdepth 2 -type f -name 'go.mod' | xargs -n 1 dirname | sort -u | { echo "e2e"; cat; } | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT 46 | 47 | build: 48 | needs: prepare-matrix 49 | runs-on: ubuntu-latest 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | module: ${{fromJson(needs.prepare-matrix.outputs.matrix)}} 54 | go-version: ['1.22.0', '1.23.0', '1.24.0'] 55 | steps: 56 | - name: Checkout code 57 | uses: actions/checkout@v4 58 | 59 | - name: Set up Go 60 | uses: actions/setup-go@v5 61 | with: 62 | go-version: ${{ matrix.go-version }} 63 | 64 | - name: Test Module 65 | run: | 66 | module_path=${{ matrix.module }} 67 | if [ "$module_path" == "." ]; then 68 | list=$(go list ./...) 69 | echo "Test Packages: $list" 70 | for n in {1..5}; do 71 | ./dockertest.sh -skip 'Integration' $list && break 72 | done 73 | elif [ "$module_path" == "e2e" ]; then 74 | list=$(go list ./...) 75 | echo "Test Packages: $list" 76 | for n in {1..5}; do 77 | ./dockertest.sh -run 'Integration' $list && break 78 | done 79 | else 80 | cd $module_path 81 | list=$(go list ./...) 82 | echo "Test Packages: $list" 83 | for n in {1..5}; do 84 | ../dockertest.sh $list && break 85 | done 86 | fi 87 | 88 | - uses: codecov/codecov-action@v5.4.2 89 | with: 90 | token: ${{ secrets.CODECOV_TOKEN }} 91 | verbose: true 92 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [ "**" ] 9 | pull_request: 10 | branches: [ "**" ] 11 | schedule: 12 | - cron: "2 23 * * 5" 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | language: [ go ] 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v2 34 | with: 35 | languages: ${{ matrix.language }} 36 | queries: +security-and-quality 37 | 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v2 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v2 43 | with: 44 | category: "/language:${{ matrix.language }}" 45 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: write 11 | jobs: 12 | update_release_draft: 13 | permissions: 14 | pull-requests: write # to add label to PR (release-drafter/release-drafter) 15 | contents: write # to create a github release (release-drafter/release-drafter) 16 | 17 | runs-on: ubuntu-latest 18 | steps: 19 | # Drafts your next Release notes as Pull Requests are merged into "main" 20 | - uses: release-drafter/release-drafter@v6.1.0 21 | with: 22 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 23 | config-name: release-drafter-config.yml 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/tag-subpkg.yml: -------------------------------------------------------------------------------- 1 | name: Tag Prefix Workflow 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | tag-and-push: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Git 19 | run: | 20 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 21 | git config --global user.name "GitHub Actions" 22 | 23 | - name: Push Additional Tags with Dynamic Prefixes 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | run: | 27 | ORIGINAL_TAG=${GITHUB_REF#refs/tags/} 28 | # Find directories containing go.mod, extract directory names, and push tags with these names as prefixes 29 | find . -maxdepth 2 -type f -name "go.mod" | while read -r line; do 30 | DIR_NAME=$(dirname "$line") 31 | PREFIX=${DIR_NAME#"./"} # Remove leading "./" 32 | 33 | # Check if PREFIX is not empty and not the root directory 34 | if [[ -n "$PREFIX" && "$PREFIX" != "." ]]; then 35 | NEW_TAG="${PREFIX}/${ORIGINAL_TAG}" 36 | echo "Creating and pushing tag: $NEW_TAG" 37 | git tag $NEW_TAG $ORIGINAL_TAG 38 | git push origin $NEW_TAG 39 | else 40 | echo "Skipping root directory" 41 | fi 42 | done 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dist/ 3 | vendor/ 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | rueidis 2 | Copyright 2024 Rueian (https://github.com/rueian) 3 | -------------------------------------------------------------------------------- /binary.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "math" 7 | "unsafe" 8 | ) 9 | 10 | // BinaryString convert the provided []byte into a string without a copy. It does what strings.Builder.String() does. 11 | // Redis Strings are binary safe; this means that it is safe to store any []byte into Redis directly. 12 | // Users can use this BinaryString helper to insert a []byte as the part of redis command. For example: 13 | // 14 | // client.B().Set().Key(rueidis.BinaryString([]byte{0})).Value(rueidis.BinaryString([]byte{0})).Build() 15 | // 16 | // To read back the []byte of the string returned from the Redis, it is recommended to use the RedisMessage.AsReader. 17 | func BinaryString(bs []byte) string { 18 | return unsafe.String(unsafe.SliceData(bs), len(bs)) 19 | } 20 | 21 | // VectorString32 convert the provided []float32 into a string. Users can use this to build vector search queries: 22 | // 23 | // client.B().FtSearch().Index("idx").Query("*=>[KNN 5 @vec $V]"). 24 | // Params().Nargs(2).NameValue().NameValue("V", rueidis.VectorString32([]float32{1})). 25 | // Dialect(2).Build() 26 | func VectorString32(v []float32) string { 27 | b := make([]byte, len(v)*4) 28 | for i, e := range v { 29 | i := i * 4 30 | binary.LittleEndian.PutUint32(b[i:i+4], math.Float32bits(e)) 31 | } 32 | return BinaryString(b) 33 | } 34 | 35 | // ToVector32 reverts VectorString32. User can use this to convert redis response back to []float32. 36 | func ToVector32(s string) []float32 { 37 | bs := unsafe.Slice(unsafe.StringData(s), len(s)) 38 | vs := make([]float32, 0, len(bs)/4) 39 | for i := 0; i < len(bs); i += 4 { 40 | vs = append(vs, math.Float32frombits(binary.LittleEndian.Uint32(bs[i:i+4]))) 41 | } 42 | return vs 43 | } 44 | 45 | // VectorString64 convert the provided []float64 into a string. Users can use this to build vector search queries: 46 | // 47 | // client.B().FtSearch().Index("idx").Query("*=>[KNN 5 @vec $V]"). 48 | // Params().Nargs(2).NameValue().NameValue("V", rueidis.VectorString64([]float64{1})). 49 | // Dialect(2).Build() 50 | func VectorString64(v []float64) string { 51 | b := make([]byte, len(v)*8) 52 | for i, e := range v { 53 | i := i * 8 54 | binary.LittleEndian.PutUint64(b[i:i+8], math.Float64bits(e)) 55 | } 56 | return BinaryString(b) 57 | } 58 | 59 | // ToVector64 reverts VectorString64. User can use this to convert redis response back to []float64. 60 | func ToVector64(s string) []float64 { 61 | bs := unsafe.Slice(unsafe.StringData(s), len(s)) 62 | vs := make([]float64, 0, len(bs)/8) 63 | for i := 0; i < len(bs); i += 8 { 64 | vs = append(vs, math.Float64frombits(binary.LittleEndian.Uint64(bs[i:i+8]))) 65 | } 66 | return vs 67 | } 68 | 69 | // JSON convert the provided parameter into a JSON string. Users can use this JSON helper to work with RedisJSON commands. 70 | // For example: 71 | // 72 | // client.B().JsonSet().Key("a").Path("$.myField").Value(rueidis.JSON("str")).Build() 73 | func JSON(in any) string { 74 | bs, err := json.Marshal(in) 75 | if err != nil { 76 | panic(err) 77 | } 78 | return BinaryString(bs) 79 | } 80 | -------------------------------------------------------------------------------- /binary_test.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestBinaryString(t *testing.T) { 11 | if str := []byte{0, 1, 2, 3, 4}; string(str) != BinaryString(str) { 12 | t.Fatalf("BinaryString mismatch") 13 | } 14 | } 15 | 16 | func TestJSON(t *testing.T) { 17 | if v := JSON("a"); v != `"a"` { 18 | t.Fatalf("unexpected JSON result") 19 | } 20 | } 21 | 22 | func TestJSONPanic(t *testing.T) { 23 | defer func() { 24 | if m := recover().(*json.UnsupportedValueError); !strings.Contains(m.Error(), "encountered a cycle") { 25 | t.Fatalf("should panic") 26 | } 27 | }() 28 | a := &recursive{} 29 | a.R = a 30 | JSON(a) 31 | } 32 | 33 | 34 | func TestVectorString32(t *testing.T) { 35 | for _, test := range [][]float32{ 36 | {}, 37 | {0, 0, 0, 0}, 38 | {9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}, 39 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 40 | {.9, .9, .9, .9, .9, .9, .9, .9, .9, .9, .9}, 41 | {-.1, -.1, -.1, -.1, -.1, -.1, -.1, -.1, -.1, -.1}, 42 | {.1, -.1, .1, -.1, .1, -.1, .1, -.1, .1, -.1}, 43 | } { 44 | if !reflect.DeepEqual(test, ToVector32(VectorString32(test))) { 45 | t.Fatalf("fail to convert %v", test) 46 | } 47 | } 48 | } 49 | 50 | func TestVectorString64(t *testing.T) { 51 | for _, test := range [][]float64{ 52 | {}, 53 | {0, 0, 0, 0}, 54 | {9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}, 55 | {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, 56 | {.9, .9, .9, .9, .9, .9, .9, .9, .9, .9, .9}, 57 | {-.1, -.1, -.1, -.1, -.1, -.1, -.1, -.1, -.1, -.1}, 58 | {.1, -.1, .1, -.1, .1, -.1, .1, -.1, .1, -.1}, 59 | } { 60 | if !reflect.DeepEqual(test, ToVector64(VectorString64(test))) { 61 | t.Fatalf("fail to convert %v", test) 62 | } 63 | } 64 | } 65 | 66 | type recursive struct { 67 | R *recursive 68 | } 69 | -------------------------------------------------------------------------------- /cmds.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import "github.com/redis/rueidis/internal/cmds" 4 | 5 | // Builder represents a command builder. It should only be created from the client.B() method. 6 | type Builder = cmds.Builder 7 | 8 | // Incomplete represents an incomplete Redis command. It should then be completed by calling Build(). 9 | type Incomplete = cmds.Incomplete 10 | 11 | // Completed represents a completed Redis command. It should only be created from the Build() of a command builder. 12 | type Completed = cmds.Completed 13 | 14 | // Cacheable represents a completed Redis command which supports server-assisted client side caching, 15 | // and it should be created by the Cache() of command builder. 16 | type Cacheable = cmds.Cacheable 17 | 18 | // Commands is an exported alias to []Completed. 19 | // This allows users to store commands for later usage, for example: 20 | // 21 | // c, release := client.Dedicate() 22 | // defer release() 23 | // 24 | // cmds := make(rueidis.Commands, 0, 10) 25 | // for i := 0; i < 10; i++ { 26 | // cmds = append(cmds, c.B().Set().Key(strconv.Itoa(i)).Value(strconv.Itoa(i)).Build()) 27 | // } 28 | // for _, resp := range c.DoMulti(ctx, cmds...) { 29 | // if err := resp.Error(); err != nil { 30 | // panic(err) 31 | // } 32 | // 33 | // However, please know that once commands are processed by the Do() or DoMulti(), they are recycled and should not be reused. 34 | type Commands []Completed 35 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: false 4 | patch: false 5 | ignore: 6 | - hack 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:7.4-alpine 4 | ports: 5 | - "6379:6379" 6 | redislock: 7 | image: redis:7.4-alpine 8 | ports: 9 | - "6376:6379" 10 | redis5: 11 | image: redis:5-alpine 12 | ports: 13 | - "6355:6379" 14 | keydb6: 15 | image: eqalpha/keydb:alpine_x86_64_v6.3.1 16 | ports: 17 | - "6344:6379" 18 | dragonflydb: 19 | image: docker.dragonflydb.io/dragonflydb/dragonfly:v1.20.1 20 | ports: 21 | - "6333:6379" 22 | kvrocks: 23 | image: apache/kvrocks:2.2.0 24 | ports: 25 | - "6666:6666" 26 | redisearch: 27 | image: redislabs/redisearch:2.8.4 28 | ports: 29 | - "6377:6379" 30 | compat: 31 | image: redis/redis-stack:7.4.0-v0 32 | ports: 33 | - "6378:6379" 34 | compat5: 35 | image: redis:5-alpine 36 | ports: 37 | - "6356:6379" 38 | compat-redisearch: 39 | image: redis/redis-stack:7.4.0-v0 40 | ports: 41 | - "6381:6379" 42 | sentinel: 43 | image: redis:7.4-alpine 44 | entrypoint: 45 | - /bin/sh 46 | - -c 47 | - | 48 | redis-server --save "" --appendonly no --port 6380 & 49 | echo "sentinel monitor test 127.0.0.1 6380 2\n" > sentinel.conf 50 | redis-server sentinel.conf --sentinel 51 | ports: 52 | - "6380:6380" 53 | - "26379:26379" 54 | sentinel5: 55 | image: redis:5-alpine 56 | entrypoint: 57 | - /bin/sh 58 | - -c 59 | - | 60 | redis-server --save "" --appendonly no --port 6385 & 61 | echo "sentinel monitor test5 127.0.0.1 6385 2\n" > sentinel.conf 62 | redis-server sentinel.conf --sentinel 63 | ports: 64 | - "6385:6385" 65 | - "26355:26379" 66 | cluster: 67 | image: redis:7.4-alpine 68 | entrypoint: 69 | - /bin/sh 70 | - -c 71 | - | 72 | redis-server --port 7001 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7001.conf & 73 | redis-server --port 7002 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7002.conf & 74 | redis-server --port 7003 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7003.conf & 75 | while ! redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 --cluster-yes; do sleep 1; done 76 | wait 77 | ports: 78 | - "7001:7001" 79 | - "7002:7002" 80 | - "7003:7003" 81 | cluster5: 82 | image: redis:5-alpine 83 | entrypoint: 84 | - /bin/sh 85 | - -c 86 | - | 87 | redis-server --port 7004 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7004.conf & 88 | redis-server --port 7005 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7005.conf & 89 | redis-server --port 7006 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7006.conf & 90 | while ! redis-cli --cluster create 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-yes; do sleep 1; done 91 | wait 92 | ports: 93 | - "7004:7004" 94 | - "7005:7005" 95 | - "7006:7006" 96 | cluster5adapter: 97 | image: redis:5-alpine 98 | entrypoint: 99 | - /bin/sh 100 | - -c 101 | - | 102 | redis-server --port 7007 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7007.conf & 103 | redis-server --port 7008 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7008.conf & 104 | redis-server --port 7009 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7009.conf & 105 | while ! redis-cli --cluster create 127.0.0.1:7007 127.0.0.1:7008 127.0.0.1:7009 --cluster-yes; do sleep 1; done 106 | wait 107 | ports: 108 | - "7007:7007" 109 | - "7008:7008" 110 | - "7009:7009" 111 | clusteradapter: 112 | image: redis:7.4-alpine 113 | entrypoint: 114 | - /bin/sh 115 | - -c 116 | - | 117 | redis-server --port 7010 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7010.conf & 118 | redis-server --port 7011 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7011.conf & 119 | redis-server --port 7012 --save "" --appendonly no --cluster-enabled yes --cluster-config-file 7012.conf & 120 | while ! redis-cli --cluster create 127.0.0.1:7010 127.0.0.1:7011 127.0.0.1:7012 --cluster-yes; do sleep 1; done 121 | wait 122 | ports: 123 | - "7010:7010" 124 | - "7011:7011" 125 | - "7012:7012" 126 | -------------------------------------------------------------------------------- /dockertest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev 4 | 5 | go vet ./... 6 | 7 | go install honnef.co/go/tools/cmd/staticcheck@latest 8 | # disabled checks 9 | # -ST1000 missing package doc in internal packages 10 | # -ST1003 wrong naming convention would require breaking changes 11 | # -ST1012 wrong error name convention in om package would require breaking changes 12 | # -ST1016 violation of methods on the same type should have the same receiver name in rueidishook 13 | # -ST1020 violation of go doc comment on exported methods in rueidiscompat 14 | # -ST1021 violation of go doc comment on exported types in rueidiscompat 15 | # -U1000 unused check in mock package 16 | staticcheck -checks "all,-ST1000,-ST1003,-ST1012,-ST1016,-ST1020,-ST1021,-U1000" ./... | (grep -v "_test.go:" && exit 1 || exit 0) 17 | 18 | trap "docker compose down -v" EXIT 19 | docker compose up -d 20 | sleep 5 21 | go install gotest.tools/gotestsum@v1.10.0 22 | gotestsum --format standard-verbose --junitfile unit-tests.xml -- -coverprofile=coverage.out -race -timeout 30m "$@" 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/onsi/gomega v1.36.2 9 | golang.org/x/sys v0.31.0 10 | ) 11 | 12 | require ( 13 | github.com/google/go-cmp v0.7.0 // indirect 14 | github.com/kr/pretty v0.1.0 // indirect 15 | github.com/kr/text v0.2.0 // indirect 16 | golang.org/x/net v0.38.0 // indirect 17 | golang.org/x/text v0.23.0 // indirect 18 | golang.org/x/tools v0.31.0 // indirect 19 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 3 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 4 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 5 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 6 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 7 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 8 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 9 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 10 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 11 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 12 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 15 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 16 | github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= 17 | github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= 18 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 19 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 20 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 21 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 22 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 23 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 24 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 25 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 26 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 27 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 30 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /hack/cmds/commands_cell.json: -------------------------------------------------------------------------------- 1 | { 2 | "CL.THROTTLE": { 3 | "summary": "Creates or Get a Throttle", 4 | "arguments": [ 5 | { 6 | "name": "key", 7 | "type": "key" 8 | }, 9 | { 10 | "name": "max_burst", 11 | "type": "integer" 12 | }, 13 | { 14 | "name": "count_per_period", 15 | "type": "integer" 16 | }, 17 | { 18 | "name": "period", 19 | "type": "integer" 20 | }, 21 | { 22 | "name": "quantity", 23 | "type": "integer", 24 | "optional": true 25 | } 26 | ], 27 | "group": "cl" 28 | } 29 | } -------------------------------------------------------------------------------- /hack/cmds/commands_gears2.json: -------------------------------------------------------------------------------- 1 | { 2 | "TFCALL": { 3 | "summary": "Invoke a JavaScript function", 4 | "since": "2.0.0", 5 | "group": "triggers_and_functions", 6 | "complexity": "Depends on the function that is executed.", 7 | "arguments": [ 8 | { 9 | "name": "library.function", 10 | "type": "string" 11 | }, 12 | { 13 | "name": "numkeys", 14 | "type": "integer" 15 | }, 16 | { 17 | "name": "key", 18 | "type": "key", 19 | "key_space_index": 0, 20 | "optional": true, 21 | "multiple": true 22 | }, 23 | { 24 | "name": "arg", 25 | "type": "string", 26 | "optional": true, 27 | "multiple": true 28 | } 29 | ] 30 | }, 31 | "TFCALLASYNC": { 32 | "summary": "Invoke an asynchronous JavaScript function", 33 | "since": "2.0.0", 34 | "group": "triggers_and_functions", 35 | "complexity": "Depends on the function that is executed.", 36 | "arguments": [ 37 | { 38 | "name": "library.function", 39 | "type": "string" 40 | }, 41 | { 42 | "name": "numkeys", 43 | "type": "integer" 44 | }, 45 | { 46 | "name": "key", 47 | "type": "key", 48 | "key_space_index": 0, 49 | "optional": true, 50 | "multiple": true 51 | }, 52 | { 53 | "name": "arg", 54 | "type": "string", 55 | "optional": true, 56 | "multiple": true 57 | } 58 | ] 59 | }, 60 | "TFUNCTION DELETE": { 61 | "summary": "Delete a JavaScript library from Redis by name", 62 | "since": "2.0.0", 63 | "group": "triggers_and_functions", 64 | "complexity": "O(1)", 65 | "arguments": [ 66 | { 67 | "name": "library-name", 68 | "type": "string", 69 | "display_text": "library name" 70 | } 71 | ] 72 | }, 73 | "TFUNCTION LOAD": { 74 | "summary": "Load a new JavaScript library into Redis", 75 | "since": "2.0.0", 76 | "group": "triggers_and_functions", 77 | "complexity": "O(1)", 78 | "arguments": [ 79 | { 80 | "name": "replace", 81 | "type": "pure-token", 82 | "display_text": "replace", 83 | "token": "REPLACE", 84 | "optional": true 85 | }, 86 | { 87 | "command": "CONFIG", 88 | "name": [ 89 | "config" 90 | ], 91 | "type": [ 92 | "string" 93 | ], 94 | "optional": true 95 | }, 96 | { 97 | "name": "library-code", 98 | "type": "string", 99 | "display_text": "library code" 100 | } 101 | ] 102 | }, 103 | "TFUNCTION LIST": { 104 | "summary": "List all JavaScript libraries loaded into Redis", 105 | "since": "2.0.0", 106 | "group": "triggers_and_functions", 107 | "complexity": "O(N) where N is the number of libraries loaded into Redis", 108 | "arguments": [ 109 | { 110 | "name": "library-name", 111 | "type": "string", 112 | "display_text": "library name", 113 | "optional": true 114 | }, 115 | { 116 | "name": "withcode", 117 | "type": "pure-token", 118 | "display_text": "withcode", 119 | "token": "WITHCODE", 120 | "optional": true 121 | }, 122 | { 123 | "name": "verbose", 124 | "type": "pure-token", 125 | "display_text": "verbose", 126 | "token": "VERBOSE", 127 | "optional": true 128 | }, 129 | { 130 | "name": "v", 131 | "type": "pure-token", 132 | "display_text": "v", 133 | "token": "V", 134 | "optional": true 135 | } 136 | ] 137 | } 138 | } -------------------------------------------------------------------------------- /hack/cmds/commands_sentinel.json: -------------------------------------------------------------------------------- 1 | { 2 | "SENTINEL GET-MASTER-ADDR-BY-NAME": { 3 | "arguments": [ 4 | { 5 | "name": "master", 6 | "type": "string" 7 | } 8 | ], 9 | "group": "sentinel" 10 | }, 11 | "SENTINEL SENTINELS": { 12 | "arguments": [ 13 | { 14 | "name": "master", 15 | "type": "string" 16 | } 17 | ], 18 | "group": "sentinel" 19 | }, 20 | "SENTINEL FAILOVER": { 21 | "arguments": [ 22 | { 23 | "name": "master", 24 | "type": "string" 25 | } 26 | ], 27 | "group": "sentinel" 28 | }, 29 | "SENTINEL REPLICAS": { 30 | "arguments": [ 31 | { 32 | "name": "master", 33 | "type": "string" 34 | } 35 | ], 36 | "group": "sentinel" 37 | } 38 | } -------------------------------------------------------------------------------- /internal/cmds/builder.go: -------------------------------------------------------------------------------- 1 | package cmds 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | const ErrBuiltTwice = "a command should not be built twice" 9 | const ErrUnfinished = "a command should be finished by calling Build() or Cache()" 10 | 11 | var pool = &sync.Pool{New: func() any { 12 | return &CommandSlice{s: make([]string, 0, 2), l: -1} 13 | }} 14 | 15 | // CommandSlice is the command container managed by the sync.Pool 16 | type CommandSlice struct { 17 | s []string 18 | l int32 19 | r int32 20 | } 21 | 22 | func (cs *CommandSlice) Build() { 23 | if cs.l != -1 { 24 | panic(ErrBuiltTwice) 25 | } 26 | cs.l = int32(len(cs.s)) 27 | } 28 | 29 | func (cs *CommandSlice) Verify() { 30 | if cs.l != int32(len(cs.s)) { 31 | panic(ErrUnfinished) 32 | } 33 | } 34 | 35 | func newCommandSlice(s []string) *CommandSlice { 36 | return &CommandSlice{s: s, l: int32(len(s))} 37 | } 38 | 39 | // NewBuilder creates a Builder and initializes the internal sync.Pool 40 | func NewBuilder(initSlot uint16) Builder { 41 | return Builder{ks: initSlot} 42 | } 43 | 44 | // Builder builds commands by reusing CommandSlice from the sync.Pool 45 | type Builder struct { 46 | ks uint16 47 | } 48 | 49 | func get() *CommandSlice { 50 | return pool.Get().(*CommandSlice) 51 | } 52 | 53 | // PutCompletedForce recycles the Completed regardless of the c.cs.r 54 | func PutCompletedForce(c Completed) { 55 | Put(c.cs) 56 | } 57 | 58 | // PutCompleted recycles the Completed 59 | func PutCompleted(c Completed) { 60 | if c.cs.r == 0 { 61 | Put(c.cs) 62 | } 63 | } 64 | 65 | // PutCacheable recycles the Cacheable 66 | func PutCacheable(c Cacheable) { 67 | if c.cs.r == 0 { 68 | Put(c.cs) 69 | } 70 | } 71 | 72 | // Arbitrary allows user to build an arbitrary redis command with Builder.Arbitrary 73 | type Arbitrary Completed 74 | 75 | // Arbitrary allows user to build an arbitrary redis command by following Arbitrary.Keys and Arbitrary.Args 76 | func (b Builder) Arbitrary(token ...string) (c Arbitrary) { 77 | c = Arbitrary{cs: get(), ks: b.ks} 78 | c.cs.s = append(c.cs.s, token...) 79 | return c 80 | } 81 | 82 | // Keys calculate which key slot the command belongs to. 83 | // Users must use Keys to construct the key part of the command, otherwise 84 | // the command will not be sent to correct redis node. 85 | func (c Arbitrary) Keys(keys ...string) Arbitrary { 86 | if c.ks&NoSlot == NoSlot { 87 | for _, k := range keys { 88 | c.ks = NoSlot | slot(k) 89 | break 90 | } 91 | } else { 92 | for _, k := range keys { 93 | c.ks = check(c.ks, slot(k)) 94 | } 95 | } 96 | c.cs.s = append(c.cs.s, keys...) 97 | return c 98 | } 99 | 100 | // Args is used to construct non-key parts of the command. 101 | func (c Arbitrary) Args(args ...string) Arbitrary { 102 | c.cs.s = append(c.cs.s, args...) 103 | return c 104 | } 105 | 106 | // Build is used to complete constructing a command 107 | func (c Arbitrary) Build() Completed { 108 | if len(c.cs.s) == 0 || len(c.cs.s[0]) == 0 { 109 | panic(arbitraryNoCommand) 110 | } 111 | if strings.HasSuffix(strings.ToUpper(c.cs.s[0]), "SUBSCRIBE") { 112 | panic(arbitrarySubscribe) 113 | } 114 | c.cs.Build() 115 | return Completed(c) 116 | } 117 | 118 | // Blocking is used to complete constructing a command and mark it as blocking command. 119 | // Blocking command will occupy a connection from a separated connection pool. 120 | func (c Arbitrary) Blocking() Completed { 121 | c.cf = blockTag 122 | return c.Build() 123 | } 124 | 125 | // ReadOnly is used to complete constructing a command and mark it as readonly command. 126 | // ReadOnly will be retried under network issues. 127 | func (c Arbitrary) ReadOnly() Completed { 128 | c.cf = readonly 129 | return c.Build() 130 | } 131 | 132 | // MultiGet is used to complete constructing a command and mark it as mtGetTag command. 133 | func (c Arbitrary) MultiGet() Completed { 134 | if len(c.cs.s) == 0 || len(c.cs.s[0]) == 0 { 135 | panic(arbitraryNoCommand) 136 | } 137 | if c.cs.s[0] != "MGET" && c.cs.s[0] != "JSON.MGET" { 138 | panic(arbitraryMultiGet) 139 | } 140 | c.cf = mtGetTag 141 | return c.Build() 142 | } 143 | 144 | // IsZero is used to test if Arbitrary is initialized 145 | func (c Arbitrary) IsZero() bool { 146 | return c.cs == nil 147 | } 148 | 149 | var ( 150 | arbitraryNoCommand = "Arbitrary should be provided with redis command" 151 | arbitrarySubscribe = "Arbitrary does not support SUBSCRIBE/UNSUBSCRIBE" 152 | arbitraryMultiGet = "Arbitrary.MultiGet is only valid for MGET and JSON.MGET" 153 | ) 154 | -------------------------------------------------------------------------------- /internal/cmds/builder_put.go: -------------------------------------------------------------------------------- 1 | package cmds 2 | 3 | func Put(cs *CommandSlice) { 4 | clear(cs.s) 5 | cs.s = cs.s[:0] 6 | cs.l = -1 7 | cs.r = 0 8 | pool.Put(cs) 9 | } 10 | -------------------------------------------------------------------------------- /internal/cmds/builder_test.go: -------------------------------------------------------------------------------- 1 | package cmds 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestPutCompleted(t *testing.T) { 9 | retry: 10 | cs1 := get() 11 | cs1.s = append(cs1.s, "1", "1", "1", "1", "1") 12 | PutCompleted(Completed{cs: cs1}) 13 | cs2 := get() 14 | if cs1 != cs2 { 15 | goto retry 16 | } 17 | if len(cs2.s) != 0 { 18 | t.Fatalf("Put doesn't clean the CommandSlice") 19 | } 20 | } 21 | 22 | func TestPutCompletedForce(t *testing.T) { 23 | retry: 24 | cs1 := get() 25 | cs1.s = append(cs1.s, "1", "1", "1", "1", "1") 26 | cs1.r = 1 // pin 27 | PutCompletedForce(Completed{cs: cs1}) 28 | cs2 := get() 29 | if cs1 != cs2 { 30 | goto retry 31 | } 32 | if len(cs2.s) != 0 { 33 | t.Fatalf("PutCompletedForce doesn't clean the CommandSlice") 34 | } 35 | } 36 | 37 | func TestPutCacheable(t *testing.T) { 38 | retry: 39 | cs1 := get() 40 | cs1.s = append(cs1.s, "1", "1", "1", "1", "1") 41 | PutCacheable(Cacheable{cs: cs1}) 42 | cs2 := get() 43 | if cs1 != cs2 { 44 | goto retry 45 | } 46 | if len(cs2.s) != 0 { 47 | t.Fatalf("Put doesn't clean the CommandSlice") 48 | } 49 | } 50 | 51 | func TestArbitraryIsZero(t *testing.T) { 52 | builder := NewBuilder(NoSlot) 53 | if cmd := builder.Arbitrary("any", "cmd"); cmd.IsZero() { 54 | t.Fatalf("arbitrary failed") 55 | } 56 | var cmd Arbitrary 57 | if !cmd.IsZero() { 58 | t.Fatalf("arbitrary failed") 59 | } 60 | } 61 | 62 | func TestArbitrary(t *testing.T) { 63 | builder := NewBuilder(NoSlot) 64 | cmd := builder.Arbitrary("any", "cmd").Keys("k1", "k2").Args("a1", "a2") 65 | if c := cmd.Build(); !reflect.DeepEqual(c.Commands(), []string{"any", "cmd", "k1", "k2", "a1", "a2"}) { 66 | t.Fatalf("arbitrary failed") 67 | } 68 | if c := builder.Arbitrary("any").Blocking(); !c.IsBlock() { 69 | t.Fatalf("arbitrary failed") 70 | } 71 | if c := builder.Arbitrary("any").ReadOnly(); !c.IsReadOnly() { 72 | t.Fatalf("arbitrary failed") 73 | } 74 | 75 | builder2 := NewBuilder(InitSlot) 76 | 77 | defer func() { 78 | if e := recover(); e != multiKeySlotErr { 79 | t.Errorf("arbitrary not check slots") 80 | } 81 | }() 82 | 83 | builder2.Arbitrary().Keys("k1", "k2") 84 | } 85 | 86 | func TestEmptyArbitrary(t *testing.T) { 87 | builder := NewBuilder(NoSlot) 88 | defer func() { 89 | if e := recover(); e != arbitraryNoCommand { 90 | t.Errorf("arbitrary not check empty") 91 | } 92 | }() 93 | builder.Arbitrary().Build() 94 | } 95 | 96 | func TestEmptySubscribe(t *testing.T) { 97 | builder := NewBuilder(NoSlot) 98 | defer func() { 99 | if e := recover(); e != arbitrarySubscribe { 100 | t.Errorf("arbitrary not check subscribe command") 101 | } 102 | }() 103 | builder.Arbitrary("SUBSCRIBE").Build() 104 | } 105 | 106 | func TestEmptyArbitraryMultiGet(t *testing.T) { 107 | builder := NewBuilder(NoSlot) 108 | defer func() { 109 | if e := recover(); e != arbitraryNoCommand { 110 | t.Errorf("arbitrary not check empty") 111 | } 112 | }() 113 | builder.Arbitrary().MultiGet() 114 | } 115 | 116 | func TestArbitraryMultiGet(t *testing.T) { 117 | builder := NewBuilder(NoSlot) 118 | cacheable := Cacheable(builder.Arbitrary("MGET").Args("KKK").MultiGet()) 119 | if !cacheable.IsMGet() { 120 | t.Fatalf("arbitrary failed") 121 | } 122 | } 123 | 124 | func TestArbitraryMultiGetPanic(t *testing.T) { 125 | builder := NewBuilder(NoSlot) 126 | defer func() { 127 | if e := recover(); e != arbitraryMultiGet { 128 | t.Errorf("arbitrary not check MGET command") 129 | } 130 | }() 131 | builder.Arbitrary("SUBSCRIBE").MultiGet() 132 | } 133 | 134 | func TestBuiltTwice(t *testing.T) { 135 | src := NewBuilder(NoSlot).Get() 136 | cmd1 := src.Key("a") 137 | cmd2 := src.Key("b") 138 | cmd1.Build() 139 | defer func() { 140 | if e := recover(); e != ErrBuiltTwice { 141 | t.Errorf("arbitrary not check MGET command") 142 | } 143 | }() 144 | cmd2.Build() 145 | } 146 | 147 | func TestVerify(t *testing.T) { 148 | src := NewBuilder(NoSlot).Get() 149 | cmd1 := src.Key("a").Build() 150 | cmd1.cs.Verify() 151 | src.Key("b") 152 | defer func() { 153 | if e := recover(); e != ErrUnfinished { 154 | t.Errorf("arbitrary not check MGET command") 155 | } 156 | }() 157 | cmd1.cs.Verify() 158 | } 159 | -------------------------------------------------------------------------------- /internal/cmds/gen_bf_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func bf0(s Builder) { 8 | s.BfAdd().Key("1").Item("1").Build() 9 | s.BfCard().Key("1").Build() 10 | s.BfExists().Key("1").Item("1").Build() 11 | s.BfExists().Key("1").Item("1").Cache() 12 | s.BfInfo().Key("1").Capacity().Build() 13 | s.BfInfo().Key("1").Capacity().Cache() 14 | s.BfInfo().Key("1").Size().Build() 15 | s.BfInfo().Key("1").Size().Cache() 16 | s.BfInfo().Key("1").Filters().Build() 17 | s.BfInfo().Key("1").Filters().Cache() 18 | s.BfInfo().Key("1").Items().Build() 19 | s.BfInfo().Key("1").Items().Cache() 20 | s.BfInfo().Key("1").Expansion().Build() 21 | s.BfInfo().Key("1").Expansion().Cache() 22 | s.BfInfo().Key("1").Build() 23 | s.BfInfo().Key("1").Cache() 24 | s.BfInsert().Key("1").Capacity(1).Error(1).Expansion(1).Nocreate().Nonscaling().Items().Item("1").Item("1").Build() 25 | s.BfInsert().Key("1").Capacity(1).Error(1).Expansion(1).Nocreate().Items().Item("1").Item("1").Build() 26 | s.BfInsert().Key("1").Capacity(1).Error(1).Expansion(1).Nonscaling().Items().Item("1").Item("1").Build() 27 | s.BfInsert().Key("1").Capacity(1).Error(1).Expansion(1).Items().Item("1").Item("1").Build() 28 | s.BfInsert().Key("1").Capacity(1).Error(1).Nocreate().Nonscaling().Items().Item("1").Item("1").Build() 29 | s.BfInsert().Key("1").Capacity(1).Error(1).Nonscaling().Items().Item("1").Item("1").Build() 30 | s.BfInsert().Key("1").Capacity(1).Error(1).Items().Item("1").Item("1").Build() 31 | s.BfInsert().Key("1").Capacity(1).Expansion(1).Nocreate().Nonscaling().Items().Item("1").Item("1").Build() 32 | s.BfInsert().Key("1").Capacity(1).Nocreate().Nonscaling().Items().Item("1").Item("1").Build() 33 | s.BfInsert().Key("1").Capacity(1).Nonscaling().Items().Item("1").Item("1").Build() 34 | s.BfInsert().Key("1").Capacity(1).Items().Item("1").Item("1").Build() 35 | s.BfInsert().Key("1").Error(1).Expansion(1).Nocreate().Nonscaling().Items().Item("1").Item("1").Build() 36 | s.BfInsert().Key("1").Expansion(1).Nocreate().Nonscaling().Items().Item("1").Item("1").Build() 37 | s.BfInsert().Key("1").Nocreate().Nonscaling().Items().Item("1").Item("1").Build() 38 | s.BfInsert().Key("1").Nonscaling().Items().Item("1").Item("1").Build() 39 | s.BfInsert().Key("1").Items().Item("1").Item("1").Build() 40 | s.BfLoadchunk().Key("1").Iterator(1).Data("1").Build() 41 | s.BfMadd().Key("1").Item("1").Item("1").Build() 42 | s.BfMexists().Key("1").Item("1").Item("1").Build() 43 | s.BfReserve().Key("1").ErrorRate(1).Capacity(1).Expansion(1).Nonscaling().Build() 44 | s.BfReserve().Key("1").ErrorRate(1).Capacity(1).Expansion(1).Build() 45 | s.BfReserve().Key("1").ErrorRate(1).Capacity(1).Nonscaling().Build() 46 | s.BfReserve().Key("1").ErrorRate(1).Capacity(1).Build() 47 | s.BfScandump().Key("1").Iterator(1).Build() 48 | } 49 | 50 | func TestCommand_InitSlot_bf(t *testing.T) { 51 | var s = NewBuilder(InitSlot) 52 | t.Run("0", func(t *testing.T) { bf0(s) }) 53 | } 54 | 55 | func TestCommand_NoSlot_bf(t *testing.T) { 56 | var s = NewBuilder(NoSlot) 57 | t.Run("0", func(t *testing.T) { bf0(s) }) 58 | } 59 | -------------------------------------------------------------------------------- /internal/cmds/gen_bitmap_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func bitmap0(s Builder) { 8 | s.Bitcount().Key("1").Start(1).End(1).Byte().Build() 9 | s.Bitcount().Key("1").Start(1).End(1).Byte().Cache() 10 | s.Bitcount().Key("1").Start(1).End(1).Bit().Build() 11 | s.Bitcount().Key("1").Start(1).End(1).Bit().Cache() 12 | s.Bitcount().Key("1").Start(1).End(1).Build() 13 | s.Bitcount().Key("1").Start(1).End(1).Cache() 14 | s.Bitcount().Key("1").Build() 15 | s.Bitcount().Key("1").Cache() 16 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Set("1", 1, 1).Incrby("1", 1, 1).Incrby("1", 1, 1).Build() 17 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Set("1", 1, 1).Set("1", 1, 1).Build() 18 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).OverflowWrap().Incrby("1", 1, 1).Build() 19 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).OverflowSat().Set("1", 1, 1).Build() 20 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).OverflowSat().Incrby("1", 1, 1).Build() 21 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).OverflowFail().Set("1", 1, 1).Build() 22 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).OverflowFail().Incrby("1", 1, 1).Build() 23 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).Set("1", 1, 1).Build() 24 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).Incrby("1", 1, 1).Build() 25 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).Get("1", 1).Incrby("1", 1, 1).Build() 26 | s.Bitfield().Key("1").Get("1", 1).OverflowWrap().Set("1", 1, 1).Incrby("1", 1, 1).Get("1", 1).Get("1", 1).Build() 27 | s.Bitfield().Key("1").OverflowWrap().Set("1", 1, 1).Build() 28 | s.Bitfield().Key("1").OverflowSat().Set("1", 1, 1).Build() 29 | s.Bitfield().Key("1").OverflowFail().Set("1", 1, 1).Build() 30 | s.Bitfield().Key("1").Set("1", 1, 1).Build() 31 | s.Bitfield().Key("1").Incrby("1", 1, 1).Build() 32 | s.Bitfield().Key("1").Build() 33 | s.BitfieldRo().Key("1").Get().Get("1", 1).Get("1", 1).Build() 34 | s.BitfieldRo().Key("1").Get().Get("1", 1).Get("1", 1).Cache() 35 | s.BitfieldRo().Key("1").Build() 36 | s.BitfieldRo().Key("1").Cache() 37 | s.Bitop().And().Destkey("1").Key("1").Key("1").Build() 38 | s.Bitop().Or().Destkey("1").Key("1").Key("1").Build() 39 | s.Bitop().Xor().Destkey("1").Key("1").Key("1").Build() 40 | s.Bitop().Not().Destkey("1").Key("1").Key("1").Build() 41 | s.Bitpos().Key("1").Bit(1).Start(1).End(1).Byte().Build() 42 | s.Bitpos().Key("1").Bit(1).Start(1).End(1).Byte().Cache() 43 | s.Bitpos().Key("1").Bit(1).Start(1).End(1).Bit().Build() 44 | s.Bitpos().Key("1").Bit(1).Start(1).End(1).Bit().Cache() 45 | s.Bitpos().Key("1").Bit(1).Start(1).End(1).Build() 46 | s.Bitpos().Key("1").Bit(1).Start(1).End(1).Cache() 47 | s.Bitpos().Key("1").Bit(1).Start(1).Build() 48 | s.Bitpos().Key("1").Bit(1).Start(1).Cache() 49 | s.Bitpos().Key("1").Bit(1).Build() 50 | s.Bitpos().Key("1").Bit(1).Cache() 51 | s.Getbit().Key("1").Offset(1).Build() 52 | s.Getbit().Key("1").Offset(1).Cache() 53 | s.Setbit().Key("1").Offset(1).Value(1).Build() 54 | } 55 | 56 | func TestCommand_InitSlot_bitmap(t *testing.T) { 57 | var s = NewBuilder(InitSlot) 58 | t.Run("0", func(t *testing.T) { bitmap0(s) }) 59 | } 60 | 61 | func TestCommand_NoSlot_bitmap(t *testing.T) { 62 | var s = NewBuilder(NoSlot) 63 | t.Run("0", func(t *testing.T) { bitmap0(s) }) 64 | } 65 | -------------------------------------------------------------------------------- /internal/cmds/gen_cf_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func cf0(s Builder) { 8 | s.CfAdd().Key("1").Item("1").Build() 9 | s.CfAddnx().Key("1").Item("1").Build() 10 | s.CfCount().Key("1").Item("1").Build() 11 | s.CfCount().Key("1").Item("1").Cache() 12 | s.CfDel().Key("1").Item("1").Build() 13 | s.CfExists().Key("1").Item("1").Build() 14 | s.CfExists().Key("1").Item("1").Cache() 15 | s.CfInfo().Key("1").Build() 16 | s.CfInfo().Key("1").Cache() 17 | s.CfInsert().Key("1").Capacity(1).Nocreate().Items().Item("1").Item("1").Build() 18 | s.CfInsert().Key("1").Capacity(1).Items().Item("1").Item("1").Build() 19 | s.CfInsert().Key("1").Nocreate().Items().Item("1").Item("1").Build() 20 | s.CfInsert().Key("1").Items().Item("1").Item("1").Build() 21 | s.CfInsertnx().Key("1").Capacity(1).Nocreate().Items().Item("1").Item("1").Build() 22 | s.CfInsertnx().Key("1").Capacity(1).Items().Item("1").Item("1").Build() 23 | s.CfInsertnx().Key("1").Nocreate().Items().Item("1").Item("1").Build() 24 | s.CfInsertnx().Key("1").Items().Item("1").Item("1").Build() 25 | s.CfLoadchunk().Key("1").Iterator(1).Data("1").Build() 26 | s.CfMexists().Key("1").Item("1").Item("1").Build() 27 | s.CfReserve().Key("1").Capacity(1).Bucketsize(1).Maxiterations(1).Expansion(1).Build() 28 | s.CfReserve().Key("1").Capacity(1).Bucketsize(1).Maxiterations(1).Build() 29 | s.CfReserve().Key("1").Capacity(1).Bucketsize(1).Expansion(1).Build() 30 | s.CfReserve().Key("1").Capacity(1).Bucketsize(1).Build() 31 | s.CfReserve().Key("1").Capacity(1).Maxiterations(1).Build() 32 | s.CfReserve().Key("1").Capacity(1).Expansion(1).Build() 33 | s.CfReserve().Key("1").Capacity(1).Build() 34 | s.CfScandump().Key("1").Iterator(1).Build() 35 | } 36 | 37 | func TestCommand_InitSlot_cf(t *testing.T) { 38 | var s = NewBuilder(InitSlot) 39 | t.Run("0", func(t *testing.T) { cf0(s) }) 40 | } 41 | 42 | func TestCommand_NoSlot_cf(t *testing.T) { 43 | var s = NewBuilder(NoSlot) 44 | t.Run("0", func(t *testing.T) { cf0(s) }) 45 | } 46 | -------------------------------------------------------------------------------- /internal/cmds/gen_cl.go: -------------------------------------------------------------------------------- 1 | // Code generated DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "strconv" 6 | 7 | type ClThrottle Incomplete 8 | 9 | func (b Builder) ClThrottle() (c ClThrottle) { 10 | c = ClThrottle{cs: get(), ks: b.ks} 11 | c.cs.s = append(c.cs.s, "CL.THROTTLE") 12 | return c 13 | } 14 | 15 | func (c ClThrottle) Key(key string) ClThrottleKey { 16 | if c.ks&NoSlot == NoSlot { 17 | c.ks = NoSlot | slot(key) 18 | } else { 19 | c.ks = check(c.ks, slot(key)) 20 | } 21 | c.cs.s = append(c.cs.s, key) 22 | return (ClThrottleKey)(c) 23 | } 24 | 25 | type ClThrottleCountPerPeriod Incomplete 26 | 27 | func (c ClThrottleCountPerPeriod) Period(period int64) ClThrottlePeriod { 28 | c.cs.s = append(c.cs.s, strconv.FormatInt(period, 10)) 29 | return (ClThrottlePeriod)(c) 30 | } 31 | 32 | type ClThrottleKey Incomplete 33 | 34 | func (c ClThrottleKey) MaxBurst(maxBurst int64) ClThrottleMaxBurst { 35 | c.cs.s = append(c.cs.s, strconv.FormatInt(maxBurst, 10)) 36 | return (ClThrottleMaxBurst)(c) 37 | } 38 | 39 | type ClThrottleMaxBurst Incomplete 40 | 41 | func (c ClThrottleMaxBurst) CountPerPeriod(countPerPeriod int64) ClThrottleCountPerPeriod { 42 | c.cs.s = append(c.cs.s, strconv.FormatInt(countPerPeriod, 10)) 43 | return (ClThrottleCountPerPeriod)(c) 44 | } 45 | 46 | type ClThrottlePeriod Incomplete 47 | 48 | func (c ClThrottlePeriod) Quantity(quantity int64) ClThrottleQuantity { 49 | c.cs.s = append(c.cs.s, strconv.FormatInt(quantity, 10)) 50 | return (ClThrottleQuantity)(c) 51 | } 52 | 53 | func (c ClThrottlePeriod) Build() Completed { 54 | c.cs.Build() 55 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 56 | } 57 | 58 | type ClThrottleQuantity Incomplete 59 | 60 | func (c ClThrottleQuantity) Build() Completed { 61 | c.cs.Build() 62 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 63 | } 64 | -------------------------------------------------------------------------------- /internal/cmds/gen_cl_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func cl0(s Builder) { 8 | s.ClThrottle().Key("1").MaxBurst(1).CountPerPeriod(1).Period(1).Quantity(1).Build() 9 | s.ClThrottle().Key("1").MaxBurst(1).CountPerPeriod(1).Period(1).Build() 10 | } 11 | 12 | func TestCommand_InitSlot_cl(t *testing.T) { 13 | var s = NewBuilder(InitSlot) 14 | t.Run("0", func(t *testing.T) { cl0(s) }) 15 | } 16 | 17 | func TestCommand_NoSlot_cl(t *testing.T) { 18 | var s = NewBuilder(NoSlot) 19 | t.Run("0", func(t *testing.T) { cl0(s) }) 20 | } 21 | -------------------------------------------------------------------------------- /internal/cmds/gen_cluster_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func cluster0(s Builder) { 8 | s.Asking().Build() 9 | s.ClusterAddslots().Slot(1).Slot(1).Build() 10 | s.ClusterAddslotsrange().StartSlotEndSlot().StartSlotEndSlot(1, 1).StartSlotEndSlot(1, 1).Build() 11 | s.ClusterBumpepoch().Build() 12 | s.ClusterCountFailureReports().NodeId("1").Build() 13 | s.ClusterCountkeysinslot().Slot(1).Build() 14 | s.ClusterDelslots().Slot(1).Slot(1).Build() 15 | s.ClusterDelslotsrange().StartSlotEndSlot().StartSlotEndSlot(1, 1).StartSlotEndSlot(1, 1).Build() 16 | s.ClusterFailover().Force().Build() 17 | s.ClusterFailover().Takeover().Build() 18 | s.ClusterFailover().Build() 19 | s.ClusterFlushslots().Build() 20 | s.ClusterForget().NodeId("1").Build() 21 | s.ClusterGetkeysinslot().Slot(1).Count(1).Build() 22 | s.ClusterInfo().Build() 23 | s.ClusterKeyslot().Key("1").Build() 24 | s.ClusterLinks().Build() 25 | s.ClusterMeet().Ip("1").Port(1).ClusterBusPort(1).Build() 26 | s.ClusterMeet().Ip("1").Port(1).Build() 27 | s.ClusterMyid().Build() 28 | s.ClusterMyshardid().Build() 29 | s.ClusterNodes().Build() 30 | s.ClusterReplicas().NodeId("1").Build() 31 | s.ClusterReplicate().NodeId("1").Build() 32 | s.ClusterReset().Hard().Build() 33 | s.ClusterReset().Soft().Build() 34 | s.ClusterReset().Build() 35 | s.ClusterSaveconfig().Build() 36 | s.ClusterSetConfigEpoch().ConfigEpoch(1).Build() 37 | s.ClusterSetslot().Slot(1).Importing().NodeId("1").Timeout(1).Build() 38 | s.ClusterSetslot().Slot(1).Importing().NodeId("1").Build() 39 | s.ClusterSetslot().Slot(1).Importing().Timeout(1).Build() 40 | s.ClusterSetslot().Slot(1).Importing().Build() 41 | s.ClusterSetslot().Slot(1).Migrating().NodeId("1").Build() 42 | s.ClusterSetslot().Slot(1).Migrating().Timeout(1).Build() 43 | s.ClusterSetslot().Slot(1).Migrating().Build() 44 | s.ClusterSetslot().Slot(1).Stable().NodeId("1").Build() 45 | s.ClusterSetslot().Slot(1).Stable().Timeout(1).Build() 46 | s.ClusterSetslot().Slot(1).Stable().Build() 47 | s.ClusterSetslot().Slot(1).Node().NodeId("1").Build() 48 | s.ClusterSetslot().Slot(1).Node().Timeout(1).Build() 49 | s.ClusterSetslot().Slot(1).Node().Build() 50 | s.ClusterShards().Build() 51 | s.ClusterSlaves().NodeId("1").Build() 52 | s.ClusterSlotStats().Slotsrange().StartSlot(1).EndSlot(1).Build() 53 | s.ClusterSlotStats().Orderby().Metric("1").Limit(1).Asc().Build() 54 | s.ClusterSlotStats().Orderby().Metric("1").Limit(1).Desc().Build() 55 | s.ClusterSlotStats().Orderby().Metric("1").Limit(1).Build() 56 | s.ClusterSlotStats().Orderby().Metric("1").Asc().Build() 57 | s.ClusterSlotStats().Orderby().Metric("1").Desc().Build() 58 | s.ClusterSlotStats().Orderby().Metric("1").Build() 59 | s.ClusterSlots().Build() 60 | s.Readonly().Build() 61 | s.Readwrite().Build() 62 | } 63 | 64 | func TestCommand_InitSlot_cluster(t *testing.T) { 65 | var s = NewBuilder(InitSlot) 66 | t.Run("0", func(t *testing.T) { cluster0(s) }) 67 | } 68 | 69 | func TestCommand_NoSlot_cluster(t *testing.T) { 70 | var s = NewBuilder(NoSlot) 71 | t.Run("0", func(t *testing.T) { cluster0(s) }) 72 | } 73 | -------------------------------------------------------------------------------- /internal/cmds/gen_cms_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func cms0(s Builder) { 8 | s.CmsIncrby().Key("1").Item("1").Increment(1).Item("1").Increment(1).Build() 9 | s.CmsInfo().Key("1").Build() 10 | s.CmsInfo().Key("1").Cache() 11 | s.CmsInitbydim().Key("1").Width(1).Depth(1).Build() 12 | s.CmsInitbyprob().Key("1").Error(1).Probability(1).Build() 13 | s.CmsMerge().Destination("1").Numkeys(1).Source("1").Source("1").Weights().Weight(1).Weight(1).Build() 14 | s.CmsMerge().Destination("1").Numkeys(1).Source("1").Source("1").Build() 15 | s.CmsQuery().Key("1").Item("1").Item("1").Build() 16 | s.CmsQuery().Key("1").Item("1").Item("1").Cache() 17 | } 18 | 19 | func TestCommand_InitSlot_cms(t *testing.T) { 20 | var s = NewBuilder(InitSlot) 21 | t.Run("0", func(t *testing.T) { cms0(s) }) 22 | } 23 | 24 | func TestCommand_NoSlot_cms(t *testing.T) { 25 | var s = NewBuilder(NoSlot) 26 | t.Run("0", func(t *testing.T) { cms0(s) }) 27 | } 28 | -------------------------------------------------------------------------------- /internal/cmds/gen_gears_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func gears0(s Builder) { 8 | s.RgAbortexecution().Id("1").Build() 9 | s.RgConfigget().Key("1").Key("1").Build() 10 | s.RgConfigset().KeyValue().KeyValue("1", "1").KeyValue("1", "1").Build() 11 | s.RgDropexecution().Id("1").Build() 12 | s.RgDumpexecutions().Build() 13 | s.RgDumpregistrations().Build() 14 | s.RgGetexecution().Id("1").Shard().Build() 15 | s.RgGetexecution().Id("1").Cluster().Build() 16 | s.RgGetexecution().Id("1").Build() 17 | s.RgGetresults().Id("1").Build() 18 | s.RgGetresultsblocking().Id("1").Build() 19 | s.RgInfocluster().Build() 20 | s.RgPydumpreqs().Build() 21 | s.RgPyexecute().Function("1").Unblocking().Id("1").Description("1").Upgrade().ReplaceWith("1").Requirements("1").Requirements("1").Build() 22 | s.RgPyexecute().Function("1").Unblocking().Id("1").Description("1").Upgrade().ReplaceWith("1").Build() 23 | s.RgPyexecute().Function("1").Unblocking().Id("1").Description("1").Upgrade().Requirements("1").Requirements("1").Build() 24 | s.RgPyexecute().Function("1").Unblocking().Id("1").Description("1").Upgrade().Build() 25 | s.RgPyexecute().Function("1").Unblocking().Id("1").Description("1").ReplaceWith("1").Build() 26 | s.RgPyexecute().Function("1").Unblocking().Id("1").Description("1").Requirements("1").Requirements("1").Build() 27 | s.RgPyexecute().Function("1").Unblocking().Id("1").Description("1").Build() 28 | s.RgPyexecute().Function("1").Unblocking().Id("1").Upgrade().Build() 29 | s.RgPyexecute().Function("1").Unblocking().Id("1").ReplaceWith("1").Build() 30 | s.RgPyexecute().Function("1").Unblocking().Id("1").Requirements("1").Requirements("1").Build() 31 | s.RgPyexecute().Function("1").Unblocking().Id("1").Build() 32 | s.RgPyexecute().Function("1").Unblocking().Description("1").Build() 33 | s.RgPyexecute().Function("1").Unblocking().Upgrade().Build() 34 | s.RgPyexecute().Function("1").Unblocking().ReplaceWith("1").Build() 35 | s.RgPyexecute().Function("1").Unblocking().Requirements("1").Requirements("1").Build() 36 | s.RgPyexecute().Function("1").Unblocking().Build() 37 | s.RgPyexecute().Function("1").Id("1").Build() 38 | s.RgPyexecute().Function("1").Description("1").Build() 39 | s.RgPyexecute().Function("1").Upgrade().Build() 40 | s.RgPyexecute().Function("1").ReplaceWith("1").Build() 41 | s.RgPyexecute().Function("1").Requirements("1").Requirements("1").Build() 42 | s.RgPyexecute().Function("1").Build() 43 | s.RgPystats().Build() 44 | s.RgRefreshcluster().Build() 45 | s.RgTrigger().Trigger("1").Argument("1").Argument("1").Build() 46 | s.RgUnregister().Id("1").Build() 47 | } 48 | 49 | func TestCommand_InitSlot_gears(t *testing.T) { 50 | var s = NewBuilder(InitSlot) 51 | t.Run("0", func(t *testing.T) { gears0(s) }) 52 | } 53 | 54 | func TestCommand_NoSlot_gears(t *testing.T) { 55 | var s = NewBuilder(NoSlot) 56 | t.Run("0", func(t *testing.T) { gears0(s) }) 57 | } 58 | -------------------------------------------------------------------------------- /internal/cmds/gen_graph_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func graph0(s Builder) { 8 | s.GraphConfigGet().Name("1").Build() 9 | s.GraphConfigSet().Name("1").Value("1").Build() 10 | s.GraphConstraintCreate().Key("1").Mandatory().Node("1").Properties(1).Prop("1").Prop("1").Build() 11 | s.GraphConstraintCreate().Key("1").Mandatory().Relationship("1").Properties(1).Prop("1").Prop("1").Build() 12 | s.GraphConstraintCreate().Key("1").Unique().Node("1").Properties(1).Prop("1").Prop("1").Build() 13 | s.GraphConstraintCreate().Key("1").Unique().Relationship("1").Properties(1).Prop("1").Prop("1").Build() 14 | s.GraphConstraintDrop().Key("1").Mandatory().Node("1").Properties(1).Prop("1").Prop("1").Build() 15 | s.GraphConstraintDrop().Key("1").Mandatory().Relationship("1").Properties(1).Prop("1").Prop("1").Build() 16 | s.GraphConstraintDrop().Key("1").Unique().Node("1").Properties(1).Prop("1").Prop("1").Build() 17 | s.GraphConstraintDrop().Key("1").Unique().Relationship("1").Properties(1).Prop("1").Prop("1").Build() 18 | s.GraphDelete().Graph("1").Build() 19 | s.GraphExplain().Graph("1").Query("1").Build() 20 | s.GraphList().Build() 21 | s.GraphProfile().Graph("1").Query("1").Timeout(1).Build() 22 | s.GraphProfile().Graph("1").Query("1").Build() 23 | s.GraphQuery().Graph("1").Query("1").Timeout(1).Build() 24 | s.GraphQuery().Graph("1").Query("1").Build() 25 | s.GraphRoQuery().Graph("1").Query("1").Timeout(1).Build() 26 | s.GraphRoQuery().Graph("1").Query("1").Timeout(1).Cache() 27 | s.GraphRoQuery().Graph("1").Query("1").Build() 28 | s.GraphRoQuery().Graph("1").Query("1").Cache() 29 | s.GraphSlowlog().Graph("1").Build() 30 | } 31 | 32 | func TestCommand_InitSlot_graph(t *testing.T) { 33 | var s = NewBuilder(InitSlot) 34 | t.Run("0", func(t *testing.T) { graph0(s) }) 35 | } 36 | 37 | func TestCommand_NoSlot_graph(t *testing.T) { 38 | var s = NewBuilder(NoSlot) 39 | t.Run("0", func(t *testing.T) { graph0(s) }) 40 | } 41 | -------------------------------------------------------------------------------- /internal/cmds/gen_hyperloglog.go: -------------------------------------------------------------------------------- 1 | // Code generated DO NOT EDIT 2 | 3 | package cmds 4 | 5 | type Pfadd Incomplete 6 | 7 | func (b Builder) Pfadd() (c Pfadd) { 8 | c = Pfadd{cs: get(), ks: b.ks} 9 | c.cs.s = append(c.cs.s, "PFADD") 10 | return c 11 | } 12 | 13 | func (c Pfadd) Key(key string) PfaddKey { 14 | if c.ks&NoSlot == NoSlot { 15 | c.ks = NoSlot | slot(key) 16 | } else { 17 | c.ks = check(c.ks, slot(key)) 18 | } 19 | c.cs.s = append(c.cs.s, key) 20 | return (PfaddKey)(c) 21 | } 22 | 23 | type PfaddElement Incomplete 24 | 25 | func (c PfaddElement) Element(element ...string) PfaddElement { 26 | c.cs.s = append(c.cs.s, element...) 27 | return c 28 | } 29 | 30 | func (c PfaddElement) Build() Completed { 31 | c.cs.Build() 32 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 33 | } 34 | 35 | type PfaddKey Incomplete 36 | 37 | func (c PfaddKey) Element(element ...string) PfaddElement { 38 | c.cs.s = append(c.cs.s, element...) 39 | return (PfaddElement)(c) 40 | } 41 | 42 | func (c PfaddKey) Build() Completed { 43 | c.cs.Build() 44 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 45 | } 46 | 47 | type Pfcount Incomplete 48 | 49 | func (b Builder) Pfcount() (c Pfcount) { 50 | c = Pfcount{cs: get(), ks: b.ks, cf: int16(readonly)} 51 | c.cs.s = append(c.cs.s, "PFCOUNT") 52 | return c 53 | } 54 | 55 | func (c Pfcount) Key(key ...string) PfcountKey { 56 | if c.ks&NoSlot == NoSlot { 57 | for _, k := range key { 58 | c.ks = NoSlot | slot(k) 59 | break 60 | } 61 | } else { 62 | for _, k := range key { 63 | c.ks = check(c.ks, slot(k)) 64 | } 65 | } 66 | c.cs.s = append(c.cs.s, key...) 67 | return (PfcountKey)(c) 68 | } 69 | 70 | type PfcountKey Incomplete 71 | 72 | func (c PfcountKey) Key(key ...string) PfcountKey { 73 | if c.ks&NoSlot == NoSlot { 74 | for _, k := range key { 75 | c.ks = NoSlot | slot(k) 76 | break 77 | } 78 | } else { 79 | for _, k := range key { 80 | c.ks = check(c.ks, slot(k)) 81 | } 82 | } 83 | c.cs.s = append(c.cs.s, key...) 84 | return c 85 | } 86 | 87 | func (c PfcountKey) Build() Completed { 88 | c.cs.Build() 89 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 90 | } 91 | 92 | type Pfmerge Incomplete 93 | 94 | func (b Builder) Pfmerge() (c Pfmerge) { 95 | c = Pfmerge{cs: get(), ks: b.ks} 96 | c.cs.s = append(c.cs.s, "PFMERGE") 97 | return c 98 | } 99 | 100 | func (c Pfmerge) Destkey(destkey string) PfmergeDestkey { 101 | if c.ks&NoSlot == NoSlot { 102 | c.ks = NoSlot | slot(destkey) 103 | } else { 104 | c.ks = check(c.ks, slot(destkey)) 105 | } 106 | c.cs.s = append(c.cs.s, destkey) 107 | return (PfmergeDestkey)(c) 108 | } 109 | 110 | type PfmergeDestkey Incomplete 111 | 112 | func (c PfmergeDestkey) Sourcekey(sourcekey ...string) PfmergeSourcekey { 113 | if c.ks&NoSlot == NoSlot { 114 | for _, k := range sourcekey { 115 | c.ks = NoSlot | slot(k) 116 | break 117 | } 118 | } else { 119 | for _, k := range sourcekey { 120 | c.ks = check(c.ks, slot(k)) 121 | } 122 | } 123 | c.cs.s = append(c.cs.s, sourcekey...) 124 | return (PfmergeSourcekey)(c) 125 | } 126 | 127 | func (c PfmergeDestkey) Build() Completed { 128 | c.cs.Build() 129 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 130 | } 131 | 132 | type PfmergeSourcekey Incomplete 133 | 134 | func (c PfmergeSourcekey) Sourcekey(sourcekey ...string) PfmergeSourcekey { 135 | if c.ks&NoSlot == NoSlot { 136 | for _, k := range sourcekey { 137 | c.ks = NoSlot | slot(k) 138 | break 139 | } 140 | } else { 141 | for _, k := range sourcekey { 142 | c.ks = check(c.ks, slot(k)) 143 | } 144 | } 145 | c.cs.s = append(c.cs.s, sourcekey...) 146 | return c 147 | } 148 | 149 | func (c PfmergeSourcekey) Build() Completed { 150 | c.cs.Build() 151 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 152 | } 153 | -------------------------------------------------------------------------------- /internal/cmds/gen_hyperloglog_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func hyperloglog0(s Builder) { 8 | s.Pfadd().Key("1").Element("1").Element("1").Build() 9 | s.Pfadd().Key("1").Build() 10 | s.Pfcount().Key("1").Key("1").Build() 11 | s.Pfmerge().Destkey("1").Sourcekey("1").Sourcekey("1").Build() 12 | s.Pfmerge().Destkey("1").Build() 13 | } 14 | 15 | func TestCommand_InitSlot_hyperloglog(t *testing.T) { 16 | var s = NewBuilder(InitSlot) 17 | t.Run("0", func(t *testing.T) { hyperloglog0(s) }) 18 | } 19 | 20 | func TestCommand_NoSlot_hyperloglog(t *testing.T) { 21 | var s = NewBuilder(NoSlot) 22 | t.Run("0", func(t *testing.T) { hyperloglog0(s) }) 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmds/gen_inference_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func inference0(s Builder) { 8 | s.AiModelexecute().Key("1").Inputs(1).Input("1").Input("1").Outputs(1).Output("1").Output("1").Timeout(1).Build() 9 | s.AiModelexecute().Key("1").Inputs(1).Input("1").Input("1").Outputs(1).Output("1").Output("1").Timeout(1).Cache() 10 | s.AiModelexecute().Key("1").Inputs(1).Input("1").Input("1").Outputs(1).Output("1").Output("1").Build() 11 | s.AiModelexecute().Key("1").Inputs(1).Input("1").Input("1").Outputs(1).Output("1").Output("1").Cache() 12 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Inputs(1).Input("1").Input("1").Args(1).Arg("1").Arg("1").Outputs(1).Output("1").Output("1").Timeout(1).Build() 13 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Inputs(1).Input("1").Input("1").Args(1).Arg("1").Arg("1").Outputs(1).Output("1").Output("1").Build() 14 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Inputs(1).Input("1").Input("1").Args(1).Arg("1").Arg("1").Timeout(1).Build() 15 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Inputs(1).Input("1").Input("1").Args(1).Arg("1").Arg("1").Build() 16 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Inputs(1).Input("1").Input("1").Outputs(1).Output("1").Output("1").Build() 17 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Inputs(1).Input("1").Input("1").Timeout(1).Build() 18 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Inputs(1).Input("1").Input("1").Build() 19 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Args(1).Arg("1").Arg("1").Build() 20 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Outputs(1).Output("1").Output("1").Build() 21 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Timeout(1).Build() 22 | s.AiScriptexecute().Key("1").Function("1").Keys(1).Key("1").Key("1").Build() 23 | s.AiScriptexecute().Key("1").Function("1").Inputs(1).Input("1").Input("1").Build() 24 | s.AiScriptexecute().Key("1").Function("1").Args(1).Arg("1").Arg("1").Build() 25 | s.AiScriptexecute().Key("1").Function("1").Outputs(1).Output("1").Output("1").Build() 26 | s.AiScriptexecute().Key("1").Function("1").Timeout(1).Build() 27 | s.AiScriptexecute().Key("1").Function("1").Build() 28 | } 29 | 30 | func TestCommand_InitSlot_inference(t *testing.T) { 31 | var s = NewBuilder(InitSlot) 32 | t.Run("0", func(t *testing.T) { inference0(s) }) 33 | } 34 | 35 | func TestCommand_NoSlot_inference(t *testing.T) { 36 | var s = NewBuilder(NoSlot) 37 | t.Run("0", func(t *testing.T) { inference0(s) }) 38 | } 39 | -------------------------------------------------------------------------------- /internal/cmds/gen_json_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func json0(s Builder) { 8 | s.JsonArrappend().Key("1").Path("1").Value("1").Value("1").Build() 9 | s.JsonArrappend().Key("1").Value("1").Value("1").Build() 10 | s.JsonArrindex().Key("1").Path("1").Value("1").Start(1).Stop(1).Build() 11 | s.JsonArrindex().Key("1").Path("1").Value("1").Start(1).Stop(1).Cache() 12 | s.JsonArrindex().Key("1").Path("1").Value("1").Start(1).Build() 13 | s.JsonArrindex().Key("1").Path("1").Value("1").Start(1).Cache() 14 | s.JsonArrindex().Key("1").Path("1").Value("1").Build() 15 | s.JsonArrindex().Key("1").Path("1").Value("1").Cache() 16 | s.JsonArrinsert().Key("1").Path("1").Index(1).Value("1").Value("1").Build() 17 | s.JsonArrlen().Key("1").Path("1").Build() 18 | s.JsonArrlen().Key("1").Path("1").Cache() 19 | s.JsonArrlen().Key("1").Build() 20 | s.JsonArrlen().Key("1").Cache() 21 | s.JsonArrpop().Key("1").Path("1").Index(1).Build() 22 | s.JsonArrpop().Key("1").Path("1").Build() 23 | s.JsonArrpop().Key("1").Build() 24 | s.JsonArrtrim().Key("1").Path("1").Start(1).Stop(1).Build() 25 | s.JsonClear().Key("1").Path("1").Build() 26 | s.JsonClear().Key("1").Build() 27 | s.JsonDebugHelp().Build() 28 | s.JsonDebugMemory().Key("1").Path("1").Build() 29 | s.JsonDebugMemory().Key("1").Build() 30 | s.JsonDel().Key("1").Path("1").Build() 31 | s.JsonDel().Key("1").Build() 32 | s.JsonForget().Key("1").Path("1").Build() 33 | s.JsonForget().Key("1").Build() 34 | s.JsonGet().Key("1").Indent("1").Newline("1").Space("1").Path("1").Path("1").Build() 35 | s.JsonGet().Key("1").Indent("1").Newline("1").Space("1").Path("1").Path("1").Cache() 36 | s.JsonGet().Key("1").Indent("1").Newline("1").Space("1").Build() 37 | s.JsonGet().Key("1").Indent("1").Newline("1").Space("1").Cache() 38 | s.JsonGet().Key("1").Indent("1").Newline("1").Path("1").Path("1").Build() 39 | s.JsonGet().Key("1").Indent("1").Newline("1").Path("1").Path("1").Cache() 40 | s.JsonGet().Key("1").Indent("1").Newline("1").Build() 41 | s.JsonGet().Key("1").Indent("1").Newline("1").Cache() 42 | s.JsonGet().Key("1").Indent("1").Space("1").Build() 43 | s.JsonGet().Key("1").Indent("1").Space("1").Cache() 44 | s.JsonGet().Key("1").Indent("1").Path("1").Path("1").Build() 45 | s.JsonGet().Key("1").Indent("1").Path("1").Path("1").Cache() 46 | s.JsonGet().Key("1").Indent("1").Build() 47 | s.JsonGet().Key("1").Indent("1").Cache() 48 | s.JsonGet().Key("1").Newline("1").Build() 49 | s.JsonGet().Key("1").Newline("1").Cache() 50 | s.JsonGet().Key("1").Space("1").Build() 51 | s.JsonGet().Key("1").Space("1").Cache() 52 | s.JsonGet().Key("1").Path("1").Path("1").Build() 53 | s.JsonGet().Key("1").Path("1").Path("1").Cache() 54 | s.JsonGet().Key("1").Build() 55 | s.JsonGet().Key("1").Cache() 56 | s.JsonMerge().Key("1").Path("1").Value("1").Build() 57 | s.JsonMget().Key("1").Key("1").Path("1").Build() 58 | s.JsonMget().Key("1").Key("1").Path("1").Cache() 59 | s.JsonMset().Key("1").Path("1").Value("1").Key("1").Path("1").Value("1").Build() 60 | s.JsonNumincrby().Key("1").Path("1").Value(1).Build() 61 | s.JsonNummultby().Key("1").Path("1").Value(1).Build() 62 | s.JsonObjkeys().Key("1").Path("1").Build() 63 | s.JsonObjkeys().Key("1").Path("1").Cache() 64 | s.JsonObjkeys().Key("1").Build() 65 | s.JsonObjkeys().Key("1").Cache() 66 | s.JsonObjlen().Key("1").Path("1").Build() 67 | s.JsonObjlen().Key("1").Path("1").Cache() 68 | s.JsonObjlen().Key("1").Build() 69 | s.JsonObjlen().Key("1").Cache() 70 | s.JsonResp().Key("1").Path("1").Build() 71 | s.JsonResp().Key("1").Path("1").Cache() 72 | s.JsonResp().Key("1").Build() 73 | s.JsonResp().Key("1").Cache() 74 | s.JsonSet().Key("1").Path("1").Value("1").Nx().Build() 75 | s.JsonSet().Key("1").Path("1").Value("1").Xx().Build() 76 | s.JsonSet().Key("1").Path("1").Value("1").Build() 77 | s.JsonStrappend().Key("1").Path("1").Value("1").Build() 78 | s.JsonStrappend().Key("1").Value("1").Build() 79 | s.JsonStrlen().Key("1").Path("1").Build() 80 | s.JsonStrlen().Key("1").Path("1").Cache() 81 | s.JsonStrlen().Key("1").Build() 82 | s.JsonStrlen().Key("1").Cache() 83 | s.JsonToggle().Key("1").Path("1").Build() 84 | s.JsonType().Key("1").Path("1").Build() 85 | s.JsonType().Key("1").Path("1").Cache() 86 | s.JsonType().Key("1").Build() 87 | s.JsonType().Key("1").Cache() 88 | } 89 | 90 | func TestCommand_InitSlot_json(t *testing.T) { 91 | var s = NewBuilder(InitSlot) 92 | t.Run("0", func(t *testing.T) { json0(s) }) 93 | } 94 | 95 | func TestCommand_NoSlot_json(t *testing.T) { 96 | var s = NewBuilder(NoSlot) 97 | t.Run("0", func(t *testing.T) { json0(s) }) 98 | } 99 | -------------------------------------------------------------------------------- /internal/cmds/gen_list_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func list0(s Builder) { 8 | s.Blmove().Source("1").Destination("1").Left().Left().Timeout(1).Build() 9 | s.Blmove().Source("1").Destination("1").Left().Right().Timeout(1).Build() 10 | s.Blmove().Source("1").Destination("1").Right().Left().Timeout(1).Build() 11 | s.Blmove().Source("1").Destination("1").Right().Right().Timeout(1).Build() 12 | s.Blmpop().Timeout(1).Numkeys(1).Key("1").Key("1").Left().Count(1).Build() 13 | s.Blmpop().Timeout(1).Numkeys(1).Key("1").Key("1").Left().Build() 14 | s.Blmpop().Timeout(1).Numkeys(1).Key("1").Key("1").Right().Count(1).Build() 15 | s.Blmpop().Timeout(1).Numkeys(1).Key("1").Key("1").Right().Build() 16 | s.Blpop().Key("1").Key("1").Timeout(1).Build() 17 | s.Brpop().Key("1").Key("1").Timeout(1).Build() 18 | s.Brpoplpush().Source("1").Destination("1").Timeout(1).Build() 19 | s.Lindex().Key("1").Index(1).Build() 20 | s.Lindex().Key("1").Index(1).Cache() 21 | s.Linsert().Key("1").Before().Pivot("1").Element("1").Build() 22 | s.Linsert().Key("1").After().Pivot("1").Element("1").Build() 23 | s.Llen().Key("1").Build() 24 | s.Llen().Key("1").Cache() 25 | s.Lmove().Source("1").Destination("1").Left().Left().Build() 26 | s.Lmove().Source("1").Destination("1").Left().Right().Build() 27 | s.Lmove().Source("1").Destination("1").Right().Left().Build() 28 | s.Lmove().Source("1").Destination("1").Right().Right().Build() 29 | s.Lmpop().Numkeys(1).Key("1").Key("1").Left().Count(1).Build() 30 | s.Lmpop().Numkeys(1).Key("1").Key("1").Left().Build() 31 | s.Lmpop().Numkeys(1).Key("1").Key("1").Right().Count(1).Build() 32 | s.Lmpop().Numkeys(1).Key("1").Key("1").Right().Build() 33 | s.Lpop().Key("1").Count(1).Build() 34 | s.Lpop().Key("1").Build() 35 | s.Lpos().Key("1").Element("1").Rank(1).Count(1).Maxlen(1).Build() 36 | s.Lpos().Key("1").Element("1").Rank(1).Count(1).Maxlen(1).Cache() 37 | s.Lpos().Key("1").Element("1").Rank(1).Count(1).Build() 38 | s.Lpos().Key("1").Element("1").Rank(1).Count(1).Cache() 39 | s.Lpos().Key("1").Element("1").Rank(1).Maxlen(1).Build() 40 | s.Lpos().Key("1").Element("1").Rank(1).Maxlen(1).Cache() 41 | s.Lpos().Key("1").Element("1").Rank(1).Build() 42 | s.Lpos().Key("1").Element("1").Rank(1).Cache() 43 | s.Lpos().Key("1").Element("1").Count(1).Build() 44 | s.Lpos().Key("1").Element("1").Count(1).Cache() 45 | s.Lpos().Key("1").Element("1").Maxlen(1).Build() 46 | s.Lpos().Key("1").Element("1").Maxlen(1).Cache() 47 | s.Lpos().Key("1").Element("1").Build() 48 | s.Lpos().Key("1").Element("1").Cache() 49 | s.Lpush().Key("1").Element("1").Element("1").Build() 50 | s.Lpushx().Key("1").Element("1").Element("1").Build() 51 | s.Lrange().Key("1").Start(1).Stop(1).Build() 52 | s.Lrange().Key("1").Start(1).Stop(1).Cache() 53 | s.Lrem().Key("1").Count(1).Element("1").Build() 54 | s.Lset().Key("1").Index(1).Element("1").Build() 55 | s.Ltrim().Key("1").Start(1).Stop(1).Build() 56 | s.Rpop().Key("1").Count(1).Build() 57 | s.Rpop().Key("1").Build() 58 | s.Rpoplpush().Source("1").Destination("1").Build() 59 | s.Rpush().Key("1").Element("1").Element("1").Build() 60 | s.Rpushx().Key("1").Element("1").Element("1").Build() 61 | } 62 | 63 | func TestCommand_InitSlot_list(t *testing.T) { 64 | var s = NewBuilder(InitSlot) 65 | t.Run("0", func(t *testing.T) { list0(s) }) 66 | } 67 | 68 | func TestCommand_NoSlot_list(t *testing.T) { 69 | var s = NewBuilder(NoSlot) 70 | t.Run("0", func(t *testing.T) { list0(s) }) 71 | } 72 | -------------------------------------------------------------------------------- /internal/cmds/gen_model_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func model0(s Builder) { 8 | s.AiModeldel().Key("1").Build() 9 | s.AiModelget().Key("1").Meta().Blob().Build() 10 | s.AiModelget().Key("1").Meta().Blob().Cache() 11 | s.AiModelget().Key("1").Meta().Build() 12 | s.AiModelget().Key("1").Meta().Cache() 13 | s.AiModelget().Key("1").Blob().Build() 14 | s.AiModelget().Key("1").Blob().Cache() 15 | s.AiModelget().Key("1").Build() 16 | s.AiModelget().Key("1").Cache() 17 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Minbatchtimeout(1).Inputs(1).Input("1").Input("1").Outputs(1).Output("1").Output("1").Blob("1").Build() 18 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Minbatchtimeout(1).Inputs(1).Input("1").Input("1").Outputs(1).Output("1").Output("1").Build() 19 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Minbatchtimeout(1).Inputs(1).Input("1").Input("1").Blob("1").Build() 20 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Minbatchtimeout(1).Inputs(1).Input("1").Input("1").Build() 21 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Minbatchtimeout(1).Outputs(1).Output("1").Output("1").Build() 22 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Minbatchtimeout(1).Blob("1").Build() 23 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Minbatchtimeout(1).Build() 24 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Inputs(1).Input("1").Input("1").Build() 25 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Outputs(1).Output("1").Output("1").Build() 26 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Blob("1").Build() 27 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchsize(1).Build() 28 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Minbatchtimeout(1).Build() 29 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Inputs(1).Input("1").Input("1").Build() 30 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Outputs(1).Output("1").Output("1").Build() 31 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Blob("1").Build() 32 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Batchsize(1).Build() 33 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Minbatchsize(1).Build() 34 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Minbatchtimeout(1).Build() 35 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Inputs(1).Input("1").Input("1").Build() 36 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Outputs(1).Output("1").Output("1").Build() 37 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Blob("1").Build() 38 | s.AiModelstore().Key("1").Tf().Cpu().Tag("1").Build() 39 | s.AiModelstore().Key("1").Tf().Cpu().Batchsize(1).Build() 40 | s.AiModelstore().Key("1").Tf().Cpu().Minbatchsize(1).Build() 41 | s.AiModelstore().Key("1").Tf().Cpu().Minbatchtimeout(1).Build() 42 | s.AiModelstore().Key("1").Tf().Cpu().Inputs(1).Input("1").Input("1").Build() 43 | s.AiModelstore().Key("1").Tf().Cpu().Outputs(1).Output("1").Output("1").Build() 44 | s.AiModelstore().Key("1").Tf().Cpu().Blob("1").Build() 45 | s.AiModelstore().Key("1").Tf().Cpu().Build() 46 | s.AiModelstore().Key("1").Tf().Gpu().Tag("1").Build() 47 | s.AiModelstore().Key("1").Tf().Gpu().Batchsize(1).Build() 48 | s.AiModelstore().Key("1").Tf().Gpu().Minbatchsize(1).Build() 49 | s.AiModelstore().Key("1").Tf().Gpu().Minbatchtimeout(1).Build() 50 | s.AiModelstore().Key("1").Tf().Gpu().Inputs(1).Input("1").Input("1").Build() 51 | s.AiModelstore().Key("1").Tf().Gpu().Outputs(1).Output("1").Output("1").Build() 52 | s.AiModelstore().Key("1").Tf().Gpu().Blob("1").Build() 53 | s.AiModelstore().Key("1").Tf().Gpu().Build() 54 | s.AiModelstore().Key("1").Torch().Cpu().Build() 55 | s.AiModelstore().Key("1").Torch().Gpu().Build() 56 | s.AiModelstore().Key("1").Onnx().Cpu().Build() 57 | s.AiModelstore().Key("1").Onnx().Gpu().Build() 58 | } 59 | 60 | func TestCommand_InitSlot_model(t *testing.T) { 61 | var s = NewBuilder(InitSlot) 62 | t.Run("0", func(t *testing.T) { model0(s) }) 63 | } 64 | 65 | func TestCommand_NoSlot_model(t *testing.T) { 66 | var s = NewBuilder(NoSlot) 67 | t.Run("0", func(t *testing.T) { model0(s) }) 68 | } 69 | -------------------------------------------------------------------------------- /internal/cmds/gen_pubsub_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func pubsub0(s Builder) { 8 | s.Psubscribe().Pattern("1").Pattern("1").Build() 9 | s.Publish().Channel("1").Message("1").Build() 10 | s.PubsubChannels().Pattern("1").Build() 11 | s.PubsubChannels().Build() 12 | s.PubsubHelp().Build() 13 | s.PubsubNumpat().Build() 14 | s.PubsubNumsub().Channel("1").Channel("1").Build() 15 | s.PubsubNumsub().Build() 16 | s.PubsubShardchannels().Pattern("1").Build() 17 | s.PubsubShardchannels().Build() 18 | s.PubsubShardnumsub().Channel("1").Channel("1").Build() 19 | s.PubsubShardnumsub().Build() 20 | s.Punsubscribe().Pattern("1").Pattern("1").Build() 21 | s.Punsubscribe().Build() 22 | s.Spublish().Channel("1").Message("1").Build() 23 | s.Ssubscribe().Channel("1").Channel("1").Build() 24 | s.Subscribe().Channel("1").Channel("1").Build() 25 | s.Sunsubscribe().Channel("1").Channel("1").Build() 26 | s.Sunsubscribe().Build() 27 | s.Unsubscribe().Channel("1").Channel("1").Build() 28 | s.Unsubscribe().Build() 29 | } 30 | 31 | func TestCommand_InitSlot_pubsub(t *testing.T) { 32 | var s = NewBuilder(InitSlot) 33 | t.Run("0", func(t *testing.T) { pubsub0(s) }) 34 | } 35 | 36 | func TestCommand_NoSlot_pubsub(t *testing.T) { 37 | var s = NewBuilder(NoSlot) 38 | t.Run("0", func(t *testing.T) { pubsub0(s) }) 39 | } 40 | -------------------------------------------------------------------------------- /internal/cmds/gen_script_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func script0(s Builder) { 8 | s.AiScriptdel().Key("1").Build() 9 | s.AiScriptget().Key("1").Meta().Source().Build() 10 | s.AiScriptget().Key("1").Meta().Source().Cache() 11 | s.AiScriptget().Key("1").Meta().Build() 12 | s.AiScriptget().Key("1").Meta().Cache() 13 | s.AiScriptget().Key("1").Source().Build() 14 | s.AiScriptget().Key("1").Source().Cache() 15 | s.AiScriptget().Key("1").Build() 16 | s.AiScriptget().Key("1").Cache() 17 | s.AiScriptstore().Key("1").Cpu().Tag("1").EntryPoints(1).EntryPoint("1").EntryPoint("1").Build() 18 | s.AiScriptstore().Key("1").Cpu().EntryPoints(1).EntryPoint("1").EntryPoint("1").Build() 19 | s.AiScriptstore().Key("1").Gpu().Tag("1").EntryPoints(1).EntryPoint("1").EntryPoint("1").Build() 20 | s.AiScriptstore().Key("1").Gpu().EntryPoints(1).EntryPoint("1").EntryPoint("1").Build() 21 | } 22 | 23 | func TestCommand_InitSlot_script(t *testing.T) { 24 | var s = NewBuilder(InitSlot) 25 | t.Run("0", func(t *testing.T) { script0(s) }) 26 | } 27 | 28 | func TestCommand_NoSlot_script(t *testing.T) { 29 | var s = NewBuilder(NoSlot) 30 | t.Run("0", func(t *testing.T) { script0(s) }) 31 | } 32 | -------------------------------------------------------------------------------- /internal/cmds/gen_scripting_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func scripting0(s Builder) { 8 | s.Eval().Script("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 9 | s.Eval().Script("1").Numkeys(1).Key("1").Key("1").Build() 10 | s.Eval().Script("1").Numkeys(1).Arg("1").Arg("1").Build() 11 | s.Eval().Script("1").Numkeys(1).Build() 12 | s.EvalRo().Script("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 13 | s.EvalRo().Script("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Cache() 14 | s.EvalRo().Script("1").Numkeys(1).Key("1").Key("1").Build() 15 | s.EvalRo().Script("1").Numkeys(1).Key("1").Key("1").Cache() 16 | s.EvalRo().Script("1").Numkeys(1).Arg("1").Arg("1").Build() 17 | s.EvalRo().Script("1").Numkeys(1).Arg("1").Arg("1").Cache() 18 | s.EvalRo().Script("1").Numkeys(1).Build() 19 | s.EvalRo().Script("1").Numkeys(1).Cache() 20 | s.Evalsha().Sha1("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 21 | s.Evalsha().Sha1("1").Numkeys(1).Key("1").Key("1").Build() 22 | s.Evalsha().Sha1("1").Numkeys(1).Arg("1").Arg("1").Build() 23 | s.Evalsha().Sha1("1").Numkeys(1).Build() 24 | s.EvalshaRo().Sha1("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 25 | s.EvalshaRo().Sha1("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Cache() 26 | s.EvalshaRo().Sha1("1").Numkeys(1).Key("1").Key("1").Build() 27 | s.EvalshaRo().Sha1("1").Numkeys(1).Key("1").Key("1").Cache() 28 | s.EvalshaRo().Sha1("1").Numkeys(1).Arg("1").Arg("1").Build() 29 | s.EvalshaRo().Sha1("1").Numkeys(1).Arg("1").Arg("1").Cache() 30 | s.EvalshaRo().Sha1("1").Numkeys(1).Build() 31 | s.EvalshaRo().Sha1("1").Numkeys(1).Cache() 32 | s.Fcall().Function("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 33 | s.Fcall().Function("1").Numkeys(1).Key("1").Key("1").Build() 34 | s.Fcall().Function("1").Numkeys(1).Arg("1").Arg("1").Build() 35 | s.Fcall().Function("1").Numkeys(1).Build() 36 | s.FcallRo().Function("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 37 | s.FcallRo().Function("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Cache() 38 | s.FcallRo().Function("1").Numkeys(1).Key("1").Key("1").Build() 39 | s.FcallRo().Function("1").Numkeys(1).Key("1").Key("1").Cache() 40 | s.FcallRo().Function("1").Numkeys(1).Arg("1").Arg("1").Build() 41 | s.FcallRo().Function("1").Numkeys(1).Arg("1").Arg("1").Cache() 42 | s.FcallRo().Function("1").Numkeys(1).Build() 43 | s.FcallRo().Function("1").Numkeys(1).Cache() 44 | s.FunctionDelete().LibraryName("1").Build() 45 | s.FunctionDump().Build() 46 | s.FunctionFlush().Async().Build() 47 | s.FunctionFlush().Sync().Build() 48 | s.FunctionFlush().Build() 49 | s.FunctionHelp().Build() 50 | s.FunctionKill().Build() 51 | s.FunctionList().Libraryname("1").Withcode().Build() 52 | s.FunctionList().Libraryname("1").Build() 53 | s.FunctionList().Withcode().Build() 54 | s.FunctionList().Build() 55 | s.FunctionLoad().Replace().FunctionCode("1").Build() 56 | s.FunctionLoad().FunctionCode("1").Build() 57 | s.FunctionRestore().SerializedValue("1").Flush().Build() 58 | s.FunctionRestore().SerializedValue("1").Append().Build() 59 | s.FunctionRestore().SerializedValue("1").Replace().Build() 60 | s.FunctionRestore().SerializedValue("1").Build() 61 | s.FunctionStats().Build() 62 | s.ScriptDebug().Yes().Build() 63 | s.ScriptDebug().Sync().Build() 64 | s.ScriptDebug().No().Build() 65 | s.ScriptExists().Sha1("1").Sha1("1").Build() 66 | s.ScriptFlush().Async().Build() 67 | s.ScriptFlush().Sync().Build() 68 | s.ScriptFlush().Build() 69 | s.ScriptKill().Build() 70 | s.ScriptLoad().Script("1").Build() 71 | s.ScriptShow().Sha1("1").Build() 72 | } 73 | 74 | func TestCommand_InitSlot_scripting(t *testing.T) { 75 | var s = NewBuilder(InitSlot) 76 | t.Run("0", func(t *testing.T) { scripting0(s) }) 77 | } 78 | 79 | func TestCommand_NoSlot_scripting(t *testing.T) { 80 | var s = NewBuilder(NoSlot) 81 | t.Run("0", func(t *testing.T) { scripting0(s) }) 82 | } 83 | -------------------------------------------------------------------------------- /internal/cmds/gen_sentinel.go: -------------------------------------------------------------------------------- 1 | // Code generated DO NOT EDIT 2 | 3 | package cmds 4 | 5 | type SentinelFailover Incomplete 6 | 7 | func (b Builder) SentinelFailover() (c SentinelFailover) { 8 | c = SentinelFailover{cs: get(), ks: b.ks} 9 | c.cs.s = append(c.cs.s, "SENTINEL", "FAILOVER") 10 | return c 11 | } 12 | 13 | func (c SentinelFailover) Master(master string) SentinelFailoverMaster { 14 | c.cs.s = append(c.cs.s, master) 15 | return (SentinelFailoverMaster)(c) 16 | } 17 | 18 | type SentinelFailoverMaster Incomplete 19 | 20 | func (c SentinelFailoverMaster) Build() Completed { 21 | c.cs.Build() 22 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 23 | } 24 | 25 | type SentinelGetMasterAddrByName Incomplete 26 | 27 | func (b Builder) SentinelGetMasterAddrByName() (c SentinelGetMasterAddrByName) { 28 | c = SentinelGetMasterAddrByName{cs: get(), ks: b.ks} 29 | c.cs.s = append(c.cs.s, "SENTINEL", "GET-MASTER-ADDR-BY-NAME") 30 | return c 31 | } 32 | 33 | func (c SentinelGetMasterAddrByName) Master(master string) SentinelGetMasterAddrByNameMaster { 34 | c.cs.s = append(c.cs.s, master) 35 | return (SentinelGetMasterAddrByNameMaster)(c) 36 | } 37 | 38 | type SentinelGetMasterAddrByNameMaster Incomplete 39 | 40 | func (c SentinelGetMasterAddrByNameMaster) Build() Completed { 41 | c.cs.Build() 42 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 43 | } 44 | 45 | type SentinelReplicas Incomplete 46 | 47 | func (b Builder) SentinelReplicas() (c SentinelReplicas) { 48 | c = SentinelReplicas{cs: get(), ks: b.ks} 49 | c.cs.s = append(c.cs.s, "SENTINEL", "REPLICAS") 50 | return c 51 | } 52 | 53 | func (c SentinelReplicas) Master(master string) SentinelReplicasMaster { 54 | c.cs.s = append(c.cs.s, master) 55 | return (SentinelReplicasMaster)(c) 56 | } 57 | 58 | type SentinelReplicasMaster Incomplete 59 | 60 | func (c SentinelReplicasMaster) Build() Completed { 61 | c.cs.Build() 62 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 63 | } 64 | 65 | type SentinelSentinels Incomplete 66 | 67 | func (b Builder) SentinelSentinels() (c SentinelSentinels) { 68 | c = SentinelSentinels{cs: get(), ks: b.ks} 69 | c.cs.s = append(c.cs.s, "SENTINEL", "SENTINELS") 70 | return c 71 | } 72 | 73 | func (c SentinelSentinels) Master(master string) SentinelSentinelsMaster { 74 | c.cs.s = append(c.cs.s, master) 75 | return (SentinelSentinelsMaster)(c) 76 | } 77 | 78 | type SentinelSentinelsMaster Incomplete 79 | 80 | func (c SentinelSentinelsMaster) Build() Completed { 81 | c.cs.Build() 82 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 83 | } 84 | -------------------------------------------------------------------------------- /internal/cmds/gen_sentinel_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func sentinel0(s Builder) { 8 | s.SentinelFailover().Master("1").Build() 9 | s.SentinelGetMasterAddrByName().Master("1").Build() 10 | s.SentinelReplicas().Master("1").Build() 11 | s.SentinelSentinels().Master("1").Build() 12 | } 13 | 14 | func TestCommand_InitSlot_sentinel(t *testing.T) { 15 | var s = NewBuilder(InitSlot) 16 | t.Run("0", func(t *testing.T) { sentinel0(s) }) 17 | } 18 | 19 | func TestCommand_NoSlot_sentinel(t *testing.T) { 20 | var s = NewBuilder(NoSlot) 21 | t.Run("0", func(t *testing.T) { sentinel0(s) }) 22 | } 23 | -------------------------------------------------------------------------------- /internal/cmds/gen_set_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func set0(s Builder) { 8 | s.Sadd().Key("1").Member("1").Member("1").Build() 9 | s.Scard().Key("1").Build() 10 | s.Scard().Key("1").Cache() 11 | s.Sdiff().Key("1").Key("1").Build() 12 | s.Sdiffstore().Destination("1").Key("1").Key("1").Build() 13 | s.Sinter().Key("1").Key("1").Build() 14 | s.Sintercard().Numkeys(1).Key("1").Key("1").Limit(1).Build() 15 | s.Sintercard().Numkeys(1).Key("1").Key("1").Build() 16 | s.Sinterstore().Destination("1").Key("1").Key("1").Build() 17 | s.Sismember().Key("1").Member("1").Build() 18 | s.Sismember().Key("1").Member("1").Cache() 19 | s.Smembers().Key("1").Build() 20 | s.Smembers().Key("1").Cache() 21 | s.Smismember().Key("1").Member("1").Member("1").Build() 22 | s.Smismember().Key("1").Member("1").Member("1").Cache() 23 | s.Smove().Source("1").Destination("1").Member("1").Build() 24 | s.Spop().Key("1").Count(1).Build() 25 | s.Spop().Key("1").Build() 26 | s.Srandmember().Key("1").Count(1).Build() 27 | s.Srandmember().Key("1").Build() 28 | s.Srem().Key("1").Member("1").Member("1").Build() 29 | s.Sscan().Key("1").Cursor(1).Match("1").Count(1).Build() 30 | s.Sscan().Key("1").Cursor(1).Match("1").Build() 31 | s.Sscan().Key("1").Cursor(1).Count(1).Build() 32 | s.Sscan().Key("1").Cursor(1).Build() 33 | s.Sunion().Key("1").Key("1").Build() 34 | s.Sunionstore().Destination("1").Key("1").Key("1").Build() 35 | } 36 | 37 | func TestCommand_InitSlot_set(t *testing.T) { 38 | var s = NewBuilder(InitSlot) 39 | t.Run("0", func(t *testing.T) { set0(s) }) 40 | } 41 | 42 | func TestCommand_NoSlot_set(t *testing.T) { 43 | var s = NewBuilder(NoSlot) 44 | t.Run("0", func(t *testing.T) { set0(s) }) 45 | } 46 | -------------------------------------------------------------------------------- /internal/cmds/gen_suggestion_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func suggestion0(s Builder) { 8 | s.FtSugadd().Key("1").String("1").Score(1).Incr().Payload("1").Build() 9 | s.FtSugadd().Key("1").String("1").Score(1).Incr().Build() 10 | s.FtSugadd().Key("1").String("1").Score(1).Payload("1").Build() 11 | s.FtSugadd().Key("1").String("1").Score(1).Build() 12 | s.FtSugdel().Key("1").String("1").Build() 13 | s.FtSugget().Key("1").Prefix("1").Fuzzy().Withscores().Withpayloads().Max(1).Build() 14 | s.FtSugget().Key("1").Prefix("1").Fuzzy().Withscores().Withpayloads().Build() 15 | s.FtSugget().Key("1").Prefix("1").Fuzzy().Withscores().Max(1).Build() 16 | s.FtSugget().Key("1").Prefix("1").Fuzzy().Withscores().Build() 17 | s.FtSugget().Key("1").Prefix("1").Fuzzy().Withpayloads().Build() 18 | s.FtSugget().Key("1").Prefix("1").Fuzzy().Max(1).Build() 19 | s.FtSugget().Key("1").Prefix("1").Fuzzy().Build() 20 | s.FtSugget().Key("1").Prefix("1").Withscores().Build() 21 | s.FtSugget().Key("1").Prefix("1").Withpayloads().Build() 22 | s.FtSugget().Key("1").Prefix("1").Max(1).Build() 23 | s.FtSugget().Key("1").Prefix("1").Build() 24 | s.FtSuglen().Key("1").Build() 25 | } 26 | 27 | func TestCommand_InitSlot_suggestion(t *testing.T) { 28 | var s = NewBuilder(InitSlot) 29 | t.Run("0", func(t *testing.T) { suggestion0(s) }) 30 | } 31 | 32 | func TestCommand_NoSlot_suggestion(t *testing.T) { 33 | var s = NewBuilder(NoSlot) 34 | t.Run("0", func(t *testing.T) { suggestion0(s) }) 35 | } 36 | -------------------------------------------------------------------------------- /internal/cmds/gen_tdigest_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func tdigest0(s Builder) { 8 | s.TdigestAdd().Key("1").Value(1).Value(1).Build() 9 | s.TdigestByrank().Key("1").Rank(1).Rank(1).Build() 10 | s.TdigestByrevrank().Key("1").ReverseRank(1).ReverseRank(1).Build() 11 | s.TdigestCdf().Key("1").Value(1).Value(1).Build() 12 | s.TdigestCreate().Key("1").Compression(1).Build() 13 | s.TdigestCreate().Key("1").Build() 14 | s.TdigestInfo().Key("1").Build() 15 | s.TdigestMax().Key("1").Build() 16 | s.TdigestMerge().DestinationKey("1").Numkeys(1).SourceKey("1").SourceKey("1").Compression(1).Override().Build() 17 | s.TdigestMerge().DestinationKey("1").Numkeys(1).SourceKey("1").SourceKey("1").Compression(1).Build() 18 | s.TdigestMerge().DestinationKey("1").Numkeys(1).SourceKey("1").SourceKey("1").Override().Build() 19 | s.TdigestMerge().DestinationKey("1").Numkeys(1).SourceKey("1").SourceKey("1").Build() 20 | s.TdigestMin().Key("1").Build() 21 | s.TdigestQuantile().Key("1").Quantile(1).Quantile(1).Build() 22 | s.TdigestRank().Key("1").Value(1).Value(1).Build() 23 | s.TdigestReset().Key("1").Build() 24 | s.TdigestRevrank().Key("1").Value(1).Value(1).Build() 25 | s.TdigestTrimmedMean().Key("1").LowCutQuantile(1).HighCutQuantile(1).Build() 26 | } 27 | 28 | func TestCommand_InitSlot_tdigest(t *testing.T) { 29 | var s = NewBuilder(InitSlot) 30 | t.Run("0", func(t *testing.T) { tdigest0(s) }) 31 | } 32 | 33 | func TestCommand_NoSlot_tdigest(t *testing.T) { 34 | var s = NewBuilder(NoSlot) 35 | t.Run("0", func(t *testing.T) { tdigest0(s) }) 36 | } 37 | -------------------------------------------------------------------------------- /internal/cmds/gen_tensor_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func tensor0(s Builder) { 8 | s.AiTensorget().Key("1").Meta().Blob().Build() 9 | s.AiTensorget().Key("1").Meta().Blob().Cache() 10 | s.AiTensorget().Key("1").Meta().Values().Build() 11 | s.AiTensorget().Key("1").Meta().Values().Cache() 12 | s.AiTensorget().Key("1").Meta().Build() 13 | s.AiTensorget().Key("1").Meta().Cache() 14 | s.AiTensorset().Key("1").Float().Shape(1).Shape(1).Blob("1").Values("1").Values("1").Build() 15 | s.AiTensorset().Key("1").Float().Shape(1).Shape(1).Blob("1").Build() 16 | s.AiTensorset().Key("1").Float().Shape(1).Shape(1).Values("1").Values("1").Build() 17 | s.AiTensorset().Key("1").Float().Shape(1).Shape(1).Build() 18 | s.AiTensorset().Key("1").Double().Shape(1).Shape(1).Build() 19 | s.AiTensorset().Key("1").Int8().Shape(1).Shape(1).Build() 20 | s.AiTensorset().Key("1").Int16().Shape(1).Shape(1).Build() 21 | s.AiTensorset().Key("1").Int32().Shape(1).Shape(1).Build() 22 | s.AiTensorset().Key("1").Int64().Shape(1).Shape(1).Build() 23 | s.AiTensorset().Key("1").Uint8().Shape(1).Shape(1).Build() 24 | s.AiTensorset().Key("1").Uint16().Shape(1).Shape(1).Build() 25 | s.AiTensorset().Key("1").String().Shape(1).Shape(1).Build() 26 | s.AiTensorset().Key("1").Bool().Shape(1).Shape(1).Build() 27 | } 28 | 29 | func TestCommand_InitSlot_tensor(t *testing.T) { 30 | var s = NewBuilder(InitSlot) 31 | t.Run("0", func(t *testing.T) { tensor0(s) }) 32 | } 33 | 34 | func TestCommand_NoSlot_tensor(t *testing.T) { 35 | var s = NewBuilder(NoSlot) 36 | t.Run("0", func(t *testing.T) { tensor0(s) }) 37 | } 38 | -------------------------------------------------------------------------------- /internal/cmds/gen_topk_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func topk0(s Builder) { 8 | s.TopkAdd().Key("1").Items("1").Items("1").Build() 9 | s.TopkCount().Key("1").Item("1").Item("1").Build() 10 | s.TopkIncrby().Key("1").Item("1").Increment(1).Item("1").Increment(1).Build() 11 | s.TopkInfo().Key("1").Build() 12 | s.TopkInfo().Key("1").Cache() 13 | s.TopkList().Key("1").Withcount().Build() 14 | s.TopkList().Key("1").Withcount().Cache() 15 | s.TopkList().Key("1").Build() 16 | s.TopkList().Key("1").Cache() 17 | s.TopkQuery().Key("1").Item("1").Item("1").Build() 18 | s.TopkQuery().Key("1").Item("1").Item("1").Cache() 19 | s.TopkReserve().Key("1").Topk(1).Width(1).Depth(1).Decay(1).Build() 20 | s.TopkReserve().Key("1").Topk(1).Build() 21 | } 22 | 23 | func TestCommand_InitSlot_topk(t *testing.T) { 24 | var s = NewBuilder(InitSlot) 25 | t.Run("0", func(t *testing.T) { topk0(s) }) 26 | } 27 | 28 | func TestCommand_NoSlot_topk(t *testing.T) { 29 | var s = NewBuilder(NoSlot) 30 | t.Run("0", func(t *testing.T) { topk0(s) }) 31 | } 32 | -------------------------------------------------------------------------------- /internal/cmds/gen_transactions.go: -------------------------------------------------------------------------------- 1 | // Code generated DO NOT EDIT 2 | 3 | package cmds 4 | 5 | type Discard Incomplete 6 | 7 | func (b Builder) Discard() (c Discard) { 8 | c = Discard{cs: get(), ks: b.ks} 9 | c.cs.s = append(c.cs.s, "DISCARD") 10 | return c 11 | } 12 | 13 | func (c Discard) Build() Completed { 14 | c.cs.Build() 15 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 16 | } 17 | 18 | type Exec Incomplete 19 | 20 | func (b Builder) Exec() (c Exec) { 21 | c = Exec{cs: get(), ks: b.ks} 22 | c.cs.s = append(c.cs.s, "EXEC") 23 | return c 24 | } 25 | 26 | func (c Exec) Build() Completed { 27 | c.cs.Build() 28 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 29 | } 30 | 31 | type Multi Incomplete 32 | 33 | func (b Builder) Multi() (c Multi) { 34 | c = Multi{cs: get(), ks: b.ks} 35 | c.cs.s = append(c.cs.s, "MULTI") 36 | return c 37 | } 38 | 39 | func (c Multi) Build() Completed { 40 | c.cs.Build() 41 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 42 | } 43 | 44 | type Unwatch Incomplete 45 | 46 | func (b Builder) Unwatch() (c Unwatch) { 47 | c = Unwatch{cs: get(), ks: b.ks} 48 | c.cs.s = append(c.cs.s, "UNWATCH") 49 | return c 50 | } 51 | 52 | func (c Unwatch) Build() Completed { 53 | c.cs.Build() 54 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 55 | } 56 | 57 | type Watch Incomplete 58 | 59 | func (b Builder) Watch() (c Watch) { 60 | c = Watch{cs: get(), ks: b.ks} 61 | c.cs.s = append(c.cs.s, "WATCH") 62 | return c 63 | } 64 | 65 | func (c Watch) Key(key ...string) WatchKey { 66 | if c.ks&NoSlot == NoSlot { 67 | for _, k := range key { 68 | c.ks = NoSlot | slot(k) 69 | break 70 | } 71 | } else { 72 | for _, k := range key { 73 | c.ks = check(c.ks, slot(k)) 74 | } 75 | } 76 | c.cs.s = append(c.cs.s, key...) 77 | return (WatchKey)(c) 78 | } 79 | 80 | type WatchKey Incomplete 81 | 82 | func (c WatchKey) Key(key ...string) WatchKey { 83 | if c.ks&NoSlot == NoSlot { 84 | for _, k := range key { 85 | c.ks = NoSlot | slot(k) 86 | break 87 | } 88 | } else { 89 | for _, k := range key { 90 | c.ks = check(c.ks, slot(k)) 91 | } 92 | } 93 | c.cs.s = append(c.cs.s, key...) 94 | return c 95 | } 96 | 97 | func (c WatchKey) Build() Completed { 98 | c.cs.Build() 99 | return Completed{cs: c.cs, cf: uint16(c.cf), ks: c.ks} 100 | } 101 | -------------------------------------------------------------------------------- /internal/cmds/gen_transactions_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func transactions0(s Builder) { 8 | s.Discard().Build() 9 | s.Exec().Build() 10 | s.Multi().Build() 11 | s.Unwatch().Build() 12 | s.Watch().Key("1").Key("1").Build() 13 | } 14 | 15 | func TestCommand_InitSlot_transactions(t *testing.T) { 16 | var s = NewBuilder(InitSlot) 17 | t.Run("0", func(t *testing.T) { transactions0(s) }) 18 | } 19 | 20 | func TestCommand_NoSlot_transactions(t *testing.T) { 21 | var s = NewBuilder(NoSlot) 22 | t.Run("0", func(t *testing.T) { transactions0(s) }) 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmds/gen_triggers_and_functions_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT 2 | 3 | package cmds 4 | 5 | import "testing" 6 | 7 | func triggers_and_functions0(s Builder) { 8 | s.Tfcall().LibraryFunction("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 9 | s.Tfcall().LibraryFunction("1").Numkeys(1).Key("1").Key("1").Build() 10 | s.Tfcall().LibraryFunction("1").Numkeys(1).Arg("1").Arg("1").Build() 11 | s.Tfcall().LibraryFunction("1").Numkeys(1).Build() 12 | s.Tfcallasync().LibraryFunction("1").Numkeys(1).Key("1").Key("1").Arg("1").Arg("1").Build() 13 | s.Tfcallasync().LibraryFunction("1").Numkeys(1).Key("1").Key("1").Build() 14 | s.Tfcallasync().LibraryFunction("1").Numkeys(1).Arg("1").Arg("1").Build() 15 | s.Tfcallasync().LibraryFunction("1").Numkeys(1).Build() 16 | s.TfunctionDelete().LibraryName("1").Build() 17 | s.TfunctionList().LibraryName("1").Withcode().Verbose().V().Build() 18 | s.TfunctionList().LibraryName("1").Withcode().Verbose().Build() 19 | s.TfunctionList().LibraryName("1").Withcode().V().Build() 20 | s.TfunctionList().LibraryName("1").Withcode().Build() 21 | s.TfunctionList().LibraryName("1").Verbose().Build() 22 | s.TfunctionList().LibraryName("1").V().Build() 23 | s.TfunctionList().LibraryName("1").Build() 24 | s.TfunctionList().Withcode().Build() 25 | s.TfunctionList().Verbose().Build() 26 | s.TfunctionList().V().Build() 27 | s.TfunctionList().Build() 28 | s.TfunctionLoad().Replace().Config("1").LibraryCode("1").Build() 29 | s.TfunctionLoad().Replace().LibraryCode("1").Build() 30 | s.TfunctionLoad().Config("1").LibraryCode("1").Build() 31 | s.TfunctionLoad().LibraryCode("1").Build() 32 | } 33 | 34 | func TestCommand_InitSlot_triggers_and_functions(t *testing.T) { 35 | var s = NewBuilder(InitSlot) 36 | t.Run("0", func(t *testing.T) { triggers_and_functions0(s) }) 37 | } 38 | 39 | func TestCommand_NoSlot_triggers_and_functions(t *testing.T) { 40 | var s = NewBuilder(NoSlot) 41 | t.Run("0", func(t *testing.T) { triggers_and_functions0(s) }) 42 | } 43 | -------------------------------------------------------------------------------- /internal/cmds/iter.go: -------------------------------------------------------------------------------- 1 | //go:build go1.23 2 | 3 | package cmds 4 | 5 | import ( 6 | "iter" 7 | "strconv" 8 | ) 9 | 10 | func (c HmsetFieldValue) FieldValueIter(seq iter.Seq2[string, string]) HmsetFieldValue { 11 | for field, value := range seq { 12 | c.cs.s = append(c.cs.s, field, value) 13 | } 14 | return c 15 | } 16 | 17 | func (c HsetFieldValue) FieldValueIter(seq iter.Seq2[string, string]) HsetFieldValue { 18 | for field, value := range seq { 19 | c.cs.s = append(c.cs.s, field, value) 20 | } 21 | return c 22 | } 23 | 24 | func (c XaddFieldValue) FieldValueIter(seq iter.Seq2[string, string]) XaddFieldValue { 25 | for field, value := range seq { 26 | c.cs.s = append(c.cs.s, field, value) 27 | } 28 | return c 29 | } 30 | 31 | func (c ZaddScoreMember) ScoreMemberIter(seq iter.Seq2[string, float64]) ZaddScoreMember { 32 | for member, score := range seq { 33 | c.cs.s = append(c.cs.s, strconv.FormatFloat(score, 'f', -1, 64), member) 34 | } 35 | return c 36 | } 37 | -------------------------------------------------------------------------------- /internal/cmds/iter_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.23 2 | 3 | package cmds 4 | 5 | import ( 6 | "maps" 7 | "testing" 8 | ) 9 | 10 | func iter0(s Builder) { 11 | s.Hmset().Key("1").FieldValue().FieldValueIter(maps.All(map[string]string{"1": "1"})).Build() 12 | s.Hset().Key("1").FieldValue().FieldValueIter(maps.All(map[string]string{"1": "1"})).Build() 13 | s.Xadd().Key("1").Id("*").FieldValue().FieldValueIter(maps.All(map[string]string{"1": "1"})).Build() 14 | s.Zadd().Key("1").ScoreMember().ScoreMemberIter(maps.All(map[string]float64{"1": float64(1)})).Build() 15 | } 16 | 17 | func TestIter(t *testing.T) { 18 | var s = NewBuilder(InitSlot) 19 | t.Run("0", func(t *testing.T) { iter0(s) }) 20 | } 21 | -------------------------------------------------------------------------------- /internal/cmds/slot_test.go: -------------------------------------------------------------------------------- 1 | package cmds 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "testing" 8 | ) 9 | 10 | func TestSlot(t *testing.T) { 11 | t.Run("use tag", func(t *testing.T) { 12 | for i := 0; i < 10000; i++ { 13 | key1 := strconv.Itoa(rand.Int()) 14 | key2 := fmt.Sprintf("%s{%s}%s", strconv.Itoa(rand.Int()), key1, strconv.Itoa(rand.Int())) 15 | if slot(key1) != slot(key2) { 16 | t.Fatalf("%v and %v should be in the same slot", key1, key2) 17 | } 18 | } 19 | }) 20 | t.Run("not use tag", func(t *testing.T) { 21 | for i := 0; i < 1000; i++ { 22 | key1 := strconv.Itoa(i) 23 | key2 := fmt.Sprintf("%s{}", key1) 24 | if slot(key1) == slot(key2) { 25 | t.Fatalf("%v and %v should not be in the same slot", key1, key2) 26 | } 27 | } 28 | }) 29 | } 30 | 31 | func TestCRC16(t *testing.T) { 32 | t.Run("123456789", func(t *testing.T) { 33 | if v := crc16("123456789"); v != 0x31C3 { 34 | t.Fatalf("crc16(123456789) should be 0x31C3, but got %v", v) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /internal/util/parallel.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func ParallelKeys[K comparable, V any](maxp int, p map[K]V, fn func(k K)) { 8 | ch := make(chan K, len(p)) 9 | for k := range p { 10 | ch <- k 11 | } 12 | closeThenParallel(maxp, ch, fn) 13 | } 14 | 15 | func ParallelVals[K comparable, V any](maxp int, p map[K]V, fn func(k V)) { 16 | ch := make(chan V, len(p)) 17 | for _, v := range p { 18 | ch <- v 19 | } 20 | closeThenParallel(maxp, ch, fn) 21 | } 22 | 23 | func worker[V any](wg *sync.WaitGroup, ch chan V, fn func(k V)) { 24 | for v := range ch { 25 | fn(v) 26 | } 27 | wg.Done() 28 | } 29 | 30 | func closeThenParallel[V any](maxp int, ch chan V, fn func(k V)) { 31 | close(ch) 32 | concurrency := len(ch) 33 | if concurrency > maxp { 34 | concurrency = maxp 35 | } 36 | var wg sync.WaitGroup 37 | wg.Add(concurrency) 38 | for i := 1; i < concurrency; i++ { 39 | go worker(&wg, ch, fn) 40 | } 41 | if concurrency > 0 { 42 | worker(&wg, ch, fn) 43 | } 44 | wg.Wait() 45 | } 46 | -------------------------------------------------------------------------------- /internal/util/parallel_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "runtime" 5 | "sync/atomic" 6 | "testing" 7 | ) 8 | 9 | func TestParallelKeys(t *testing.T) { 10 | var sum int64 11 | data, sk, _ := gen(int64(runtime.NumCPU() * 1000)) 12 | ParallelKeys(runtime.GOMAXPROCS(0), data, func(i int64) { 13 | atomic.AddInt64(&sum, i) 14 | }) 15 | if atomic.LoadInt64(&sum) != sk { 16 | t.Fatalf("unexpected") 17 | } 18 | } 19 | 20 | func TestParallelVals(t *testing.T) { 21 | var sum int64 22 | data, _, sv := gen(int64(runtime.NumCPU() * 1000)) 23 | ParallelVals(runtime.GOMAXPROCS(0), data, func(i int64) { 24 | atomic.AddInt64(&sum, i) 25 | }) 26 | if atomic.LoadInt64(&sum) != sv { 27 | t.Fatalf("unexpected") 28 | } 29 | } 30 | 31 | func gen(count int64) (data map[int64]int64, sk, sv int64) { 32 | data = make(map[int64]int64, count) 33 | for i := int64(1); i <= count; i++ { 34 | sk += i 35 | sv += i * -1 36 | data[i] = i * -1 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /internal/util/parser.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | func ToFloat64(s string) (float64, error) { 9 | v, err := strconv.ParseFloat(s, 64) 10 | if err != nil && s == "-nan" { 11 | return math.NaN(), nil 12 | } 13 | return v, err 14 | } 15 | 16 | func ToFloat32(s string) (float32, error) { 17 | v, err := strconv.ParseFloat(s, 32) 18 | if err != nil && s == "-nan" { 19 | return float32(math.NaN()), nil 20 | } 21 | return float32(v), err 22 | } 23 | -------------------------------------------------------------------------------- /internal/util/parser_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestToFloat64(t *testing.T) { 9 | for _, c := range []struct { 10 | Str string 11 | Val float64 12 | }{ 13 | {Str: "1", Val: 1}, 14 | {Str: "-1", Val: -1}, 15 | {Str: "nan", Val: math.NaN()}, 16 | {Str: "-nan", Val: math.NaN()}, 17 | } { 18 | if v, _ := ToFloat64(c.Str); v != c.Val && !(math.IsNaN(v) && math.IsNaN(c.Val)) { 19 | t.Fail() 20 | } 21 | } 22 | } 23 | 24 | func TestToFloat32(t *testing.T) { 25 | for _, c := range []struct { 26 | Str string 27 | Val float32 28 | }{ 29 | {Str: "1", Val: 1}, 30 | {Str: "-1", Val: -1}, 31 | {Str: "nan", Val: float32(math.NaN())}, 32 | {Str: "-nan", Val: float32(math.NaN())}, 33 | } { 34 | if v, _ := ToFloat32(c.Str); v != c.Val && !(math.IsNaN(float64(v)) && math.IsNaN(float64(c.Val))) { 35 | t.Fail() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/util/pool.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Container interface { 8 | Capacity() int 9 | ResetLen(n int) 10 | } 11 | 12 | func NewPool[T Container](fn func(capacity int) T) *Pool[T] { 13 | return &Pool[T]{fn: fn} 14 | } 15 | 16 | type Pool[T Container] struct { 17 | sp sync.Pool 18 | fn func(capacity int) T 19 | } 20 | 21 | func (p *Pool[T]) Get(length, capacity int) T { 22 | s, ok := p.sp.Get().(T) 23 | if !ok { 24 | s = p.fn(capacity) 25 | } else if s.Capacity() < capacity { 26 | p.sp.Put(s) 27 | s = p.fn(capacity) 28 | } 29 | s.ResetLen(length) 30 | return s 31 | } 32 | 33 | func (p *Pool[T]) Put(s T) { 34 | s.ResetLen(0) 35 | p.sp.Put(s) 36 | } 37 | -------------------------------------------------------------------------------- /internal/util/pool_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | type container struct { 6 | s []int 7 | } 8 | 9 | func (c *container) Capacity() int { 10 | return cap(c.s) 11 | } 12 | 13 | func (c *container) ResetLen(n int) { 14 | c.s = c.s[:n] 15 | } 16 | 17 | func TestPool(t *testing.T) { 18 | p := NewPool(func(capacity int) *container { 19 | return &container{s: make([]int, 0, capacity)} 20 | }) 21 | c := p.Get(5, 10) 22 | if len(c.s) != 5 || cap(c.s) != 10 { 23 | t.Fatal("wrong length or capacity") 24 | } 25 | c.s[0] = 1 26 | p.Put(c) 27 | for { 28 | c = p.Get(5, 10) 29 | if c.s[0] == 1 { 30 | break 31 | } 32 | c.s[0] = 1 33 | p.Put(c) 34 | } 35 | c = p.Get(5, 20) 36 | if c.s[0] != 0 { 37 | t.Fatal("should not use recycled") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/util/rand.1.22.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/binary" 5 | "math/rand/v2" 6 | ) 7 | 8 | func Shuffle(n int, swap func(i, j int)) { 9 | rand.Shuffle(n, swap) 10 | } 11 | 12 | func FastRand(n int) int { 13 | return rand.IntN(n) 14 | } 15 | 16 | func RandomBytes() []byte { 17 | val := make([]byte, 24) 18 | binary.BigEndian.PutUint64(val[0:8], rand.Uint64()) 19 | binary.BigEndian.PutUint64(val[8:16], rand.Uint64()) 20 | binary.BigEndian.PutUint64(val[16:24], rand.Uint64()) 21 | return val 22 | } 23 | -------------------------------------------------------------------------------- /internal/util/rand_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestShuffle(t *testing.T) { 8 | arr := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 9 | Shuffle(len(arr), func(i, j int) { 10 | arr[i], arr[j] = arr[j], arr[i] 11 | }) 12 | if len(arr) != 10 { 13 | t.Errorf("Expected array length 10, got %d", len(arr)) 14 | } 15 | } 16 | 17 | func TestFastRand(t *testing.T) { 18 | n := 10 19 | res := FastRand(n) 20 | if res < 0 || res >= n { 21 | t.Errorf("Expected result between 0 and %d, got %d", n-1, res) 22 | } 23 | } 24 | 25 | func TestRandomBytes(t *testing.T) { 26 | val := RandomBytes() 27 | if len(val) != 24 { 28 | t.Errorf("Expected length 24, got %d", len(val)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mock/README.md: -------------------------------------------------------------------------------- 1 | # rueidis mock 2 | 3 | Due to the design of the command builder, it is impossible for users to mock `rueidis.Client` for testing. 4 | 5 | Therefore, rueidis provides an implemented one, based on the `gomock`, with some helpers 6 | to make user writing tests more easily, including command matcher `mock.Match`, `mock.MatchFn` and `mock.Result` for faking redis responses. 7 | 8 | ## Examples 9 | 10 | ### Mock `client.Do` 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "context" 17 | "testing" 18 | 19 | "go.uber.org/mock/gomock" 20 | "github.com/redis/rueidis/mock" 21 | ) 22 | 23 | func TestWithRueidis(t *testing.T) { 24 | ctrl := gomock.NewController(t) 25 | defer ctrl.Finish() 26 | 27 | ctx := context.Background() 28 | client := mock.NewClient(ctrl) 29 | 30 | client.EXPECT().Do(ctx, mock.Match("GET", "key")).Return(mock.Result(mock.RedisString("val"))) 31 | if v, _ := client.Do(ctx, client.B().Get().Key("key").Build()).ToString(); v != "val" { 32 | t.Fatalf("unexpected val %v", v) 33 | } 34 | client.EXPECT().DoMulti(ctx, mock.Match("GET", "c"), mock.Match("GET", "d")).Return([]rueidis.RedisResult{ 35 | mock.Result(mock.RedisNil()), 36 | mock.Result(mock.RedisNil()), 37 | }) 38 | for _, resp := range client.DoMulti(ctx, client.B().Get().Key("c").Build(), client.B().Get().Key("d").Build()) { 39 | if err := resp.Error(); !rueidis.IsRedisNil(err) { 40 | t.Fatalf("unexpected err %v", err) 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | ### Mock `client.Receive` 47 | 48 | ```go 49 | package main 50 | 51 | import ( 52 | "context" 53 | "testing" 54 | 55 | "go.uber.org/mock/gomock" 56 | "github.com/redis/rueidis/mock" 57 | ) 58 | 59 | func TestWithRueidisReceive(t *testing.T) { 60 | ctrl := gomock.NewController(t) 61 | defer ctrl.Finish() 62 | 63 | ctx := context.Background() 64 | client := mock.NewClient(ctrl) 65 | 66 | client.EXPECT().Receive(ctx, mock.Match("SUBSCRIBE", "ch"), gomock.Any()).Do(func(_, _ any, fn func(message rueidis.PubSubMessage)) { 67 | fn(rueidis.PubSubMessage{Message: "msg"}) 68 | }) 69 | 70 | client.Receive(ctx, client.B().Subscribe().Channel("ch").Build(), func(msg rueidis.PubSubMessage) { 71 | if msg.Message != "msg" { 72 | t.Fatalf("unexpected val %v", msg.Message) 73 | } 74 | }) 75 | } 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /mock/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/mock 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace github.com/redis/rueidis => ../ 8 | 9 | require ( 10 | github.com/redis/rueidis v1.0.61 11 | go.uber.org/mock v0.5.0 12 | ) 13 | 14 | require golang.org/x/sys v0.31.0 // indirect 15 | -------------------------------------------------------------------------------- /mock/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 4 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 5 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 6 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 7 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 8 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 9 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 10 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 11 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 12 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /mock/match.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/redis/rueidis" 8 | "go.uber.org/mock/gomock" 9 | ) 10 | 11 | func Match(cmd ...string) gomock.Matcher { 12 | return gomock.GotFormatterAdapter( 13 | gomock.GotFormatterFunc(func(i any) string { 14 | return format(i) 15 | }), 16 | &cmdMatcher{expect: cmd}, 17 | ) 18 | } 19 | 20 | type cmdMatcher struct { 21 | expect []string 22 | } 23 | 24 | func (c *cmdMatcher) Matches(x any) bool { 25 | return gomock.Eq(commands(x)).Matches(c.expect) 26 | } 27 | 28 | func (c *cmdMatcher) String() string { 29 | return fmt.Sprintf("redis command %v", c.expect) 30 | } 31 | 32 | func MatchFn(fn func(cmd []string) bool, description ...string) gomock.Matcher { 33 | return gomock.GotFormatterAdapter( 34 | gomock.GotFormatterFunc(func(i any) string { 35 | return format(i) 36 | }), 37 | &fnMatcher{matcher: fn, description: description}, 38 | ) 39 | } 40 | 41 | type fnMatcher struct { 42 | matcher func(cmd []string) bool 43 | description []string 44 | } 45 | 46 | func (c *fnMatcher) Matches(x any) bool { 47 | if cmd, ok := commands(x).([]string); ok { 48 | return c.matcher(cmd) 49 | } 50 | return false 51 | } 52 | 53 | func (c *fnMatcher) String() string { 54 | return fmt.Sprintf("matches %v", strings.Join(c.description, " ")) 55 | } 56 | 57 | func format(v any) string { 58 | if _, ok := v.([]any); !ok { 59 | v = []any{v} 60 | } 61 | sb := &strings.Builder{} 62 | sb.WriteString("\n") 63 | for i, c := range v.([]any) { 64 | fmt.Fprintf(sb, "index %d redis command %v\n", i+1, commands(c)) 65 | } 66 | return sb.String() 67 | } 68 | 69 | func commands(x any) any { 70 | if cmd, ok := x.(rueidis.Completed); ok { 71 | return cmd.Commands() 72 | } 73 | if cmd, ok := x.(rueidis.Cacheable); ok { 74 | return cmd.Commands() 75 | } 76 | if cmd, ok := x.(rueidis.CacheableTTL); ok { 77 | return cmd.Cmd.Commands() 78 | } 79 | return x 80 | } 81 | -------------------------------------------------------------------------------- /mock/match_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/redis/rueidis" 9 | "github.com/redis/rueidis/internal/cmds" 10 | "go.uber.org/mock/gomock" 11 | ) 12 | 13 | func TestMatch_Completed(t *testing.T) { 14 | cmd := cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Build() 15 | if m := Match("GET", "k"); !m.Matches(cmd) { 16 | t.Fatalf("not matched %s", m.String()) 17 | } 18 | } 19 | 20 | func TestMatchFn_Completed(t *testing.T) { 21 | cmd := cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Build() 22 | if m := MatchFn(func(cmd []string) bool { 23 | return reflect.DeepEqual(cmd, []string{"GET", "k"}) 24 | }, "GET k"); !m.Matches(cmd) { 25 | t.Fatalf("not matched %s", m.String()) 26 | } 27 | } 28 | 29 | func TestMatch_Cacheable(t *testing.T) { 30 | cmd := cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Cache() 31 | if m := Match("GET", "k"); !m.Matches(cmd) { 32 | t.Fatalf("not matched %s", m.String()) 33 | } 34 | } 35 | 36 | func TestMatchFn_Cacheable(t *testing.T) { 37 | cmd := cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Cache() 38 | if m := MatchFn(func(cmd []string) bool { 39 | return reflect.DeepEqual(cmd, []string{"GET", "k"}) 40 | }, "GET k"); !m.Matches(cmd) { 41 | t.Fatalf("not matched %s", m.String()) 42 | } 43 | } 44 | 45 | func TestMatch_CacheableTTL(t *testing.T) { 46 | cmd := cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Cache() 47 | if m := Match("GET", "k"); !m.Matches(rueidis.CacheableTTL{Cmd: cmd}) { 48 | t.Fatalf("not matched %s", m.String()) 49 | } 50 | } 51 | 52 | func TestMatchFn_CacheableTTL(t *testing.T) { 53 | cmd := cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Cache() 54 | if m := MatchFn(func(cmd []string) bool { 55 | return reflect.DeepEqual(cmd, []string{"GET", "k"}) 56 | }, "GET k"); !m.Matches(cmd) { 57 | t.Fatalf("not matched %s", m.String()) 58 | } 59 | } 60 | 61 | func TestMatch_Other(t *testing.T) { 62 | if m := Match("GET", "k"); m.Matches(1) { 63 | t.Fatalf("unexpected matched %s", m.String()) 64 | } 65 | if m := Match("GET", "k"); m.Matches([]rueidis.Completed{ 66 | cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Build(), // https://github.com/redis/rueidis/issues/120 67 | }) { 68 | t.Fatalf("unexpected matched %s", m.String()) 69 | } 70 | } 71 | 72 | func TestMatchFn_Other(t *testing.T) { 73 | if m := MatchFn(func(cmd []string) bool { 74 | return reflect.DeepEqual(cmd, []string{"GET", "k"}) 75 | }, "GET k"); m.Matches(1) { 76 | t.Fatalf("unexpected matched %s", m.String()) 77 | } 78 | if m := MatchFn(func(cmd []string) bool { 79 | return reflect.DeepEqual(cmd, []string{"GET", "k"}) 80 | }, "GET k"); m.Matches([]rueidis.Completed{ 81 | cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Build(), // https://github.com/redis/rueidis/issues/120 82 | }) { 83 | t.Fatalf("unexpected matched %s", m.String()) 84 | } 85 | } 86 | 87 | func TestMatch_Format(t *testing.T) { 88 | matcher := Match("GET", "t") 89 | if !strings.Contains(matcher.String(), "GET t") { 90 | t.Fatalf("unexpected format %v", matcher.String()) 91 | } 92 | if !strings.Contains(matcher.(gomock.GotFormatter).Got(cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Build()), "GET k") { 93 | t.Fatalf("unexpected format %v", matcher.String()) 94 | } 95 | } 96 | 97 | func TestMatchFn_Format(t *testing.T) { 98 | matcher := MatchFn(func(cmd []string) bool { 99 | return reflect.DeepEqual(cmd, []string{"GET", "t"}) 100 | }, "GET t") 101 | if !strings.Contains(matcher.String(), "GET t") { 102 | t.Fatalf("unexpected format %v", matcher.String()) 103 | } 104 | if !strings.Contains(matcher.(gomock.GotFormatter).Got(cmds.NewBuilder(cmds.NoSlot).Get().Key("k").Build()), "GET k") { 105 | t.Fatalf("unexpected format %v", matcher.String()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /om/README.md: -------------------------------------------------------------------------------- 1 | # Generic Object Mapping 2 | 3 | The `om.NewHashRepository` and `om.NewJSONRepository` creates an OM repository backed by redis hash or RedisJSON. 4 | 5 | ```golang 6 | package main 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "time" 12 | 13 | "github.com/redis/rueidis" 14 | "github.com/redis/rueidis/om" 15 | ) 16 | 17 | type Example struct { 18 | Key string `json:"key" redis:",key"` // the redis:",key" is required to indicate which field is the ULID key 19 | Ver int64 `json:"ver" redis:",ver"` // the redis:",ver" is required to do optimistic locking to prevent lost update 20 | ExAt time.Time `json:"exat" redis:",exat"` // the redis:",exat" is optional for setting record expiry with unix timestamp 21 | Str string `json:"str"` // both NewHashRepository and NewJSONRepository use json tag as field name 22 | } 23 | 24 | func main() { 25 | ctx := context.Background() 26 | c, err := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | // create the repo with NewHashRepository or NewJSONRepository 31 | repo := om.NewHashRepository("my_prefix", Example{}, c) 32 | 33 | exp := repo.NewEntity() 34 | exp.Str = "mystr" 35 | exp.ExAt = time.Now().Add(time.Hour) 36 | fmt.Println(exp.Key) // output 01FNH4FCXV9JTB9WTVFAAKGSYB 37 | repo.Save(ctx, exp) // success 38 | 39 | // lookup "my_prefix:01FNH4FCXV9JTB9WTVFAAKGSYB" through client side caching 40 | exp2, _ := repo.FetchCache(ctx, exp.Key, time.Second*5) 41 | fmt.Println(exp2.Str) // output "mystr", which equals to exp.Str 42 | 43 | exp2.Ver = 0 // if someone changes the version during your GET then SET operation, 44 | repo.Save(ctx, exp2) // the save will fail with ErrVersionMismatch. 45 | } 46 | 47 | ``` 48 | 49 | ### Object Mapping + RediSearch 50 | 51 | If you have RediSearch, you can create and search the repository against the index. 52 | 53 | ```golang 54 | if _, ok := repo.(*om.HashRepository[Example]); ok { 55 | repo.CreateIndex(ctx, func(schema om.FtCreateSchema) rueidis.Completed { 56 | return schema.FieldName("str").Tag().Build() // Note that the Example.Str field is mapped to str on redis by its json tag 57 | }) 58 | } 59 | 60 | if _, ok := repo.(*om.JSONRepository[Example]); ok { 61 | repo.CreateIndex(ctx, func(schema om.FtCreateSchema) rueidis.Completed { 62 | return schema.FieldName("$.str").As("str").Tag().Build() // the FieldName of a JSON index should be a JSON path syntax 63 | }) 64 | } 65 | 66 | exp := repo.NewEntity() 67 | exp.Str = "special_chars:[$.-]" 68 | repo.Save(ctx, exp) 69 | 70 | n, records, _ := repo.Search(ctx, func(search om.FtSearchIndex) rueidis.Completed { 71 | // Note that by using the named parameters with DIALECT >= 2, you won't have to escape chars for building queries. 72 | return search.Query("@str:{$v}").Params().Nargs(2).NameValue().NameValue("v", exp.Str).Dialect(2).Build() 73 | }) 74 | 75 | fmt.Println("total", n) // n is the total number of results matched in redis, which is >= len(records) 76 | 77 | for _, v := range records { 78 | fmt.Println(v.Str) // print "special_chars:[$.-]" 79 | } 80 | ``` 81 | 82 | ### Change Search Index Name 83 | 84 | The default index name for `HashRepository` and `JSONRepository` is `hashidx:{prefix}` and `jsonidx:{prefix}` respectively. 85 | 86 | They can be changed by `WithIndexName` option to allow searching difference indexes: 87 | 88 | ```golang 89 | repo1 := om.NewHashRepository("my_prefix", Example{}, c, om.WithIndexName("my_index1")) 90 | repo2 := om.NewHashRepository("my_prefix", Example{}, c, om.WithIndexName("my_index2")) 91 | ``` 92 | 93 | ### Object Expiry Timestamp 94 | 95 | Setting a `redis:",exat"` tag on a `time.Time` field will set `PEXPIREAT` on the record accordingly when calling `.Save()`. 96 | 97 | If the `time.Time` is zero, then the expiry will be untouched when calling `.Save()`. 98 | 99 | ### Object Mapping Limitation 100 | 101 | `NewHashRepository` only accepts these field types: 102 | * `string`, `*string` 103 | * `int64`, `*int64` 104 | * `bool`, `*bool` 105 | * `[]byte`, `json.RawMessage` 106 | * `[]float32`, `[]float64` for vector search 107 | * `json.Marshaler+json.Unmarshaler` 108 | 109 | Field projection by RediSearch is not supported. -------------------------------------------------------------------------------- /om/cursor.go: -------------------------------------------------------------------------------- 1 | package om 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/redis/rueidis" 8 | ) 9 | 10 | var EndOfCursor = errors.New("end of cursor") 11 | 12 | func newAggregateCursor(idx string, client rueidis.Client, first []map[string]string, cursor, total int64) *AggregateCursor { 13 | return &AggregateCursor{client: client, idx: idx, first: first, id: cursor, n: total} 14 | } 15 | 16 | // AggregateCursor unifies the response of FT.AGGREGATE with or without WITHCURSOR 17 | type AggregateCursor struct { 18 | client rueidis.Client 19 | idx string 20 | first []map[string]string 21 | id int64 22 | n int64 23 | } 24 | 25 | // Total return the total numbers of records of the initial FT.AGGREGATE result 26 | func (c *AggregateCursor) Total() int64 { 27 | return c.n 28 | } 29 | 30 | // Read return the partial result from the initial FT.AGGREGATE 31 | // This may invoke FT.CURSOR READ to retrieve a further result 32 | func (c *AggregateCursor) Read(ctx context.Context) (partial []map[string]string, err error) { 33 | if first := c.first; first != nil { 34 | c.first = nil 35 | return first, nil 36 | } 37 | if c.id == 0 { 38 | return nil, EndOfCursor 39 | } 40 | c.id, _, partial, err = c.client.Do(ctx, c.client.B().FtCursorRead().Index(c.idx).CursorId(c.id).Build()).AsFtAggregateCursor() 41 | return 42 | } 43 | 44 | // Del uses FT.CURSOR DEL to destroy the cursor 45 | func (c *AggregateCursor) Del(ctx context.Context) (err error) { 46 | if c.id == 0 { 47 | return nil 48 | } 49 | return c.client.Do(ctx, c.client.B().FtCursorDel().Index(c.idx).CursorId(c.id).Build()).Error() 50 | } 51 | -------------------------------------------------------------------------------- /om/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/om 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace github.com/redis/rueidis => ../ 8 | 9 | require ( 10 | github.com/oklog/ulid/v2 v2.1.0 11 | github.com/redis/rueidis v1.0.61 12 | ) 13 | 14 | require golang.org/x/sys v0.31.0 // indirect 15 | -------------------------------------------------------------------------------- /om/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= 4 | github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= 5 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 6 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 7 | github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= 8 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 9 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 10 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 11 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 12 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 13 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 14 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 15 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /om/repo.go: -------------------------------------------------------------------------------- 1 | package om 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/redis/rueidis" 9 | "github.com/redis/rueidis/internal/cmds" 10 | ) 11 | 12 | type ( 13 | // FtCreateSchema is the FT.CREATE command builder 14 | FtCreateSchema = cmds.FtCreateSchema 15 | // FtSearchIndex is the FT.SEARCH command builder 16 | FtSearchIndex = cmds.FtSearchIndex 17 | // FtAggregateIndex is the FT.AGGREGATE command builder 18 | FtAggregateIndex = cmds.FtAggregateIndex 19 | // FtAlterSchema is the FT.ALTERINDEX command builder 20 | FtAlterIndex = cmds.FtAlterIndex 21 | // Arbitrary is an alias to cmds.Arbitrary. This allows the user to build an arbitrary command in Repository.CreateIndex 22 | Arbitrary = cmds.Arbitrary 23 | ) 24 | 25 | var ( 26 | // ErrVersionMismatch indicates that the optimistic update failed. That is someone else had already changed the entity. 27 | ErrVersionMismatch = errors.New("object version mismatched, please retry") 28 | // ErrEmptyHashRecord indicates the requested hash entity is not found. 29 | ErrEmptyHashRecord = errors.New("hash object not found") 30 | ) 31 | 32 | // IsRecordNotFound checks if the error is indicating the requested entity is not found. 33 | func IsRecordNotFound(err error) bool { 34 | return rueidis.IsRedisNil(err) || err == ErrEmptyHashRecord 35 | } 36 | 37 | // Repository is backed by HashRepository or JSONRepository 38 | type Repository[T any] interface { 39 | NewEntity() (entity *T) 40 | Fetch(ctx context.Context, id string) (*T, error) 41 | FetchCache(ctx context.Context, id string, ttl time.Duration) (v *T, err error) 42 | Search(ctx context.Context, cmdFn func(search FtSearchIndex) rueidis.Completed) (int64, []*T, error) 43 | Aggregate(ctx context.Context, cmdFn func(agg FtAggregateIndex) rueidis.Completed) (*AggregateCursor, error) 44 | Save(ctx context.Context, entity *T) (err error) 45 | SaveMulti(ctx context.Context, entity ...*T) (errs []error) 46 | Remove(ctx context.Context, id string) error 47 | CreateIndex(ctx context.Context, cmdFn func(schema FtCreateSchema) rueidis.Completed) error 48 | AlterIndex(ctx context.Context, cmdFn func(alter FtAlterIndex) rueidis.Completed) error 49 | DropIndex(ctx context.Context) error 50 | IndexName() string 51 | } 52 | 53 | type RepositoryOption func(Repository[any]) 54 | 55 | func WithIndexName(name string) RepositoryOption { 56 | return func(r Repository[any]) { 57 | switch repo := r.(type) { 58 | case *HashRepository[any]: 59 | repo.idx = name 60 | case *JSONRepository[any]: 61 | repo.idx = name 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /om/repo_test.go: -------------------------------------------------------------------------------- 1 | package om 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/redis/rueidis" 7 | ) 8 | 9 | func setup(t *testing.T) rueidis.Client { 10 | client, err := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6377"}}) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | return client 15 | } 16 | 17 | type TestStruct struct { 18 | Key string `redis:",key"` 19 | Ver int64 `redis:",ver"` 20 | } 21 | 22 | func TestWithIndexName(t *testing.T) { 23 | client := setup(t) 24 | defer client.Close() 25 | 26 | for _, repo := range []Repository[TestStruct]{ 27 | NewHashRepository("custom_prefix", TestStruct{}, client, WithIndexName("custom_index")), 28 | NewJSONRepository("custom_prefix", TestStruct{}, client, WithIndexName("custom_index")), 29 | } { 30 | if repo.IndexName() != "custom_index" { 31 | t.Fail() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /om/schema.go: -------------------------------------------------------------------------------- 1 | package om 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const ignoreField = "-" 11 | 12 | type schema struct { 13 | key *field 14 | ver *field 15 | ext *field 16 | fields map[string]*field 17 | } 18 | 19 | type field struct { 20 | typ reflect.Type 21 | name string 22 | idx int 23 | isKey bool 24 | isVer bool 25 | isExt bool 26 | } 27 | 28 | func newSchema(t reflect.Type) schema { 29 | if t.Kind() != reflect.Struct { 30 | panic(fmt.Sprintf("schema %q should be a struct", t)) 31 | } 32 | 33 | s := schema{fields: make(map[string]*field, t.NumField())} 34 | 35 | for i := 0; i < t.NumField(); i++ { 36 | sf := t.Field(i) 37 | if !sf.IsExported() { 38 | continue 39 | } 40 | f := parse(sf) 41 | if f.name == ignoreField { 42 | continue 43 | } 44 | f.idx = i 45 | s.fields[f.name] = &f 46 | 47 | if f.isKey { 48 | if sf.Type.Kind() != reflect.String { 49 | panic(fmt.Sprintf("field with tag `redis:\",key\"` in schema %q should be a string", t)) 50 | } 51 | s.key = &f 52 | } 53 | if f.isVer { 54 | if sf.Type.Kind() != reflect.Int64 { 55 | panic(fmt.Sprintf("field with tag `redis:\",ver\"` in schema %q should be a int64", t)) 56 | } 57 | s.ver = &f 58 | } 59 | if f.isExt { 60 | if sf.Type != reflect.TypeOf(time.Time{}) { 61 | panic(fmt.Sprintf("field with tag `redis:\",exat\"` in schema %q should be a time.Time", t)) 62 | } 63 | s.ext = &f 64 | } 65 | } 66 | 67 | if s.key == nil { 68 | panic(fmt.Sprintf("schema %q should have one field with `redis:\",key\"` tag", t)) 69 | } 70 | if s.ver == nil { 71 | panic(fmt.Sprintf("schema %q should have one field with `redis:\",ver\"` tag", t)) 72 | } 73 | 74 | return s 75 | } 76 | 77 | func parse(f reflect.StructField) (field field) { 78 | v, _ := f.Tag.Lookup("json") 79 | vs := strings.SplitN(v, ",", 1) 80 | if vs[0] == "" { 81 | field.name = f.Name 82 | } else { 83 | field.name = vs[0] 84 | } 85 | 86 | v, _ = f.Tag.Lookup("redis") 87 | field.isKey = strings.Contains(v, ",key") 88 | field.isVer = strings.Contains(v, ",ver") 89 | field.isExt = strings.Contains(v, ",exat") 90 | field.typ = f.Type 91 | return field 92 | } 93 | 94 | func key(prefix, id string) (key string) { 95 | sb := strings.Builder{} 96 | sb.Grow(len(prefix) + len(id) + 1) 97 | sb.WriteString(prefix) 98 | sb.WriteString(":") 99 | sb.WriteString(id) 100 | return sb.String() 101 | } 102 | -------------------------------------------------------------------------------- /om/schema_test.go: -------------------------------------------------------------------------------- 1 | package om 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | type s1 struct { 10 | A int `redis:",key"` 11 | } 12 | 13 | type s2 struct { 14 | A string `redis:",ver"` 15 | } 16 | 17 | type s3 struct { 18 | A string `json:"-" redis:",key"` 19 | B int64 `redis:",ver"` 20 | private int64 21 | } 22 | 23 | type s4 struct { 24 | A string `redis:",key"` 25 | B int64 `json:"-" redis:",ver"` 26 | private int64 27 | } 28 | 29 | type s5 struct { 30 | A string `redis:",key"` 31 | B int64 `redis:",ver"` 32 | C int64 `redis:",exat"` 33 | } 34 | 35 | func TestSchema(t *testing.T) { 36 | t.Run("non struct", func(t *testing.T) { 37 | if v := recovered(func() { 38 | newSchema(reflect.TypeOf(map[string]string{})) 39 | }); !strings.Contains(v, "should be a struct") { 40 | t.Fatalf("unexpected msg %v", v) 41 | } 42 | }) 43 | t.Run("non string `redis:\",key\"`", func(t *testing.T) { 44 | if v := recovered(func() { 45 | newSchema(reflect.TypeOf(s1{})) 46 | }); !strings.Contains(v, "should be a string") { 47 | t.Fatalf("unexpected msg %v", v) 48 | } 49 | }) 50 | t.Run("non string `redis:\",ver\"`", func(t *testing.T) { 51 | if v := recovered(func() { 52 | newSchema(reflect.TypeOf(s2{})) 53 | }); !strings.Contains(v, "should be a int64") { 54 | t.Fatalf("unexpected msg %v", v) 55 | } 56 | }) 57 | t.Run("missing `redis:\",key\"`", func(t *testing.T) { 58 | if v := recovered(func() { 59 | newSchema(reflect.TypeOf(s3{})) 60 | }); !strings.Contains(v, "should have one field with `redis:\",key\"` tag") { 61 | t.Fatalf("unexpected msg %v", v) 62 | } 63 | }) 64 | t.Run("missing `redis:\",ver\"`", func(t *testing.T) { 65 | if v := recovered(func() { 66 | newSchema(reflect.TypeOf(s4{})) 67 | }); !strings.Contains(v, "should have one field with `redis:\",ver\"` tag") { 68 | t.Fatalf("unexpected msg %v", v) 69 | } 70 | }) 71 | t.Run("non time.Time `redis:\",exat\"`", func(t *testing.T) { 72 | if v := recovered(func() { 73 | newSchema(reflect.TypeOf(s5{})) 74 | }); !strings.Contains(v, "should be a time.Time") { 75 | t.Fatalf("unexpected msg %v", v) 76 | } 77 | }) 78 | } 79 | 80 | func recovered(fn func()) (msg string) { 81 | defer func() { 82 | msg = recover().(string) 83 | }() 84 | fn() 85 | return msg 86 | } 87 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // errAcquireComplete is a special error used to indicate that the Acquire operation has completed successfully 11 | var errAcquireComplete = errors.New("acquire complete") 12 | 13 | func newPool(cap int, dead wire, cleanup time.Duration, minSize int, makeFn func(context.Context) wire) *pool { 14 | if cap <= 0 { 15 | cap = DefaultPoolSize 16 | } 17 | 18 | return &pool{ 19 | size: 0, 20 | minSize: minSize, 21 | cap: cap, 22 | dead: dead, 23 | make: makeFn, 24 | list: make([]wire, 0, 4), 25 | cond: sync.NewCond(&sync.Mutex{}), 26 | cleanup: cleanup, 27 | } 28 | } 29 | 30 | type pool struct { 31 | dead wire 32 | cond *sync.Cond 33 | timer *time.Timer 34 | make func(ctx context.Context) wire 35 | list []wire 36 | cleanup time.Duration 37 | size int 38 | minSize int 39 | cap int 40 | down bool 41 | timerOn bool 42 | } 43 | 44 | func (p *pool) Acquire(ctx context.Context) (v wire) { 45 | p.cond.L.Lock() 46 | 47 | // Set up ctx handling when waiting for an available connection 48 | if len(p.list) == 0 && p.size == p.cap && !p.down && ctx.Err() == nil && ctx.Done() != nil { 49 | poolCtx, cancel := context.WithCancelCause(ctx) 50 | defer cancel(errAcquireComplete) 51 | 52 | go func() { 53 | <-poolCtx.Done() 54 | if context.Cause(poolCtx) != errAcquireComplete { // no need to broadcast if the poolCtx is cancelled explicitly. 55 | p.cond.Broadcast() 56 | } 57 | }() 58 | } 59 | 60 | retry: 61 | for len(p.list) == 0 && p.size == p.cap && !p.down && ctx.Err() == nil { 62 | p.cond.Wait() 63 | } 64 | 65 | if ctx.Err() != nil { 66 | deadPipe := deadFn() 67 | deadPipe.error.Store(&errs{error: ctx.Err()}) 68 | v = deadPipe 69 | p.cond.L.Unlock() 70 | return v 71 | } 72 | 73 | if p.down { 74 | v = p.dead 75 | p.cond.L.Unlock() 76 | return v 77 | } 78 | if len(p.list) == 0 { 79 | p.size++ 80 | // unlock before start to make a new wire 81 | // allowing others to make wires concurrently instead of waiting in line 82 | p.cond.L.Unlock() 83 | v = p.make(ctx) 84 | v.StopTimer() 85 | return v 86 | } 87 | 88 | i := len(p.list) - 1 89 | v = p.list[i] 90 | p.list[i] = nil 91 | p.list = p.list[:i] 92 | if !v.StopTimer() || v.Error() != nil { 93 | p.size-- 94 | v.Close() 95 | goto retry 96 | } 97 | p.cond.L.Unlock() 98 | return v 99 | } 100 | 101 | func (p *pool) Store(v wire) { 102 | p.cond.L.Lock() 103 | if !p.down && v.Error() == nil { 104 | p.list = append(p.list, v) 105 | p.startTimerIfNeeded() 106 | v.ResetTimer() 107 | } else { 108 | p.size-- 109 | v.Close() 110 | } 111 | p.cond.L.Unlock() 112 | p.cond.Signal() 113 | } 114 | 115 | func (p *pool) Close() { 116 | p.cond.L.Lock() 117 | p.down = true 118 | p.stopTimer() 119 | for _, w := range p.list { 120 | w.Close() 121 | } 122 | p.cond.L.Unlock() 123 | p.cond.Broadcast() 124 | } 125 | 126 | func (p *pool) startTimerIfNeeded() { 127 | if p.cleanup == 0 || p.timerOn || len(p.list) <= p.minSize { 128 | return 129 | } 130 | 131 | p.timerOn = true 132 | if p.timer == nil { 133 | p.timer = time.AfterFunc(p.cleanup, p.removeIdleConns) 134 | } else { 135 | p.timer.Reset(p.cleanup) 136 | } 137 | } 138 | 139 | func (p *pool) removeIdleConns() { 140 | p.cond.L.Lock() 141 | defer p.cond.L.Unlock() 142 | 143 | newLen := min(p.minSize, len(p.list)) 144 | for i, w := range p.list[newLen:] { 145 | w.Close() 146 | p.list[newLen+i] = nil 147 | p.size-- 148 | } 149 | 150 | p.list = p.list[:newLen] 151 | p.timerOn = false 152 | } 153 | 154 | func (p *pool) stopTimer() { 155 | p.timerOn = false 156 | if p.timer != nil { 157 | p.timer.Stop() 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /pubsub.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // PubSubMessage represents a pubsub message from redis 9 | type PubSubMessage struct { 10 | // Pattern is only available with pmessage. 11 | Pattern string 12 | // Channel is the channel the message belongs to 13 | Channel string 14 | // Message is the message content 15 | Message string 16 | } 17 | 18 | // PubSubSubscription represent a pubsub "subscribe", "unsubscribe", "ssubscribe", "sunsubscribe", "psubscribe" or "punsubscribe" event. 19 | type PubSubSubscription struct { 20 | // Kind is "subscribe", "unsubscribe", "ssubscribe", "sunsubscribe", "psubscribe" or "punsubscribe" 21 | Kind string 22 | // Channel is the event subject. 23 | Channel string 24 | // Count is the current number of subscriptions for a connection. 25 | Count int64 26 | } 27 | 28 | // PubSubHooks can be registered into DedicatedClient to process pubsub messages without using Client.Receive 29 | type PubSubHooks struct { 30 | // OnMessage will be called when receiving "message" and "pmessage" event. 31 | OnMessage func(m PubSubMessage) 32 | // OnSubscription will be called when receiving "subscribe", "unsubscribe", "psubscribe" and "punsubscribe" event. 33 | OnSubscription func(s PubSubSubscription) 34 | } 35 | 36 | func (h *PubSubHooks) isZero() bool { 37 | return h.OnMessage == nil && h.OnSubscription == nil 38 | } 39 | 40 | func newSubs() *subs { 41 | return &subs{chs: make(map[string]chs), sub: make(map[uint64]*sub)} 42 | } 43 | 44 | type subs struct { 45 | chs map[string]chs 46 | sub map[uint64]*sub 47 | cnt uint64 48 | mu sync.RWMutex 49 | } 50 | 51 | type chs struct { 52 | sub map[uint64]*sub 53 | } 54 | 55 | type sub struct { 56 | ch chan PubSubMessage 57 | fn func(PubSubSubscription) 58 | cs []string 59 | } 60 | 61 | func (s *subs) Publish(channel string, msg PubSubMessage) { 62 | if atomic.LoadUint64(&s.cnt) != 0 { 63 | s.mu.RLock() 64 | for _, sb := range s.chs[channel].sub { 65 | sb.ch <- msg 66 | } 67 | s.mu.RUnlock() 68 | } 69 | } 70 | 71 | func (s *subs) Subscribe(channels []string, fn func(PubSubSubscription)) (ch chan PubSubMessage, cancel func()) { 72 | id := atomic.AddUint64(&s.cnt, 1) 73 | s.mu.Lock() 74 | if s.chs != nil { 75 | ch = make(chan PubSubMessage, 16) 76 | sb := &sub{cs: channels, ch: ch, fn: fn} 77 | s.sub[id] = sb 78 | for _, channel := range channels { 79 | c := s.chs[channel].sub 80 | if c == nil { 81 | c = make(map[uint64]*sub, 1) 82 | s.chs[channel] = chs{sub: c} 83 | } 84 | c[id] = sb 85 | } 86 | cancel = func() { 87 | go func() { 88 | for range ch { 89 | } 90 | }() 91 | s.mu.Lock() 92 | if s.chs != nil { 93 | s.remove(id) 94 | } 95 | s.mu.Unlock() 96 | } 97 | } 98 | s.mu.Unlock() 99 | return ch, cancel 100 | } 101 | 102 | func (s *subs) remove(id uint64) { 103 | if sb := s.sub[id]; sb != nil { 104 | for _, channel := range sb.cs { 105 | if c := s.chs[channel].sub; c != nil { 106 | delete(c, id) 107 | } 108 | } 109 | close(sb.ch) 110 | delete(s.sub, id) 111 | } 112 | } 113 | 114 | func (s *subs) Confirm(sub PubSubSubscription) { 115 | if atomic.LoadUint64(&s.cnt) != 0 { 116 | s.mu.RLock() 117 | for _, sb := range s.chs[sub.Channel].sub { 118 | if sb.fn != nil { 119 | sb.fn(sub) 120 | } 121 | } 122 | s.mu.RUnlock() 123 | } 124 | } 125 | 126 | func (s *subs) Unsubscribe(sub PubSubSubscription) { 127 | if atomic.LoadUint64(&s.cnt) != 0 { 128 | s.mu.Lock() 129 | for id, sb := range s.chs[sub.Channel].sub { 130 | if sb.fn != nil { 131 | sb.fn(sub) 132 | } 133 | s.remove(id) 134 | } 135 | delete(s.chs, sub.Channel) 136 | s.mu.Unlock() 137 | } 138 | } 139 | 140 | func (s *subs) Close() { 141 | var sbs map[uint64]*sub 142 | s.mu.Lock() 143 | sbs = s.sub 144 | s.chs = nil 145 | s.sub = nil 146 | s.mu.Unlock() 147 | for _, sb := range sbs { 148 | close(sb.ch) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /pubsub_test.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestSubs_Publish(t *testing.T) { 9 | defer ShouldNotLeaked(SetupLeakDetection()) 10 | t.Run("without subs", func(t *testing.T) { 11 | s := newSubs() 12 | s.Publish("aa", PubSubMessage{}) // just no block 13 | }) 14 | 15 | t.Run("with multiple subs", func(t *testing.T) { 16 | s := newSubs() 17 | counts := map[string]int{ 18 | "a": 0, 19 | "b": 0, 20 | } 21 | subFn := func(s PubSubSubscription) { 22 | counts[s.Channel]++ 23 | } 24 | 25 | ch1, cancel1 := s.Subscribe([]string{"a"}, subFn) 26 | ch2, cancel2 := s.Subscribe([]string{"a"}, subFn) 27 | ch3, cancel3 := s.Subscribe([]string{"b"}, subFn) 28 | s.Confirm(PubSubSubscription{Channel: "a"}) 29 | s.Confirm(PubSubSubscription{Channel: "b"}) 30 | 31 | if counts["a"] != 2 || counts["b"] != 1 { 32 | t.Fatalf("unexpected counts %v", counts) 33 | } 34 | 35 | m1 := PubSubMessage{Pattern: "1", Channel: "2", Message: "3"} 36 | m2 := PubSubMessage{Pattern: "11", Channel: "22", Message: "33"} 37 | go func() { 38 | s.Publish("a", m1) 39 | s.Publish("b", m2) 40 | }() 41 | for m := range ch1 { 42 | if m != m1 { 43 | t.Fatalf("unexpected msg %v", m) 44 | } 45 | cancel1() 46 | } 47 | for m := range ch2 { 48 | if m != m1 { 49 | t.Fatalf("unexpected msg %v", m) 50 | } 51 | cancel2() 52 | } 53 | for m := range ch3 { 54 | if m != m2 { 55 | t.Fatalf("unexpected msg %v", m) 56 | } 57 | cancel3() 58 | } 59 | }) 60 | 61 | t.Run("drain ch", func(t *testing.T) { 62 | s := newSubs() 63 | ch, cancel := s.Subscribe([]string{"a"}, nil) 64 | s.Publish("a", PubSubMessage{}) 65 | if len(ch) != 1 { 66 | t.Fatalf("unexpected ch len %v", len(ch)) 67 | } 68 | cancel() 69 | for ; len(ch) != 0; time.Sleep(time.Millisecond * 100) { 70 | t.Log("wait ch to be drain") 71 | } 72 | }) 73 | } 74 | 75 | func TestSubs_Unsubscribe(t *testing.T) { 76 | defer ShouldNotLeaked(SetupLeakDetection()) 77 | s := newSubs() 78 | counts := map[string]int{"1": 0, "2": 0} 79 | subFn := func(s PubSubSubscription) { 80 | counts[s.Channel]++ 81 | } 82 | ch, _ := s.Subscribe([]string{"1", "2"}, subFn) 83 | go func() { 84 | s.Publish("1", PubSubMessage{}) 85 | }() 86 | _, ok := <-ch 87 | if !ok { 88 | t.Fatalf("unexpected ch closed") 89 | } 90 | s.Unsubscribe(PubSubSubscription{Channel: "1"}) 91 | if counts["1"] != 1 { 92 | t.Fatalf("unexpected counts %v", counts) 93 | } 94 | _, ok = <-ch 95 | if ok { 96 | t.Fatalf("unexpected ch unclosed") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "time" 7 | 8 | "github.com/redis/rueidis/internal/util" 9 | ) 10 | 11 | const ( 12 | defaultMaxRetries = 20 13 | defaultMaxRetryDelay = 1 * time.Second 14 | ) 15 | 16 | // RetryDelayFn returns the delay that should be used before retrying the 17 | // attempt. Will return a negative delay if the delay could not be determined or does not retry. 18 | type RetryDelayFn func(attempts int, cmd Completed, err error) time.Duration 19 | 20 | // defaultRetryDelayFn delays the next retry exponentially without considering the error. 21 | // Max delay is 1 second. 22 | // This "Equal Jitter" delay produced by this implementation is not monotonic increasing. ref: https://aws.amazon.com/ko/blogs/architecture/exponential-backoff-and-jitter/ 23 | func defaultRetryDelayFn(attempts int, _ Completed, _ error) time.Duration { 24 | base := 1 << min(defaultMaxRetries, attempts) 25 | jitter := util.FastRand(base) 26 | return min(defaultMaxRetryDelay, time.Duration(base+jitter)*time.Microsecond) 27 | } 28 | 29 | type retryHandler interface { 30 | // RetryDelay returns the delay that should be used before retrying the 31 | // attempt. Will return a negative delay if the delay could not be determined or does 32 | // not retry. 33 | // If the delay is zero, the next retry should be attempted immediately. 34 | RetryDelay(attempts int, cmd Completed, err error) time.Duration 35 | 36 | // WaitForRetry waits until the next retry should be attempted. 37 | WaitForRetry(ctx context.Context, duration time.Duration) 38 | 39 | // WaitOrSkipRetry waits until the next retry should be attempted 40 | // or returns false if the command should not be retried. 41 | // Returns false immediately if the command should not be retried. 42 | // Returns true after the delay if the command should be retried. 43 | WaitOrSkipRetry(ctx context.Context, attempts int, cmd Completed, err error) bool 44 | } 45 | 46 | type retryer struct { 47 | RetryDelayFn RetryDelayFn 48 | } 49 | 50 | var _ retryHandler = (*retryer)(nil) 51 | 52 | func newRetryer(retryDelayFn RetryDelayFn) *retryer { 53 | return &retryer{RetryDelayFn: retryDelayFn} 54 | } 55 | 56 | func (r *retryer) RetryDelay(attempts int, cmd Completed, err error) time.Duration { 57 | return r.RetryDelayFn(attempts, cmd, err) 58 | } 59 | 60 | func (r *retryer) WaitForRetry(ctx context.Context, duration time.Duration) { 61 | if duration > 0 { 62 | if ch := ctx.Done(); ch != nil { 63 | tm := time.NewTimer(duration) 64 | defer tm.Stop() 65 | select { 66 | case <-ch: 67 | case <-tm.C: 68 | } 69 | } else { 70 | time.Sleep(duration) 71 | } 72 | } 73 | } 74 | 75 | func (r *retryer) WaitOrSkipRetry( 76 | ctx context.Context, attempts int, cmd Completed, err error, 77 | ) bool { 78 | if delay := r.RetryDelay(attempts, cmd, err); delay == 0 { 79 | runtime.Gosched() 80 | return true 81 | } else if delay > 0 { 82 | if dl, ok := ctx.Deadline(); !ok || time.Until(dl) > delay { 83 | r.WaitForRetry(ctx, delay) 84 | return true 85 | } 86 | } 87 | return false 88 | } 89 | -------------------------------------------------------------------------------- /ring.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | 7 | "golang.org/x/sys/cpu" 8 | ) 9 | 10 | type queue interface { 11 | PutOne(m Completed) chan RedisResult 12 | PutMulti(m []Completed, resps []RedisResult) chan RedisResult 13 | NextWriteCmd() (Completed, []Completed, chan RedisResult) 14 | WaitForWrite() (Completed, []Completed, chan RedisResult) 15 | NextResultCh() (Completed, []Completed, chan RedisResult, []RedisResult, *sync.Cond) 16 | } 17 | 18 | var _ queue = (*ring)(nil) 19 | 20 | func newRing(factor int) *ring { 21 | if factor <= 0 { 22 | factor = DefaultRingScale 23 | } 24 | r := &ring{store: make([]node, 2<<(factor-1))} 25 | r.mask = uint32(len(r.store) - 1) 26 | for i := range r.store { 27 | m := &sync.Mutex{} 28 | r.store[i].c1 = sync.NewCond(m) 29 | r.store[i].c2 = sync.NewCond(m) 30 | r.store[i].ch = make(chan RedisResult) // this channel can't be buffered 31 | } 32 | return r 33 | } 34 | 35 | type ring struct { 36 | store []node // store's size must be 2^N to work with the mask 37 | _ cpu.CacheLinePad 38 | write uint32 39 | _ cpu.CacheLinePad 40 | read1 uint32 41 | read2 uint32 42 | mask uint32 43 | } 44 | 45 | type node struct { 46 | c1 *sync.Cond 47 | c2 *sync.Cond 48 | ch chan RedisResult 49 | one Completed 50 | multi []Completed 51 | resps []RedisResult 52 | mark uint32 53 | slept bool 54 | } 55 | 56 | func (r *ring) PutOne(m Completed) chan RedisResult { 57 | n := &r.store[atomic.AddUint32(&r.write, 1)&r.mask] 58 | n.c1.L.Lock() 59 | for n.mark != 0 { 60 | n.c1.Wait() 61 | } 62 | n.one = m 63 | n.mark = 1 64 | s := n.slept 65 | n.c1.L.Unlock() 66 | if s { 67 | n.c2.Broadcast() 68 | } 69 | return n.ch 70 | } 71 | 72 | func (r *ring) PutMulti(m []Completed, resps []RedisResult) chan RedisResult { 73 | n := &r.store[atomic.AddUint32(&r.write, 1)&r.mask] 74 | n.c1.L.Lock() 75 | for n.mark != 0 { 76 | n.c1.Wait() 77 | } 78 | n.multi = m 79 | n.resps = resps 80 | n.mark = 1 81 | s := n.slept 82 | n.c1.L.Unlock() 83 | if s { 84 | n.c2.Broadcast() 85 | } 86 | return n.ch 87 | } 88 | 89 | // NextWriteCmd should be only called by one dedicated thread 90 | func (r *ring) NextWriteCmd() (one Completed, multi []Completed, ch chan RedisResult) { 91 | r.read1++ 92 | p := r.read1 & r.mask 93 | n := &r.store[p] 94 | n.c1.L.Lock() 95 | if n.mark == 1 { 96 | one, multi, ch = n.one, n.multi, n.ch 97 | n.mark = 2 98 | } else { 99 | r.read1-- 100 | } 101 | n.c1.L.Unlock() 102 | return 103 | } 104 | 105 | // WaitForWrite should be only called by one dedicated thread 106 | func (r *ring) WaitForWrite() (one Completed, multi []Completed, ch chan RedisResult) { 107 | r.read1++ 108 | p := r.read1 & r.mask 109 | n := &r.store[p] 110 | n.c1.L.Lock() 111 | for n.mark != 1 { 112 | n.slept = true 113 | n.c2.Wait() // c1 and c2 share the same mutex 114 | n.slept = false 115 | } 116 | one, multi, ch = n.one, n.multi, n.ch 117 | n.mark = 2 118 | n.c1.L.Unlock() 119 | return 120 | } 121 | 122 | // NextResultCh should be only called by one dedicated thread 123 | func (r *ring) NextResultCh() (one Completed, multi []Completed, ch chan RedisResult, resps []RedisResult, cond *sync.Cond) { 124 | r.read2++ 125 | p := r.read2 & r.mask 126 | n := &r.store[p] 127 | cond = n.c1 128 | n.c1.L.Lock() 129 | if n.mark == 2 { 130 | one, multi, ch, resps = n.one, n.multi, n.ch, n.resps 131 | n.mark = 0 132 | n.one = Completed{} 133 | n.multi = nil 134 | n.resps = nil 135 | } else { 136 | r.read2-- 137 | } 138 | return 139 | } 140 | -------------------------------------------------------------------------------- /ring_test.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "runtime" 5 | "strconv" 6 | "testing" 7 | "time" 8 | 9 | "github.com/redis/rueidis/internal/cmds" 10 | ) 11 | 12 | //gocyclo:ignore 13 | func TestRing(t *testing.T) { 14 | defer ShouldNotLeaked(SetupLeakDetection()) 15 | t.Run("PutOne", func(t *testing.T) { 16 | ring := newRing(DefaultRingScale) 17 | size := 5000 18 | fixture := make(map[string]struct{}, size) 19 | for i := 0; i < size; i++ { 20 | fixture[strconv.Itoa(i)] = struct{}{} 21 | } 22 | 23 | for cmd := range fixture { 24 | go ring.PutOne(cmds.NewCompleted([]string{cmd})) 25 | } 26 | 27 | for len(fixture) != 0 { 28 | cmd1, _, _ := ring.NextWriteCmd() 29 | if cmd1.IsEmpty() { 30 | runtime.Gosched() 31 | continue 32 | } 33 | cmd2, _, ch, _, cond := ring.NextResultCh() 34 | cond.L.Unlock() 35 | cond.Signal() 36 | if cmd1.Commands()[0] != cmd2.Commands()[0] { 37 | t.Fatalf("cmds read by NextWriteCmd and NextResultCh is not the same one") 38 | } 39 | if ch == nil || len(ch) != 0 { 40 | t.Fatalf("channel from NextResultCh is broken") 41 | } 42 | delete(fixture, cmd1.Commands()[0]) 43 | } 44 | }) 45 | 46 | t.Run("PutMulti", func(t *testing.T) { 47 | ring := newRing(DefaultRingScale) 48 | size := 5000 49 | fixture := make(map[string]struct{}, size) 50 | for i := 0; i < size; i++ { 51 | fixture[strconv.Itoa(i)] = struct{}{} 52 | } 53 | 54 | base := [][]string{{"a"}, {"b"}, {"c"}, {"d"}} 55 | for cmd := range fixture { 56 | go ring.PutMulti(cmds.NewMultiCompleted(append([][]string{{cmd}}, base...)), nil) 57 | } 58 | 59 | for len(fixture) != 0 { 60 | _, cmd1, _ := ring.NextWriteCmd() 61 | if cmd1 == nil { 62 | runtime.Gosched() 63 | continue 64 | } 65 | _, cmd2, ch, _, cond := ring.NextResultCh() 66 | cond.L.Unlock() 67 | cond.Signal() 68 | for j := 0; j < len(cmd1); j++ { 69 | if cmd1[j].Commands()[0] != cmd2[j].Commands()[0] { 70 | t.Fatalf("cmds read by NextWriteCmd and NextResultCh is not the same one") 71 | } 72 | } 73 | if ch == nil || len(ch) != 0 { 74 | t.Fatalf("channel from NextResultCh is broken") 75 | } 76 | delete(fixture, cmd1[0].Commands()[0]) 77 | } 78 | }) 79 | 80 | t.Run("NextWriteCmd & NextResultCh", func(t *testing.T) { 81 | ring := newRing(DefaultRingScale) 82 | if one, multi, _ := ring.NextWriteCmd(); !one.IsEmpty() || multi != nil { 83 | t.Fatalf("NextWriteCmd should returns nil if empty") 84 | } 85 | if one, multi, ch, _, cond := ring.NextResultCh(); !one.IsEmpty() || multi != nil || ch != nil { 86 | t.Fatalf("NextResultCh should returns nil if not NextWriteCmd yet") 87 | } else { 88 | cond.L.Unlock() 89 | cond.Signal() 90 | } 91 | 92 | ring.PutOne(cmds.NewCompleted([]string{"0"})) 93 | if one, _, _ := ring.NextWriteCmd(); len(one.Commands()) == 0 || one.Commands()[0] != "0" { 94 | t.Fatalf("NextWriteCmd should returns next cmd") 95 | } 96 | if one, _, ch, _, cond := ring.NextResultCh(); len(one.Commands()) == 0 || one.Commands()[0] != "0" || ch == nil { 97 | t.Fatalf("NextResultCh should returns next cmd after NextWriteCmd") 98 | } else { 99 | cond.L.Unlock() 100 | cond.Signal() 101 | } 102 | 103 | ring.PutMulti(cmds.NewMultiCompleted([][]string{{"0"}}), nil) 104 | if _, multi, _ := ring.NextWriteCmd(); len(multi) == 0 || multi[0].Commands()[0] != "0" { 105 | t.Fatalf("NextWriteCmd should returns next cmd") 106 | } 107 | if _, multi, ch, _, cond := ring.NextResultCh(); len(multi) == 0 || multi[0].Commands()[0] != "0" || ch == nil { 108 | t.Fatalf("NextResultCh should returns next cmd after NextWriteCmd") 109 | } else { 110 | cond.L.Unlock() 111 | cond.Signal() 112 | } 113 | }) 114 | 115 | t.Run("PutOne Wakeup WaitForWrite", func(t *testing.T) { 116 | ring := newRing(DefaultRingScale) 117 | if one, _, ch := ring.NextWriteCmd(); ch == nil { 118 | go func() { 119 | time.Sleep(time.Millisecond * 100) 120 | ring.PutOne(cmds.PingCmd) 121 | }() 122 | if one, _, ch = ring.WaitForWrite(); ch != nil && one.Commands()[0] == cmds.PingCmd.Commands()[0] { 123 | return 124 | } 125 | } 126 | t.Fatal("Should sleep") 127 | }) 128 | 129 | t.Run("PutMulti Wakeup WaitForWrite", func(t *testing.T) { 130 | ring := newRing(DefaultRingScale) 131 | if _, _, ch := ring.NextWriteCmd(); ch == nil { 132 | go func() { 133 | time.Sleep(time.Millisecond * 100) 134 | ring.PutMulti([]Completed{cmds.PingCmd}, nil) 135 | }() 136 | if _, multi, ch := ring.WaitForWrite(); ch != nil && multi[0].Commands()[0] == cmds.PingCmd.Commands()[0] { 137 | return 138 | } 139 | } 140 | t.Fatal("Should sleep") 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /rueidisaside/README.md: -------------------------------------------------------------------------------- 1 | # rueidisaside 2 | 3 | A Cache-Aside pattern implementation enhanced by [Client Side Caching](https://redis.io/docs/manual/client-side-caching/). 4 | 5 | ## Features backed by the Redis Client Side Caching 6 | 7 | Cache-Aside is a widely used pattern to cache other data sources into Redis. However, there are many issues to be considered when implementing it. 8 | 9 | For example, an implementation without locking or versioning may cause a fresh cache to be overridden by a stale one. 10 | And if using a locking mechanism, how to get notified when a lock is released? If using a versioning mechanism, how to version an empty value? 11 | 12 | Thankfully, the above issues can be addressed better with the client-side caching along with the following additional benefits: 13 | 14 | * Avoiding unnecessary network round trips. Redis will proactively invalidate the client-side cache. 15 | * Avoiding Cache Stampede by locking keys with the client-side caching, the same technique used in [rueidislock](https://github.com/redis/rueidis/tree/main/rueidislock). Only the first cache missed call can update the cache, and others will wait for notifications. 16 | 17 | ## Example 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "context" 24 | "database/sql" 25 | "time" 26 | 27 | "github.com/redis/rueidis" 28 | "github.com/redis/rueidis/rueidisaside" 29 | ) 30 | 31 | func main() { 32 | var db sql.DB 33 | client, err := rueidisaside.NewClient(rueidisaside.ClientOption{ 34 | ClientOption: rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}, 35 | }) 36 | if err != nil { 37 | panic(err) 38 | } 39 | val, err := client.Get(context.Background(), time.Minute, "mykey", func(ctx context.Context, key string) (val string, err error) { 40 | if err = db.QueryRowContext(ctx, "SELECT val FROM mytab WHERE id = ?", key).Scan(&val); err == sql.ErrNoRows { 41 | val = "_nil_" // cache nil to avoid penetration. 42 | err = nil // clear err in the case of sql.ErrNoRows. 43 | } 44 | return 45 | }) 46 | if err != nil { 47 | panic(err) 48 | } else if val == "_nil_" { 49 | val = "" 50 | err = sql.ErrNoRows 51 | } else { 52 | // ... 53 | } 54 | } 55 | ``` 56 | 57 | If you want to use cache typed value, not string, you can use `rueidisaside.TypedCacheAsideClient`. 58 | 59 | ```go 60 | package main 61 | 62 | import ( 63 | "context" 64 | "database/sql" 65 | "encoding/json" 66 | "time" 67 | 68 | "github.com/redis/rueidis" 69 | "github.com/redis/rueidis/rueidisaside" 70 | ) 71 | 72 | type MyValue struct { 73 | Val string `json:"val"` 74 | } 75 | 76 | func main() { 77 | var db sql.DB 78 | client, err := rueidisaside.NewClient(rueidisaside.ClientOption{ 79 | ClientOption: rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}, 80 | }) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | serializer := func(val *MyValue) (string, error) { 86 | b, err := json.Marshal(val) 87 | return string(b), err 88 | } 89 | deserializer := func(s string) (*MyValue, error) { 90 | var val *MyValue 91 | if err := json.Unmarshal([]byte(s), &val); err != nil { 92 | return nil, err 93 | } 94 | return val, nil 95 | } 96 | 97 | typedClient := rueidisaside.NewTypedCacheAsideClient(client, serializer, deserializer) 98 | val, err := typedClient.Get(context.Background(), time.Minute, "myKey", func(ctx context.Context, key string) (*MyValue, error) { 99 | var val MyValue 100 | if err := db.QueryRowContext(ctx, "SELECT val FROM mytab WHERE id = ?", key).Scan(&val.Val); err == sql.ErrNoRows { 101 | return nil, nil 102 | } else if err != nil { 103 | return nil, err 104 | } 105 | return &val, nil 106 | }) 107 | // ... 108 | } 109 | ``` 110 | 111 | ## Limitation 112 | 113 | Currently, requires Redis >= 7.0. 114 | However, the `UseLuaLock` option is available and allows you to use the `rueidisaside` with older Redis versions < 7.0 as well. 115 | 116 | To configure the Lua fallback option: 117 | 118 | ```go 119 | client, err := rueidisaside.NewClient(rueidisaside.ClientOption{ 120 | ClientOption: rueidis.ClientOption{ 121 | InitAddress: []string{"127.0.0.1:6379"}, 122 | }, 123 | UseLuaLock: true, // Enable Lua script for older Redis versions 124 | }) 125 | if err != nil { 126 | panic(err) 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /rueidisaside/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/rueidisaside 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace github.com/redis/rueidis => ../ 8 | 9 | require github.com/redis/rueidis v1.0.61 10 | 11 | require golang.org/x/sys v0.31.0 // indirect 12 | -------------------------------------------------------------------------------- /rueidisaside/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 4 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 5 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 6 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 7 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 8 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 9 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 10 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /rueidisaside/typed_aside.go: -------------------------------------------------------------------------------- 1 | package rueidisaside 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // TypedCacheAsideClient is an interface that provides a typed cache-aside client. 9 | // It allows you to cache and retrieve values of a specific type T. 10 | type TypedCacheAsideClient[T any] interface { 11 | Get(ctx context.Context, ttl time.Duration, key string, fn func(ctx context.Context, key string) (val *T, err error)) (val *T, err error) 12 | Del(ctx context.Context, key string) error 13 | Client() CacheAsideClient 14 | } 15 | 16 | // typedCacheAsideClient is an implementation of the TypedCacheAsideClient interface. 17 | // It provides a typed cache-aside client that allows caching and retrieving values of a specific type T. 18 | type typedCacheAsideClient[T any] struct { 19 | client CacheAsideClient 20 | serializer func(*T) (string, error) 21 | deserializer func(string) (*T, error) 22 | } 23 | 24 | // NewTypedCacheAsideClient creates a new TypedCacheAsideClient instance that provides a typed cache-aside client. 25 | // The client, serializer, and deserializer functions are used to interact with the underlying cache. 26 | // The serializer function is used to convert the provided value of type T to a string, and the deserializer function 27 | // is used to convert the cached string value back to the original type T. 28 | func NewTypedCacheAsideClient[T any]( 29 | client CacheAsideClient, 30 | serializer func(*T) (string, error), 31 | deserializer func(string) (*T, error), 32 | ) TypedCacheAsideClient[T] { 33 | return &typedCacheAsideClient[T]{ 34 | client: client, 35 | serializer: serializer, 36 | deserializer: deserializer, 37 | } 38 | } 39 | 40 | // Get retrieves a value of type T from the cache or fetches it using the provided 41 | // function and stores it in the cache. The value is cached for the specified TTL. 42 | // If the value cannot be retrieved or deserialized, an error is returned. 43 | func (c typedCacheAsideClient[T]) Get(ctx context.Context, ttl time.Duration, key string, fn func(ctx context.Context, key string) (val *T, err error)) (val *T, err error) { 44 | strVal, err := c.client.Get(ctx, ttl, key, func(ctx context.Context, key string) (val string, err error) { 45 | result, err := fn(ctx, key) 46 | if err != nil { 47 | return "", err 48 | } 49 | return c.serializer(result) 50 | }) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return c.deserializer(strVal) 55 | } 56 | 57 | // Del deletes the value associated with the given key from the cache. 58 | func (c typedCacheAsideClient[T]) Del(ctx context.Context, key string) error { 59 | return c.client.Del(ctx, key) 60 | } 61 | 62 | // Client returns the underlying CacheAsideClient instance used by the TypedCacheAsideClient. 63 | func (c typedCacheAsideClient[T]) Client() CacheAsideClient { 64 | return c.client 65 | } 66 | -------------------------------------------------------------------------------- /rueidiscompat/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/rueidiscompat 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace github.com/redis/rueidis => ../ 8 | 9 | replace github.com/redis/rueidis/mock => ../mock 10 | 11 | require ( 12 | github.com/onsi/ginkgo/v2 v2.22.2 13 | github.com/onsi/gomega v1.36.2 14 | github.com/redis/rueidis v1.0.61 15 | github.com/redis/rueidis/mock v1.0.61 16 | go.uber.org/mock v0.5.0 17 | ) 18 | 19 | require ( 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 22 | github.com/google/go-cmp v0.7.0 // indirect 23 | github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect 24 | golang.org/x/net v0.38.0 // indirect 25 | golang.org/x/sys v0.31.0 // indirect 26 | golang.org/x/text v0.23.0 // indirect 27 | golang.org/x/tools v0.31.0 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /rueidiscompat/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 4 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 6 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 7 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 8 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 9 | github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= 10 | github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 11 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 12 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= 16 | github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= 17 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 18 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 22 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 24 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 25 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 26 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 27 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 28 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 29 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 30 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 31 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 32 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 33 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 34 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 37 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 39 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | -------------------------------------------------------------------------------- /rueidiscompat/script.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 The github.com/go-redis/redis Authors. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | package rueidiscompat 28 | 29 | import ( 30 | "context" 31 | "crypto/sha1" 32 | "encoding/hex" 33 | "io" 34 | "strings" 35 | ) 36 | 37 | type Scripter interface { 38 | Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd 39 | EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd 40 | EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd 41 | EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd 42 | ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd 43 | ScriptLoad(ctx context.Context, script string) *StringCmd 44 | } 45 | 46 | var ( 47 | _ Scripter = (*Compat)(nil) 48 | ) 49 | 50 | type Script struct { 51 | src, hash string 52 | } 53 | 54 | func NewScript(src string) *Script { 55 | h := sha1.New() 56 | _, _ = io.WriteString(h, src) 57 | return &Script{ 58 | src: src, 59 | hash: hex.EncodeToString(h.Sum(nil)), 60 | } 61 | } 62 | 63 | func (s *Script) Hash() string { 64 | return s.hash 65 | } 66 | 67 | func (s *Script) Load(ctx context.Context, c Scripter) *StringCmd { 68 | return c.ScriptLoad(ctx, s.src) 69 | } 70 | 71 | func (s *Script) Exists(ctx context.Context, c Scripter) *BoolSliceCmd { 72 | return c.ScriptExists(ctx, s.hash) 73 | } 74 | 75 | func (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 76 | return c.Eval(ctx, s.src, keys, args...) 77 | } 78 | 79 | func (s *Script) EvalRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 80 | return c.EvalRO(ctx, s.src, keys, args...) 81 | } 82 | 83 | func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 84 | return c.EvalSha(ctx, s.hash, keys, args...) 85 | } 86 | 87 | func (s *Script) EvalShaRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 88 | return c.EvalShaRO(ctx, s.hash, keys, args...) 89 | } 90 | 91 | // Run optimistically uses EVALSHA to run the script. If the script does not exist, 92 | // it is retried using EVAL. 93 | func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 94 | r := s.EvalSha(ctx, c, keys, args...) 95 | if err := r.Err(); err != nil { 96 | msg := err.Error() 97 | msg = strings.TrimPrefix(msg, "ERR ") 98 | if strings.HasPrefix(msg, "NOSCRIPT") { 99 | return s.Eval(ctx, c, keys, args...) 100 | } 101 | } 102 | return r 103 | } 104 | 105 | // RunRO optimistically uses EVALSHA_RO to run the script. If the script does not exist, 106 | // it is retried using EVAL_RO. 107 | func (s *Script) RunRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 108 | r := s.EvalShaRO(ctx, c, keys, args...) 109 | if err := r.Err(); err != nil { 110 | msg := err.Error() 111 | msg = strings.TrimPrefix(msg, "ERR ") 112 | if strings.HasPrefix(msg, "NOSCRIPT") { 113 | return s.EvalRO(ctx, c, keys, args...) 114 | } 115 | } 116 | return r 117 | } 118 | -------------------------------------------------------------------------------- /rueidiscompat/script_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 The github.com/go-redis/redis Authors. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | package rueidiscompat 28 | 29 | import ( 30 | "context" 31 | "fmt" 32 | 33 | "github.com/redis/rueidis" 34 | ) 35 | 36 | func ExampleScript() { 37 | ctx = context.Background() 38 | IncrByXX := NewScript(` 39 | if redis.call("GET", KEYS[1]) ~= false then 40 | return redis.call("INCRBY", KEYS[1], ARGV[1]) 41 | end 42 | return false 43 | `) 44 | client, err := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}) 45 | if err != nil { 46 | panic(err) 47 | } 48 | defer client.Close() 49 | rdb := NewAdapter(client) 50 | n, err := IncrByXX.Run(ctx, rdb, []string{"xx_counter"}, 2).Result() 51 | fmt.Println(n, err) 52 | 53 | err = rdb.Set(ctx, "xx_counter", "40", 0).Err() 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | n, err = IncrByXX.Run(ctx, rdb, []string{"xx_counter"}, 2).Result() 59 | fmt.Println(n, err) 60 | } 61 | -------------------------------------------------------------------------------- /rueidiscompat/tx.go: -------------------------------------------------------------------------------- 1 | package rueidiscompat 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | "unsafe" 8 | 9 | "github.com/redis/rueidis" 10 | ) 11 | 12 | var TxFailedErr = errors.New("redis: transaction failed") 13 | 14 | var _ Pipeliner = (*TxPipeline)(nil) 15 | 16 | type rePipeline = Pipeline 17 | 18 | func newTxPipeline(real rueidis.Client) *TxPipeline { 19 | return &TxPipeline{rePipeline: newPipeline(real)} 20 | } 21 | 22 | type TxPipeline struct { 23 | *rePipeline 24 | } 25 | 26 | func (c *TxPipeline) Exec(ctx context.Context) ([]Cmder, error) { 27 | p := c.comp.client.(*proxy) 28 | if len(p.cmds) == 0 { 29 | return nil, nil 30 | } 31 | 32 | rets := c.rets 33 | cmds := p.cmds 34 | c.rets = nil 35 | p.cmds = nil 36 | 37 | cmds = append(cmds, c.comp.client.B().Multi().Build(), c.comp.client.B().Exec().Build()) 38 | for i := len(cmds) - 2; i >= 1; i-- { 39 | j := i - 1 40 | cmds[j], cmds[i] = cmds[i], cmds[j] 41 | } 42 | 43 | resp := p.DoMulti(ctx, cmds...) 44 | results, err := resp[len(resp)-1].ToArray() 45 | if rueidis.IsRedisNil(err) { 46 | err = TxFailedErr 47 | } 48 | for i, r := range results { 49 | rets[i].from(*(*rueidis.RedisResult)(unsafe.Pointer(&proxyresult{ 50 | err: resp[i+1].NonRedisError(), 51 | val: r, 52 | }))) 53 | } 54 | return rets, err 55 | } 56 | 57 | func (c *TxPipeline) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 58 | if err := fn(c); err != nil { 59 | return nil, err 60 | } 61 | return c.Exec(ctx) 62 | } 63 | 64 | func (c *TxPipeline) Pipeline() Pipeliner { 65 | return c 66 | } 67 | 68 | func (c *TxPipeline) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 69 | return c.Pipelined(ctx, fn) 70 | } 71 | 72 | func (c *TxPipeline) TxPipeline() Pipeliner { 73 | return c 74 | } 75 | 76 | var _ rueidis.Client = (*txproxy)(nil) 77 | 78 | type txproxy struct { 79 | rueidis.CoreClient 80 | } 81 | 82 | func (p *txproxy) DoCache(_ context.Context, _ rueidis.Cacheable, _ time.Duration) (resp rueidis.RedisResult) { 83 | panic("not implemented") 84 | } 85 | 86 | func (p *txproxy) DoMultiCache(_ context.Context, _ ...rueidis.CacheableTTL) (resp []rueidis.RedisResult) { 87 | panic("not implemented") 88 | } 89 | 90 | func (p *txproxy) DoStream(_ context.Context, _ rueidis.Completed) rueidis.RedisResultStream { 91 | panic("not implemented") 92 | } 93 | 94 | func (p *txproxy) DoMultiStream(_ context.Context, _ ...rueidis.Completed) rueidis.MultiRedisResultStream { 95 | panic("not implemented") 96 | } 97 | 98 | func (p *txproxy) Dedicated(_ func(rueidis.DedicatedClient) error) (err error) { 99 | panic("not implemented") 100 | } 101 | 102 | func (p *txproxy) Dedicate() (client rueidis.DedicatedClient, cancel func()) { 103 | panic("not implemented") 104 | } 105 | 106 | func (p *txproxy) Nodes() map[string]rueidis.Client { 107 | panic("not implemented") 108 | } 109 | 110 | func (p *txproxy) Mode() rueidis.ClientMode { 111 | panic("not implemented") 112 | } 113 | 114 | type Tx interface { 115 | CoreCmdable 116 | Watch(ctx context.Context, keys ...string) *StatusCmd 117 | Unwatch(ctx context.Context, keys ...string) *StatusCmd 118 | Close(ctx context.Context) error 119 | } 120 | 121 | func newTx(client rueidis.DedicatedClient, cancel func()) *tx { 122 | return &tx{CoreCmdable: NewAdapter(&txproxy{CoreClient: client}), cancel: cancel} 123 | } 124 | 125 | type tx struct { 126 | CoreCmdable 127 | cancel func() 128 | } 129 | 130 | func (t *tx) Watch(ctx context.Context, keys ...string) *StatusCmd { 131 | ret := &StatusCmd{} 132 | if len(keys) != 0 { 133 | client := t.CoreCmdable.(*Compat).client 134 | ret.from(client.Do(ctx, client.B().Watch().Key(keys...).Build())) 135 | } 136 | return ret 137 | } 138 | 139 | func (t *tx) Unwatch(ctx context.Context, _ ...string) *StatusCmd { 140 | ret := &StatusCmd{} 141 | client := t.CoreCmdable.(*Compat).client 142 | ret.from(client.Do(ctx, client.B().Unwatch().Build())) 143 | return ret 144 | } 145 | 146 | func (t *tx) Close(_ context.Context) error { 147 | t.cancel() 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /rueidiscompat/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 The github.com/go-redis/redis Authors. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | package rueidiscompat 28 | 29 | import ( 30 | "net" 31 | "strconv" 32 | "strings" 33 | ) 34 | 35 | func ToLower(s string) string { 36 | if isLower(s) { 37 | return s 38 | } 39 | 40 | b := make([]byte, len(s)) 41 | for i := range b { 42 | c := s[i] 43 | if c >= 'A' && c <= 'Z' { 44 | c += 'a' - 'A' 45 | } 46 | b[i] = c 47 | } 48 | return BytesToString(b) 49 | } 50 | 51 | func BytesToString(b []byte) string { 52 | return string(b) 53 | } 54 | 55 | func StringToBytes(s string) []byte { 56 | return []byte(s) 57 | } 58 | 59 | func isLower(s string) bool { 60 | for i := 0; i < len(s); i++ { 61 | c := s[i] 62 | if c >= 'A' && c <= 'Z' { 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | 69 | func ReplaceSpaces(s string) string { 70 | // Pre-allocate a builder with the same length as s to minimize allocations. 71 | // This is a basic optimization; adjust the initial size based on your use case. 72 | var builder strings.Builder 73 | builder.Grow(len(s)) 74 | 75 | for _, char := range s { 76 | if char == ' ' { 77 | // Replace space with a hyphen. 78 | builder.WriteRune('-') 79 | } else { 80 | // Copy the character as-is. 81 | builder.WriteRune(char) 82 | } 83 | } 84 | 85 | return builder.String() 86 | } 87 | 88 | func GetAddr(addr string) string { 89 | ind := strings.LastIndexByte(addr, ':') 90 | if ind == -1 { 91 | return "" 92 | } 93 | 94 | if strings.IndexByte(addr, '.') != -1 { 95 | return addr 96 | } 97 | 98 | if addr[0] == '[' { 99 | return addr 100 | } 101 | return net.JoinHostPort(addr[:ind], addr[ind+1:]) 102 | } 103 | 104 | func ToInteger(val interface{}) int { 105 | switch v := val.(type) { 106 | case int: 107 | return v 108 | case int64: 109 | return int(v) 110 | case string: 111 | i, _ := strconv.Atoi(v) 112 | return i 113 | default: 114 | return 0 115 | } 116 | } 117 | 118 | func ToFloat(val interface{}) float64 { 119 | switch v := val.(type) { 120 | case float64: 121 | return v 122 | case string: 123 | f, _ := strconv.ParseFloat(v, 64) 124 | return f 125 | default: 126 | return 0.0 127 | } 128 | } 129 | 130 | func ToString(val interface{}) string { 131 | if str, ok := val.(string); ok { 132 | return str 133 | } 134 | return "" 135 | } 136 | 137 | func ToStringSlice(val interface{}) []string { 138 | if arr, ok := val.([]interface{}); ok { 139 | result := make([]string, len(arr)) 140 | for i, v := range arr { 141 | result[i] = ToString(v) 142 | } 143 | return result 144 | } 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /rueidiscompat/util_test.go: -------------------------------------------------------------------------------- 1 | package rueidiscompat 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | func TestToLower(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | exp string 13 | }{{"HELLO", "hello"}, {"Hello", "hello"}, {"123!@#AbC!@#", "123!@#abc!@#"}, {"", ""}} 14 | for _, test := range tests { 15 | res := ToLower(test.input) 16 | if res != test.exp { 17 | t.Errorf("ToLower(%q) = %q; want %q", test.input, res, test.exp) 18 | } 19 | } 20 | } 21 | 22 | func TestStringToBytes(t *testing.T) { 23 | exp := []byte("hello") 24 | input := "hello" 25 | res := StringToBytes(input) 26 | if string(res) != string(exp) { 27 | t.Errorf("StringToBytes(%q) = %q; want %q", input, res, exp) 28 | } 29 | } 30 | 31 | func TestReplaceSpaces(t *testing.T) { 32 | tests := []struct { 33 | input string 34 | exp string 35 | }{{"one space", "one-space"}, {"multiple spaces", "multiple---spaces"}, {"", ""}} 36 | for _, test := range tests { 37 | res := ReplaceSpaces(test.input) 38 | if res != test.exp { 39 | t.Errorf("ReplaceSpaces(%q)= %q; want %q", test.input, res, test.exp) 40 | } 41 | } 42 | } 43 | 44 | func TestGetAddr(t *testing.T) { 45 | tests := []struct { 46 | input string 47 | exp string 48 | }{{"192.168.1.1:8080", "192.168.1.1:8080"}, {"[2001:db8::1]:443", "[2001:db8::1]:443"}, {"localhost:3000", "localhost:3000"}, {"12345", ""}, {"", ""}} 49 | for _, test := range tests { 50 | res := GetAddr(test.input) 51 | if res != test.exp { 52 | t.Errorf("GetAddr(%q)= %q; want %q", test.input, res, test.exp) 53 | } 54 | } 55 | } 56 | 57 | func TestToInteger(t *testing.T) { 58 | tests := []struct { 59 | input interface{} 60 | exp int 61 | }{{123, 123}, {int64(123), 123}, {"123", 123}, {"abc", 0}, {nil, 0}, {float64(123), 0}} 62 | for _, test := range tests { 63 | res := ToInteger(test.input) 64 | if res != test.exp { 65 | t.Errorf("ToInteger(%v) = %d, want %d", test.input, res, test.exp) 66 | } 67 | } 68 | } 69 | 70 | func TestToFloat(t *testing.T) { 71 | tests := []struct { 72 | input interface{} 73 | exp float64 74 | }{ 75 | {123.45, 123.45}, 76 | {int64(123), 0.0}, 77 | {123, 0.0}, 78 | {"123.45", 123.45}, 79 | {"abc", 0}, 80 | {nil, 0}, 81 | } 82 | for _, test := range tests { 83 | res := ToFloat(test.input) 84 | if math.Abs(res-test.exp) > 0.001 { 85 | t.Errorf("Testing ToFloat(%v): got %.1f, expected %.1f", 86 | test.input, res, test.exp) 87 | } 88 | } 89 | } 90 | 91 | func TestToStringSlice(t *testing.T) { 92 | tests := []struct { 93 | input interface{} 94 | exp []string 95 | }{{[]interface{}{"abc", "def"}, []string{"abc", "def"}}, {[]interface{}{1, "abc", 2}, []string{"", "abc", ""}}, {[]interface{}{nil, "abc"}, []string{"", "abc"}}, {[]interface{}{1.2, true, 3.5}, []string{"", "", ""}}, {"abc", nil}} 96 | for _, test := range tests { 97 | res := ToStringSlice(test.input) 98 | if fmt.Sprintf("%v", res) != fmt.Sprintf("%v", test.exp) { 99 | t.Errorf("For %v, expected %v, but got %v", test.input, test.exp, res) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rueidishook/README.md: -------------------------------------------------------------------------------- 1 | # rueidishook 2 | 3 | With `rueidishook.WithHook`, users can easily intercept `rueidis.Client` by implementing custom `rueidishook.Hook` handler. 4 | 5 | This can be useful to change the behavior of `rueidis.Client` or add other integrations such as observability, APM, etc. 6 | 7 | ## Example 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "context" 14 | "time" 15 | 16 | "github.com/redis/rueidis" 17 | "github.com/redis/rueidis/rueidishook" 18 | ) 19 | 20 | type hook struct{} 21 | 22 | func (h *hook) Do(client rueidis.Client, ctx context.Context, cmd rueidis.Completed) (resp rueidis.RedisResult) { 23 | // do whatever you want before a client.Do 24 | resp = client.Do(ctx, cmd) 25 | // do whatever you want after a client.Do 26 | return 27 | } 28 | 29 | func (h *hook) DoMulti(client rueidis.Client, ctx context.Context, multi ...rueidis.Completed) (resps []rueidis.RedisResult) { 30 | // do whatever you want before a client.DoMulti 31 | resps = client.DoMulti(ctx, multi...) 32 | // do whatever you want after a client.DoMulti 33 | return 34 | } 35 | 36 | func (h *hook) DoCache(client rueidis.Client, ctx context.Context, cmd rueidis.Cacheable, ttl time.Duration) (resp rueidis.RedisResult) { 37 | // do whatever you want before a client.DoCache 38 | resp = client.DoCache(ctx, cmd, ttl) 39 | // do whatever you want after a client.DoCache 40 | return 41 | } 42 | 43 | func (h *hook) DoMultiCache(client rueidis.Client, ctx context.Context, multi ...rueidis.CacheableTTL) (resps []rueidis.RedisResult) { 44 | // do whatever you want before a client.DoMultiCache 45 | resps = client.DoMultiCache(ctx, multi...) 46 | // do whatever you want after a client.DoMultiCache 47 | return 48 | } 49 | 50 | func (h *hook) Receive(client rueidis.Client, ctx context.Context, subscribe rueidis.Completed, fn func(msg rueidis.PubSubMessage)) (err error) { 51 | // do whatever you want before a client.Receive 52 | err = client.Receive(ctx, subscribe, fn) 53 | // do whatever you want after a client.Receive 54 | return 55 | } 56 | 57 | func main() { 58 | client, err := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}) 59 | if err != nil { 60 | panic(err) 61 | } 62 | client = rueidishook.WithHook(client, &hook{}) 63 | defer client.Close() 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /rueidishook/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/rueidishook 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace ( 8 | github.com/redis/rueidis => ../ 9 | github.com/redis/rueidis/mock => ../mock 10 | ) 11 | 12 | require ( 13 | github.com/redis/rueidis v1.0.61 14 | github.com/redis/rueidis/mock v1.0.61 15 | go.uber.org/mock v0.5.0 16 | ) 17 | 18 | require golang.org/x/sys v0.31.0 // indirect 19 | -------------------------------------------------------------------------------- /rueidishook/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 4 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 5 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 6 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 7 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 8 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 9 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 10 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 11 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 12 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /rueidislimiter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/rueidislimiter 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace github.com/redis/rueidis => ../ 8 | 9 | replace github.com/redis/rueidis/mock => ../mock 10 | 11 | require ( 12 | github.com/redis/rueidis v1.0.61 13 | github.com/redis/rueidis/mock v1.0.61 14 | go.uber.org/mock v0.5.0 15 | ) 16 | 17 | require golang.org/x/sys v0.31.0 // indirect 18 | -------------------------------------------------------------------------------- /rueidislimiter/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 4 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 5 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 6 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 7 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 8 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 9 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 10 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 11 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 12 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /rueidislimiter/limit.go: -------------------------------------------------------------------------------- 1 | package rueidislimiter 2 | 3 | import "time" 4 | 5 | type RateLimitOption struct { 6 | limit int64 7 | window time.Duration 8 | } 9 | 10 | func WithCustomRateLimit(limit int, window time.Duration) RateLimitOption { 11 | return RateLimitOption{ 12 | limit: int64(limit), 13 | window: window, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rueidislimiter/syncp.go: -------------------------------------------------------------------------------- 1 | package rueidislimiter 2 | 3 | import "github.com/redis/rueidis/internal/util" 4 | 5 | var rateBuffersPool = util.NewPool(func(capacity int) *rateBuffersContainer { 6 | return &rateBuffersContainer{ 7 | keyBuf: make([]byte, 0, capacity), 8 | } 9 | }) 10 | 11 | type rateBuffersContainer struct { 12 | keyBuf []byte 13 | } 14 | 15 | func (r *rateBuffersContainer) Capacity() int { 16 | return cap(r.keyBuf) 17 | } 18 | 19 | func (r *rateBuffersContainer) ResetLen(n int) { 20 | r.keyBuf = r.keyBuf[:0] 21 | } 22 | -------------------------------------------------------------------------------- /rueidislock/README.md: -------------------------------------------------------------------------------- 1 | # rueidislock 2 | 3 | A [Redis Distributed Lock Pattern](https://redis.io/docs/latest/develop/use/patterns/distributed-locks/) enhanced by [Client Side Caching](https://redis.io/docs/manual/client-side-caching/). 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "context" 10 | "github.com/redis/rueidis" 11 | "github.com/redis/rueidis/rueidislock" 12 | ) 13 | 14 | func main() { 15 | locker, err := rueidislock.NewLocker(rueidislock.LockerOption{ 16 | ClientOption: rueidis.ClientOption{InitAddress: []string{"localhost:6379"}}, 17 | KeyMajority: 1, // Use KeyMajority=1 if you have only one Redis instance. Also make sure that all your `Locker`s share the same KeyMajority. 18 | NoLoopTracking: true, // Enable this to have better performance if all your Redis are >= 7.0.5. 19 | }) 20 | if err != nil { 21 | panic(err) 22 | } 23 | defer locker.Close() 24 | 25 | // acquire the lock "my_lock" 26 | ctx, cancel, err := locker.WithContext(context.Background(), "my_lock") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | // "my_lock" is acquired. use the ctx as normal. 32 | doSomething(ctx) 33 | 34 | // invoke cancel() to release the lock. 35 | cancel() 36 | } 37 | ``` 38 | 39 | ## Features backed by the Redis Client Side Caching 40 | * The returned `ctx` will be canceled automatically and immediately once the `KeyMajority` is not held anymore, for example: 41 | * Redis are down. 42 | * Acquired keys have been deleted by other programs or administrators. 43 | * The waiting `Locker.WithContext` will try acquiring the lock again automatically and immediately once it has been released by someone or by another program. 44 | 45 | ## How it works 46 | 47 | When the `locker.WithContext` is invoked, it will: 48 | 49 | 1. Try acquiring 3 keys (given that the default `KeyMajority` is 2), which are `rueidislock:0:my_lock`, `rueidislock:1:my_lock` and `rueidislock:2:my_lock`, by sending redis command `SET NX PXAT` or `SET NX PX` if `FallbackSETPX` is set. 50 | 2. If the `KeyMajority` is satisfied within the `KeyValidity` duration, the invocation is successful and a `ctx` is returned as the lock. 51 | 3. If the invocation is not successful, it will wait for client-side caching notifications to retry again. 52 | 4. If the invocation is successful, the `Locker` will extend the `ctx` validity periodically and also watch client-side caching notifications for canceling the `ctx` if the `KeyMajority` is not held anymore. 53 | 54 | ### Disable Client Side Caching 55 | 56 | Some Redis providers don't support client-side caching, ex. Google Cloud Memorystore. 57 | You can disable client-side caching by setting `ClientOption.DisableCache` to `true`. 58 | Please note that when the client-side caching is disabled, rueidislock will only try to re-acquire locks for every ExtendInterval. 59 | 60 | ## Benchmark 61 | 62 | ```bash 63 | ▶ go test -bench=. -benchmem -run=. 64 | goos: darwin 65 | goarch: arm64 66 | pkg: rueidis-benchmark/locker 67 | Benchmark/rueidislock-10 20103 57842 ns/op 1849 B/op 29 allocs/op 68 | Benchmark/redislock-10 13209 86285 ns/op 8083 B/op 225 allocs/op 69 | PASS 70 | ok rueidis-benchmark/locker 3.782s 71 | ``` 72 | 73 | ```go 74 | package locker 75 | 76 | import ( 77 | "context" 78 | "testing" 79 | "time" 80 | 81 | "github.com/bsm/redislock" 82 | "github.com/redis/go-redis/v9" 83 | "github.com/redis/rueidis" 84 | "github.com/redis/rueidis/rueidislock" 85 | ) 86 | 87 | func Benchmark(b *testing.B) { 88 | b.Run("rueidislock", func(b *testing.B) { 89 | l, _ := rueidislock.NewLocker(rueidislock.LockerOption{ 90 | ClientOption: rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}, 91 | KeyMajority: 1, 92 | NoLoopTracking: true, 93 | }) 94 | defer l.Close() 95 | b.ResetTimer() 96 | b.RunParallel(func(pb *testing.PB) { 97 | for pb.Next() { 98 | _, cancel, err := l.WithContext(context.Background(), "mylock") 99 | if err != nil { 100 | panic(err) 101 | } 102 | cancel() 103 | } 104 | }) 105 | b.StopTimer() 106 | }) 107 | b.Run("redislock", func(b *testing.B) { 108 | client := redis.NewUniversalClient(&redis.UniversalOptions{Addrs: []string{"127.0.0.1:6379"}}) 109 | locker := redislock.New(client) 110 | defer client.Close() 111 | b.ResetTimer() 112 | b.RunParallel(func(pb *testing.PB) { 113 | for pb.Next() { 114 | retry: 115 | lock, err := locker.Obtain(context.Background(), "mylock", time.Minute, nil) 116 | if err == redislock.ErrNotObtained { 117 | goto retry 118 | } else if err != nil { 119 | panic(err) 120 | } 121 | lock.Release(context.Background()) 122 | } 123 | }) 124 | b.StopTimer() 125 | }) 126 | } 127 | ``` 128 | -------------------------------------------------------------------------------- /rueidisotel/README.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry Tracing & Connection Metrics 2 | 3 | Use `rueidisotel.NewClient` to create a client with OpenTelemetry Tracing and Connection Metrics enabled. 4 | Builtin connection metrics are: 5 | - `rueidis_dial_attempt`: number of dial attempts 6 | - `rueidis_dial_success`: number of successful dials 7 | - `rueidis_dial_conns`: number of connections 8 | - `rueidis_dial_latency`: dial latency in seconds 9 | 10 | Client side caching metrics: 11 | - `rueidis_do_cache_miss`: number of cache miss on client side 12 | - `rueidis_do_cache_hits`: number of cache hits on client side 13 | 14 | Client side commmand metrics: 15 | - `rueidis_command_duration_seconds`: histogram of command duration 16 | - `rueidis_command_errors`: number of command errors 17 | 18 | ```golang 19 | package main 20 | 21 | import ( 22 | "github.com/redis/rueidis" 23 | "github.com/redis/rueidis/rueidisotel" 24 | ) 25 | 26 | func main() { 27 | client, err := rueidisotel.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}}) 28 | if err != nil { 29 | panic(err) 30 | } 31 | defer client.Close() 32 | } 33 | ``` 34 | 35 | See [rueidishook](../rueidishook) if you want more customizations. 36 | 37 | Note: `rueidisotel.NewClient` is not supported on go1.18 and go1.19 builds. [Reference](https://github.com/redis/rueidis/issues/442#issuecomment-1886993707) -------------------------------------------------------------------------------- /rueidisotel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/rueidisotel 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace github.com/redis/rueidis => ../ 8 | 9 | require ( 10 | github.com/redis/rueidis v1.0.61 11 | go.opentelemetry.io/otel v1.35.0 12 | go.opentelemetry.io/otel/metric v1.35.0 13 | go.opentelemetry.io/otel/sdk v1.35.0 14 | go.opentelemetry.io/otel/sdk/metric v1.35.0 15 | go.opentelemetry.io/otel/trace v1.35.0 16 | ) 17 | 18 | require ( 19 | github.com/go-logr/logr v1.4.2 // indirect 20 | github.com/go-logr/stdr v1.2.2 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 23 | golang.org/x/sys v0.31.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /rueidisotel/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 4 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 5 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 6 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 7 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 8 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 9 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 10 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 11 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 13 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 17 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 18 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 19 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 20 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 21 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 22 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 23 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 24 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 25 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 26 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 27 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 28 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 29 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 30 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 31 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 32 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 33 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 34 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 35 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 36 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 37 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 38 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 39 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | -------------------------------------------------------------------------------- /rueidisprob/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/rueidis/rueidisprob 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | replace github.com/redis/rueidis => ../ 8 | 9 | require ( 10 | github.com/redis/rueidis v1.0.61 11 | github.com/twmb/murmur3 v1.1.8 12 | ) 13 | 14 | require golang.org/x/sys v0.31.0 // indirect 15 | -------------------------------------------------------------------------------- /rueidisprob/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 4 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 5 | github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= 6 | github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= 7 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 8 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 9 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 10 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 11 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 12 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /rueidisprob/index.go: -------------------------------------------------------------------------------- 1 | package rueidisprob 2 | 3 | import "github.com/twmb/murmur3" 4 | 5 | func hash(data []byte) (uint64, uint64) { 6 | return murmur3.Sum128(data) 7 | } 8 | 9 | func index(h1, h2 uint64, i uint, maxSize uint64) uint64 { 10 | offset := h1 + uint64(i)*h2 11 | return offset % maxSize 12 | } 13 | -------------------------------------------------------------------------------- /rueidisprob/synp.go: -------------------------------------------------------------------------------- 1 | package rueidisprob 2 | 3 | import "github.com/redis/rueidis/internal/util" 4 | 5 | var bytesPool = util.NewPool(func(capacity int) *bytesContainer { 6 | return &bytesContainer{s: make([]byte, 0, capacity)} 7 | }) 8 | 9 | type bytesContainer struct { 10 | s []byte 11 | } 12 | 13 | func (r *bytesContainer) Capacity() int { 14 | return cap(r.s) 15 | } 16 | 17 | func (r *bytesContainer) ResetLen(n int) { 18 | clear(r.s) 19 | r.s = r.s[:n] 20 | } 21 | -------------------------------------------------------------------------------- /singleflight.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type call struct { 10 | ts time.Time 11 | ch chan struct{} 12 | cn int 13 | mu sync.Mutex 14 | } 15 | 16 | func (c *call) Do(ctx context.Context, fn func() error) error { 17 | c.mu.Lock() 18 | c.cn++ 19 | ch := c.ch 20 | if ch != nil { 21 | c.mu.Unlock() 22 | if ctxCh := ctx.Done(); ctxCh != nil { 23 | select { 24 | case <-ch: 25 | case <-ctxCh: 26 | return ctx.Err() 27 | } 28 | } else { 29 | <-ch 30 | } 31 | return nil 32 | } 33 | ch = make(chan struct{}) 34 | c.ch = ch 35 | c.mu.Unlock() 36 | return c.do(ch, fn) 37 | } 38 | 39 | func (c *call) LazyDo(threshold time.Duration, fn func() error) { 40 | c.mu.Lock() 41 | ch := c.ch 42 | if ch != nil { 43 | c.mu.Unlock() 44 | return 45 | } 46 | ch = make(chan struct{}) 47 | c.ch = ch 48 | c.cn++ 49 | ts := c.ts 50 | c.mu.Unlock() 51 | go func(ts time.Time, ch chan struct{}, fn func() error) { 52 | time.Sleep(time.Until(ts)) 53 | c.do(ch, fn) 54 | }(ts.Add(threshold), ch, fn) 55 | } 56 | 57 | func (c *call) do(ch chan struct{}, fn func() error) (err error) { 58 | err = fn() 59 | c.mu.Lock() 60 | c.ch = nil 61 | c.cn = 0 62 | c.ts = time.Now() 63 | c.mu.Unlock() 64 | close(ch) 65 | return 66 | } 67 | 68 | func (c *call) suppressing() int { 69 | c.mu.Lock() 70 | defer c.mu.Unlock() 71 | return c.cn 72 | } 73 | -------------------------------------------------------------------------------- /singleflight_test.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "runtime" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestSingleFlight(t *testing.T) { 13 | defer ShouldNotLeaked(SetupLeakDetection()) 14 | var calls, done, err int64 15 | 16 | sg := call{} 17 | 18 | for i := 0; i < 1000; i++ { 19 | go func() { 20 | if ret := sg.Do(context.Background(), func() error { 21 | atomic.AddInt64(&calls, 1) 22 | // wait for all goroutine invoked then return 23 | for sg.suppressing() != 1000 { 24 | runtime.Gosched() 25 | } 26 | return errors.New("I should be the only ret") 27 | }); ret != nil { 28 | atomic.AddInt64(&err, 1) 29 | } 30 | 31 | atomic.AddInt64(&done, 1) 32 | }() 33 | } 34 | 35 | for atomic.LoadInt64(&done) != 1000 { 36 | runtime.Gosched() 37 | } 38 | 39 | if atomic.LoadInt64(&calls) == 0 { 40 | t.Fatalf("singleflight not call at all") 41 | } 42 | 43 | if v := atomic.LoadInt64(&calls); v != 1 { 44 | t.Fatalf("singleflight should suppress all concurrent calls, got: %v", v) 45 | } 46 | 47 | if atomic.LoadInt64(&err) != 1 { 48 | t.Fatalf("singleflight should that one call get the return value") 49 | } 50 | } 51 | 52 | func TestSingleFlightWithContext(t *testing.T) { 53 | defer ShouldNotLeaked(SetupLeakDetection()) 54 | ch := make(chan struct{}) 55 | sg := call{} 56 | go func() { 57 | sg.Do(context.Background(), func() error { 58 | <-ch 59 | return nil 60 | }) 61 | }() 62 | for sg.suppressing() != 1 { 63 | time.Sleep(time.Millisecond) 64 | } 65 | ctx, cancel := context.WithCancel(context.Background()) 66 | cancel() 67 | if err := sg.Do(ctx, func() error { return nil }); err != context.Canceled { 68 | t.Fatalf("unexpected err %v", err) 69 | } 70 | go func() { 71 | ctx, cancel := context.WithCancel(context.Background()) 72 | defer cancel() 73 | if err := sg.Do(ctx, func() error { return nil }); err != nil { 74 | t.Errorf("unexpected err %v", err) 75 | } 76 | }() 77 | for sg.suppressing() != 3 { 78 | time.Sleep(time.Millisecond) 79 | } 80 | close(ch) 81 | if err := sg.Do(context.Background(), func() error { return nil }); err != nil { 82 | t.Fatalf("unexpected err %v", err) 83 | } 84 | } 85 | 86 | func TestSingleFlightLazyDo(t *testing.T) { 87 | defer ShouldNotLeaked(SetupLeakDetection()) 88 | ch := make(chan struct{}) 89 | sg := call{} 90 | sg.LazyDo(time.Second, func() error { 91 | <-ch 92 | return nil 93 | }) 94 | cn := 0 95 | sg.LazyDo(time.Second, func() error { 96 | cn++ // this should not occur 97 | return nil 98 | }) 99 | if cn != 0 { 100 | t.Fatalf("unexpected cn %v", cn) 101 | } 102 | if sc := sg.suppressing(); sc != 1 { 103 | t.Fatalf("unexpected suppressing %v", sc) 104 | } 105 | close(ch) 106 | } 107 | -------------------------------------------------------------------------------- /standalone.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "context" 5 | "math/rand/v2" 6 | "time" 7 | 8 | "github.com/redis/rueidis/internal/cmds" 9 | ) 10 | 11 | func newStandaloneClient(opt *ClientOption, connFn connFn, retryer retryHandler) (*standalone, error) { 12 | if len(opt.InitAddress) == 0 { 13 | return nil, ErrNoAddr 14 | } 15 | p := connFn(opt.InitAddress[0], opt) 16 | if err := p.Dial(); err != nil { 17 | return nil, err 18 | } 19 | s := &standalone{ 20 | toReplicas: opt.SendToReplicas, 21 | primary: newSingleClientWithConn(p, cmds.NewBuilder(cmds.NoSlot), !opt.DisableRetry, opt.DisableCache, retryer, false), 22 | replicas: make([]*singleClient, len(opt.Standalone.ReplicaAddress)), 23 | } 24 | opt.ReplicaOnly = true 25 | for i := range s.replicas { 26 | replicaConn := connFn(opt.Standalone.ReplicaAddress[i], opt) 27 | if err := replicaConn.Dial(); err != nil { 28 | s.primary.Close() // close primary if any replica fails 29 | for j := 0; j < i; j++ { 30 | s.replicas[j].Close() 31 | } 32 | return nil, err 33 | } 34 | s.replicas[i] = newSingleClientWithConn(replicaConn, cmds.NewBuilder(cmds.NoSlot), !opt.DisableRetry, opt.DisableCache, retryer, false) 35 | } 36 | return s, nil 37 | } 38 | 39 | type standalone struct { 40 | toReplicas func(Completed) bool 41 | primary *singleClient 42 | replicas []*singleClient 43 | } 44 | 45 | func (s *standalone) B() Builder { 46 | return s.primary.B() 47 | } 48 | 49 | func (s *standalone) pick() int { 50 | if len(s.replicas) == 1 { 51 | return 0 52 | } 53 | return rand.IntN(len(s.replicas)) 54 | } 55 | 56 | func (s *standalone) Do(ctx context.Context, cmd Completed) (resp RedisResult) { 57 | if s.toReplicas(cmd) { 58 | return s.replicas[s.pick()].Do(ctx, cmd) 59 | } 60 | return s.primary.Do(ctx, cmd) 61 | } 62 | 63 | func (s *standalone) DoMulti(ctx context.Context, multi ...Completed) (resp []RedisResult) { 64 | toReplica := true 65 | for _, cmd := range multi { 66 | if !s.toReplicas(cmd) { 67 | toReplica = false 68 | break 69 | } 70 | } 71 | if toReplica { 72 | return s.replicas[s.pick()].DoMulti(ctx, multi...) 73 | } 74 | return s.primary.DoMulti(ctx, multi...) 75 | } 76 | 77 | func (s *standalone) Receive(ctx context.Context, subscribe Completed, fn func(msg PubSubMessage)) error { 78 | if s.toReplicas(subscribe) { 79 | return s.replicas[s.pick()].Receive(ctx, subscribe, fn) 80 | } 81 | return s.primary.Receive(ctx, subscribe, fn) 82 | } 83 | 84 | func (s *standalone) Close() { 85 | s.primary.Close() 86 | for _, replica := range s.replicas { 87 | replica.Close() 88 | } 89 | } 90 | 91 | func (s *standalone) DoCache(ctx context.Context, cmd Cacheable, ttl time.Duration) (resp RedisResult) { 92 | return s.primary.DoCache(ctx, cmd, ttl) 93 | } 94 | 95 | func (s *standalone) DoMultiCache(ctx context.Context, multi ...CacheableTTL) (resp []RedisResult) { 96 | return s.primary.DoMultiCache(ctx, multi...) 97 | } 98 | 99 | func (s *standalone) DoStream(ctx context.Context, cmd Completed) RedisResultStream { 100 | if s.toReplicas(cmd) { 101 | return s.replicas[s.pick()].DoStream(ctx, cmd) 102 | } 103 | return s.primary.DoStream(ctx, cmd) 104 | } 105 | 106 | func (s *standalone) DoMultiStream(ctx context.Context, multi ...Completed) MultiRedisResultStream { 107 | toReplica := true 108 | for _, cmd := range multi { 109 | if !s.toReplicas(cmd) { 110 | toReplica = false 111 | break 112 | } 113 | } 114 | if toReplica { 115 | return s.replicas[s.pick()].DoMultiStream(ctx, multi...) 116 | } 117 | return s.primary.DoMultiStream(ctx, multi...) 118 | } 119 | 120 | func (s *standalone) Dedicated(fn func(DedicatedClient) error) (err error) { 121 | return s.primary.Dedicated(fn) 122 | } 123 | 124 | func (s *standalone) Dedicate() (client DedicatedClient, cancel func()) { 125 | return s.primary.Dedicate() 126 | } 127 | 128 | func (s *standalone) Nodes() map[string]Client { 129 | nodes := make(map[string]Client, len(s.replicas)+1) 130 | for addr, client := range s.primary.Nodes() { 131 | nodes[addr] = client 132 | } 133 | for _, replica := range s.replicas { 134 | for addr, client := range replica.Nodes() { 135 | nodes[addr] = client 136 | } 137 | } 138 | return nodes 139 | } 140 | 141 | func (s *standalone) Mode() ClientMode { 142 | return ClientModeStandalone 143 | } 144 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | package rueidis 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // ParseURL parses a redis URL into ClientOption. 15 | // https://github.com/redis/redis-specifications/blob/master/uri/redis.txt 16 | // Example: 17 | // 18 | // redis://:@:/ 19 | // redis://:@:?addr=:&addr=: 20 | // unix://:@?db= 21 | func ParseURL(str string) (opt ClientOption, err error) { 22 | u, err := url.Parse(str) 23 | if err != nil { 24 | return opt, err 25 | } 26 | parseAddr := func(hostport string) (host string, addr string) { 27 | host, port, _ := net.SplitHostPort(hostport) 28 | if host == "" { 29 | host = u.Host 30 | } 31 | if host == "" { 32 | host = "localhost" 33 | } 34 | if port == "" { 35 | port = "6379" 36 | } 37 | return host, net.JoinHostPort(host, port) 38 | } 39 | switch u.Scheme { 40 | case "unix": 41 | opt.DialCtxFn = func(ctx context.Context, s string, dialer *net.Dialer, config *tls.Config) (conn net.Conn, err error) { 42 | return dialer.DialContext(ctx, "unix", s) 43 | } 44 | opt.InitAddress = []string{strings.TrimSpace(u.Path)} 45 | case "rediss", "valkeys": 46 | opt.TLSConfig = &tls.Config{ 47 | MinVersion: tls.VersionTLS12, 48 | } 49 | case "redis", "valkey": 50 | default: 51 | return opt, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme) 52 | } 53 | if opt.InitAddress == nil { 54 | host, addr := parseAddr(u.Host) 55 | opt.InitAddress = []string{addr} 56 | if opt.TLSConfig != nil { 57 | opt.TLSConfig.ServerName = host 58 | } 59 | } 60 | if u.User != nil { 61 | opt.Username = u.User.Username() 62 | opt.Password, _ = u.User.Password() 63 | } 64 | if u.Scheme != "unix" { 65 | if ps := strings.Split(u.Path, "/"); len(ps) == 2 { 66 | if opt.SelectDB, err = strconv.Atoi(ps[1]); err != nil { 67 | return opt, fmt.Errorf("redis: invalid database number: %q", ps[1]) 68 | } 69 | } else if len(ps) > 2 { 70 | return opt, fmt.Errorf("redis: invalid URL path: %s", u.Path) 71 | } 72 | } 73 | q := u.Query() 74 | if q.Has("db") { 75 | if opt.SelectDB, err = strconv.Atoi(q.Get("db")); err != nil { 76 | return opt, fmt.Errorf("redis: invalid database number: %q", q.Get("db")) 77 | } 78 | } 79 | if q.Has("dial_timeout") { 80 | if opt.Dialer.Timeout, err = time.ParseDuration(q.Get("dial_timeout")); err != nil { 81 | return opt, fmt.Errorf("redis: invalid dial timeout: %q", q.Get("dial_timeout")) 82 | } 83 | } 84 | if q.Has("write_timeout") { 85 | if opt.Dialer.Timeout, err = time.ParseDuration(q.Get("write_timeout")); err != nil { 86 | return opt, fmt.Errorf("redis: invalid write timeout: %q", q.Get("write_timeout")) 87 | } 88 | } 89 | for _, addr := range q["addr"] { 90 | _, addr = parseAddr(addr) 91 | opt.InitAddress = append(opt.InitAddress, addr) 92 | } 93 | if opt.TLSConfig != nil && q.Has("skip_verify") { 94 | skipVerifyParam := q.Get("skip_verify") 95 | if skipVerifyParam == "" { 96 | opt.TLSConfig.InsecureSkipVerify = true 97 | } else { 98 | skipVerify, err := strconv.ParseBool(skipVerifyParam) 99 | if err != nil { 100 | return opt, fmt.Errorf("valkey: invalid skip verify: %q", skipVerifyParam) 101 | } 102 | opt.TLSConfig.InsecureSkipVerify = skipVerify 103 | } 104 | } 105 | opt.AlwaysRESP2 = q.Get("protocol") == "2" 106 | opt.DisableCache = q.Get("client_cache") == "0" 107 | opt.DisableRetry = q.Get("max_retries") == "0" 108 | opt.ClientName = q.Get("client_name") 109 | opt.Sentinel.MasterSet = q.Get("master_set") 110 | return 111 | } 112 | 113 | func MustParseURL(str string) ClientOption { 114 | opt, err := ParseURL(str) 115 | if err != nil { 116 | panic(err) 117 | } 118 | return opt 119 | } 120 | --------------------------------------------------------------------------------