This commit is contained in:
ofekshenawa 2024-06-20 02:30:37 +03:00
commit 0b95fd7fa5
188 changed files with 45644 additions and 0 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
doctests/* @dmaier-redislabs

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: ['https://uptrace.dev/sponsor']

49
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,49 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
Issue tracker is used for reporting bugs and discussing new features. Please use
[stackoverflow](https://stackoverflow.com) for supporting issues.
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Context (Environment)
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
<!--- Provide a general summary of the issue in the Title above -->
## Detailed Description
<!--- Provide a detailed description of the change or addition you are proposing -->
## Possible Implementation
<!--- Not obligatory, but suggest an idea for implementing addition or change -->

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/go-redis/redis/discussions
about: Ask a question via GitHub Discussions

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

48
.github/release-drafter-config.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name-template: '$NEXT_MINOR_VERSION'
tag-template: 'v$NEXT_MINOR_VERSION'
autolabeler:
- label: 'maintenance'
files:
- '*.md'
- '.github/*'
- label: 'bug'
branch:
- '/bug-.+'
- label: 'maintenance'
branch:
- '/maintenance-.+'
- label: 'feature'
branch:
- '/feature-.+'
categories:
- title: 'Breaking Changes'
labels:
- 'breakingchange'
- title: '🧪 Experimental Features'
labels:
- 'experimental'
- title: '🚀 New Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- 'BUG'
- title: '🧰 Maintenance'
label: 'maintenance'
change-template: '- $TITLE (#$NUMBER)'
exclude-labels:
- 'skip-changelog'
template: |
# Changes
$CHANGES
## Contributors
We'd like to thank all the contributors who worked on this release!
$CONTRIBUTORS

29
.github/spellcheck-settings.yml vendored Normal file
View File

@ -0,0 +1,29 @@
matrix:
- name: Markdown
expect_match: false
apsell:
lang: en
d: en_US
ignore-case: true
dictionary:
wordlists:
- .github/wordlist.txt
output: wordlist.dic
pipeline:
- pyspelling.filters.markdown:
markdown_extensions:
- markdown.extensions.extra:
- pyspelling.filters.html:
comments: false
attributes:
- alt
ignores:
- ':matches(code, pre)'
- code
- pre
- blockquote
- img
sources:
- 'README.md'
- 'FAQ.md'
- 'docs/**'

60
.github/wordlist.txt vendored Normal file
View File

@ -0,0 +1,60 @@
ACLs
autoload
autoloader
autoloading
analytics
Autoloading
backend
backends
behaviour
CAS
ClickHouse
config
customizable
Customizable
dataset
de
DisableIdentity
ElastiCache
extensibility
FPM
Golang
IANA
keyspace
keyspaces
Kvrocks
localhost
Lua
MSSQL
namespace
NoSQL
ORM
Packagist
PhpRedis
pipelining
pluggable
Predis
PSR
Quickstart
README
rebalanced
rebalancing
redis
Redis
RocksDB
runtime
SHA
sharding
SETNAME
SSL
struct
stunnel
TCP
TLS
uri
URI
url
variadic
RedisStack
RedisGears
RedisTimeseries

39
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Go
on:
push:
branches: [master, v9]
pull_request:
branches: [master, v9]
permissions:
contents: read
jobs:
build:
name: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: [1.19.x, 1.20.x, 1.21.x]
services:
redis:
image: redis/redis-stack-server:edge
options: >-
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 6379:6379
steps:
- name: Set up ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- name: Test
run: make test

41
.github/workflows/doctests.yaml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Documentation Tests
on:
push:
branches: [master, examples]
pull_request:
branches: [master, examples]
permissions:
contents: read
jobs:
doctests:
name: doctests
runs-on: ubuntu-latest
services:
redis-stack:
image: redis/redis-stack-server:latest
options: >-
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 6379:6379
strategy:
fail-fast: false
matrix:
go-version: [ "1.18", "1.19", "1.20", "1.21" ]
steps:
- name: Set up ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- name: Test doc examples
working-directory: ./doctests
run: go test

26
.github/workflows/golangci-lint.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: golangci-lint
on:
push:
tags:
- v*
branches:
- master
- main
- v9
pull_request:
permissions:
contents: read
jobs:
golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v4

24
.github/workflows/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- master
permissions: {}
jobs:
update_release_draft:
permissions:
pull-requests: write # to add label to PR (release-drafter/release-drafter)
contents: write # to create a github release (release-drafter/release-drafter)
runs-on: ubuntu-latest
steps:
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v6
with:
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
config-name: release-drafter-config.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

14
.github/workflows/spellcheck.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: spellcheck
on:
pull_request:
jobs:
check-spelling:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check Spelling
uses: rojopolis/spellcheck-github-actions@0.36.0
with:
config_path: .github/spellcheck-settings.yml
task_name: Markdown

25
.github/workflows/stale-issues.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: "Close stale issues"
on:
schedule:
- cron: "0 0 * * *"
permissions: {}
jobs:
stale:
permissions:
issues: write # to close stale issues (actions/stale)
pull-requests: write # to close stale PRs (actions/stale)
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.'
stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.'
days-before-stale: 365
days-before-close: 30
stale-issue-label: "Stale"
stale-pr-label: "Stale"
operations-per-run: 10
remove-stale-when-updated: true

View File

@ -0,0 +1,57 @@
name: RE Tests
on:
push:
branches: [master]
pull_request:
permissions:
contents: read
jobs:
build:
name: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: [1.21.x]
re-build: ["7.4.2-54"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Clone Redis EE docker repository
uses: actions/checkout@v4
with:
repository: RedisLabs/redis-ee-docker
path: redis-ee
- name: Set up ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Build cluster
working-directory: redis-ee
env:
IMAGE: "redislabs/redis:${{ matrix.re-build }}"
RE_USERNAME: test@test.com
RE_PASS: 12345
RE_CLUSTER_NAME: re-test
RE_USE_OSS_CLUSTER: false
RE_DB_PORT: 6379
run: ./build.sh
- name: Test
env:
RE_CLUSTER: "1"
run: |
go test \
--ginkgo.skip-file="ring_test.go" \
--ginkgo.skip-file="sentinel_test.go" \
--ginkgo.skip-file="osscluster_test.go" \
--ginkgo.skip-file="pubsub_test.go" \
--ginkgo.skip-file="gears_commands_test.go" \
--ginkgo.label-filter='!NonRedisEnterprise'

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.rdb
testdata/*
.idea/
.DS_Store
*.tar.gz
*.dic

4
.golangci.yml Normal file
View File

@ -0,0 +1,4 @@
run:
concurrency: 8
deadline: 5m
tests: false

4
.prettierrc.yml Normal file
View File

@ -0,0 +1,4 @@
semi: false
singleQuote: true
proseWrap: always
printWidth: 100

133
CHANGELOG.md Normal file
View File

@ -0,0 +1,133 @@
## Unreleased
### Changed
* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980))
Users can use the OpenTelemetry sampler to control the sampling behavior.
For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep
a similar behavior (drop orphan spans) of `go-redis` as before.
## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29)
### Features
* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602))
* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe))
* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af))
## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01)
### Bug Fixes
* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241))
### Features
* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e))
* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8))
* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af))
## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02)
### New Features
- feat(scan): scan time.Time sets the default decoding (#2413)
- Add support for CLUSTER LINKS command (#2504)
- Add support for acl dryrun command (#2502)
- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500)
- Add support for LCS Command (#2480)
- Add support for BZMPOP (#2456)
- Adding support for ZMPOP command (#2408)
- Add support for LMPOP (#2440)
- feat: remove pool unused fields (#2438)
- Expiretime and PExpireTime (#2426)
- Implement `FUNCTION` group of commands (#2475)
- feat(zadd): add ZAddLT and ZAddGT (#2429)
- Add: Support for COMMAND LIST command (#2491)
- Add support for BLMPOP (#2442)
- feat: check pipeline.Do to prevent confusion with Exec (#2517)
- Function stats, function kill, fcall and fcall_ro (#2486)
- feat: Add support for CLUSTER SHARDS command (#2507)
- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498)
### Fixed
- fix: eval api cmd.SetFirstKeyPos (#2501)
- fix: limit the number of connections created (#2441)
- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479)
- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458)
- fix: group lag can be null (#2448)
### Maintenance
- Updating to the latest version of redis (#2508)
- Allowing for running tests on a port other than the fixed 6380 (#2466)
- redis 7.0.8 in tests (#2450)
- docs: Update redisotel example for v9 (#2425)
- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476)
- chore: add Chinese translation (#2436)
- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421)
- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420)
- chore(deps): bump actions/setup-go from 3 to 4 (#2495)
- docs: add instructions for the HSet api (#2503)
- docs: add reading lag field comment (#2451)
- test: update go mod before testing(go mod tidy) (#2423)
- docs: fix comment typo (#2505)
- test: remove testify (#2463)
- refactor: change ListElementCmd to KeyValuesCmd. (#2443)
- fix(appendArg): appendArg case special type (#2489)
## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01)
### Features
* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65))
## v9 2023-01-30
### Breaking
- Changed Pipelines to not be thread-safe any more.
### Added
- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was
contributed by @monkey92t who has done the majority of work in this release.
- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts
and deadlines. See
[Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details.
- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example,
`redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`.
- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html)
- Added `redis.HasErrorPrefix` to help working with errors.
### Changed
- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is
completely gone in v9.
- Reworked hook interface and added `DialHook`.
- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See
[example](example/otel) and
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html).
- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making
an allocation.
- Renamed the option `MaxConnAge` to `ConnMaxLifetime`.
- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`.
- Removed connection reaper in favor of `MaxIdleConns`.
- Removed `WithContext` since `context.Context` can be passed directly as an arg.
- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and
it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to
reset commands for some reason.
### Fixed
- Improved and fixed pipeline retries.
- As usually, added support for more commands and fixed some bugs.

101
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,101 @@
# Contributing
## Introduction
We appreciate your interest in considering contributing to go-redis.
Community contributions mean a lot to us.
## Contributions we need
You may already know how you'd like to contribute, whether it's a fix for a bug you
encountered, or a new feature your team wants to use.
If you don't know where to start, consider improving
documentation, bug triaging, and writing tutorials are all examples of
helpful contributions that mean less work for you.
## Your First Contribution
Unsure where to begin contributing? You can start by looking through
[help-wanted
issues](https://github.com/redis/go-redis/issues?q=is%3Aopen+is%3Aissue+label%3ahelp-wanted).
Never contributed to open source before? Here are a couple of friendly
tutorials:
- <http://makeapullrequest.com/>
- <http://www.firsttimersonly.com/>
## Getting Started
Here's how to get started with your code contribution:
1. Create your own fork of go-redis
2. Do the changes in your fork
3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running:
```docker run -p 6379:6379 -it redis/redis-stack-server:edge```
4. While developing, make sure the tests pass by running `make tests`
5. If you like the change and think the project could use it, send a
pull request
To see what else is part of the automation, run `invoke -l`
## Testing
Call `make test` to run all tests, including linters.
Continuous Integration uses these same wrappers to run all of these
tests against multiple versions of python. Feel free to test your
changes against all the go versions supported, as declared by the
[build.yml](./.github/workflows/build.yml) file.
### Troubleshooting
If you get any errors when running `make test`, make sure
that you are using supported versions of Docker and go.
## How to Report a Bug
### Security Vulnerabilities
**NOTE**: If you find a security vulnerability, do NOT open an issue.
Email [Redis Open Source (<oss@redis.com>)](mailto:oss@redis.com) instead.
In order to determine whether you are dealing with a security issue, ask
yourself these two questions:
- Can I access something that's not mine, or something I shouldn't
have access to?
- Can I disable something for other people?
If the answer to either of those two questions are *yes*, then you're
probably dealing with a security issue. Note that even if you answer
*no* to both questions, you may still be dealing with a security
issue, so if you're unsure, just email [us](mailto:oss@redis.com).
### Everything Else
When filing an issue, make sure to answer these five questions:
1. What version of go-redis are you using?
2. What version of redis are you using?
3. What did you do?
4. What did you expect to see?
5. What did you see instead?
## Suggest a feature or enhancement
If you'd like to contribute a new feature, make sure you check our
issue list to see if someone has already proposed it. Work may already
be underway on the feature you want or we may have rejected a
feature like it already.
If you don't see anything, open a new issue that describes the feature
you would like and how it should work.
## Code review process
The core team regularly looks at pull requests. We will provide
feedback as soon as possible. After receiving our feedback, please respond
within two weeks. After that time, we may close your PR if it isn't
showing any activity.

25
LICENSE Normal file
View File

@ -0,0 +1,25 @@
Copyright (c) 2013 The github.com/redis/go-redis Authors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

49
Makefile Normal file
View File

@ -0,0 +1,49 @@
GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
test: testdeps
$(eval GO_VERSION := $(shell go version | cut -d " " -f 3 | cut -d. -f2))
set -e; for dir in $(GO_MOD_DIRS); do \
if echo "$${dir}" | grep -q "./example" && [ "$(GO_VERSION)" = "19" ]; then \
echo "Skipping go test in $${dir} due to Go version 1.19 and dir contains ./example"; \
continue; \
fi; \
echo "go test in $${dir}"; \
(cd "$${dir}" && \
go mod tidy -compat=1.18 && \
go test && \
go test ./... -short -race && \
go test ./... -run=NONE -bench=. -benchmem && \
env GOOS=linux GOARCH=386 go test && \
go vet); \
done
cd internal/customvet && go build .
go vet -vettool ./internal/customvet/customvet
testdeps: testdata/redis/src/redis-server
bench: testdeps
go test ./... -test.run=NONE -test.bench=. -test.benchmem
.PHONY: all test testdeps bench fmt
build:
go build .
testdata/redis:
mkdir -p $@
wget -qO- https://download.redis.io/releases/redis-7.4-rc1.tar.gz | tar xvz --strip-components=1 -C $@
testdata/redis/src/redis-server: testdata/redis
cd $< && make all
fmt:
gofumpt -w ./
goimports -w -local github.com/redis/go-redis ./
go_mod_tidy:
set -e; for dir in $(GO_MOD_DIRS); do \
echo "go mod tidy in $${dir}"; \
(cd "$${dir}" && \
go get -u ./... && \
go mod tidy -compat=1.18); \
done

271
README.md Normal file
View File

@ -0,0 +1,271 @@
# Redis client for Go
[![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)
[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/)
[![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj)
> go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can
> use it to monitor applications and set up automatic alerts to receive notifications via email,
> Slack, Telegram, and others.
>
> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which
> demonstrates how you can use Uptrace to monitor go-redis.
## How do I Redis?
[Learn for free at Redis University](https://university.redis.com/)
[Build faster with the Redis Launchpad](https://launchpad.redis.com/)
[Try the Redis Cloud](https://redis.com/try-free/)
[Dive in developer tutorials](https://developer.redis.com/)
[Join the Redis community](https://redis.com/community/)
[Work at Redis](https://redis.com/company/careers/jobs/)
## Documentation
- [English](https://redis.uptrace.dev)
- [简体中文](https://redis.uptrace.dev/zh/)
## Resources
- [Discussions](https://github.com/redis/go-redis/discussions)
- [Chat](https://discord.gg/rWtp5Aj)
- [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9)
- [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples)
## Ecosystem
- [Redis Mock](https://github.com/go-redis/redismock)
- [Distributed Locks](https://github.com/bsm/redislock)
- [Redis Cache](https://github.com/go-redis/cache)
- [Rate limiting](https://github.com/go-redis/redis_rate)
This client also works with [Kvrocks](https://github.com/apache/incubator-kvrocks), a distributed
key value NoSQL database that uses RocksDB as storage engine and is compatible with Redis protocol.
## Features
- Redis commands except QUIT and SYNC.
- Automatic connection pooling.
- [Pub/Sub](https://redis.uptrace.dev/guide/go-redis-pubsub.html).
- [Pipelines and transactions](https://redis.uptrace.dev/guide/go-redis-pipelines.html).
- [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html).
- [Redis Sentinel](https://redis.uptrace.dev/guide/go-redis-sentinel.html).
- [Redis Cluster](https://redis.uptrace.dev/guide/go-redis-cluster.html).
- [Redis Ring](https://redis.uptrace.dev/guide/ring.html).
- [Redis Performance Monitoring](https://redis.uptrace.dev/guide/redis-performance-monitoring.html).
- [Redis Probabilistic [RedisStack]](https://redis.io/docs/data-types/probabilistic/)
## Installation
go-redis supports 2 last Go versions and requires a Go version with
[modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go
module:
```shell
go mod init github.com/my/repo
```
Then install go-redis/**v9**:
```shell
go get github.com/redis/go-redis/v9
```
## Quickstart
```go
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
var ctx = context.Background()
func ExampleClient() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
panic(err)
}
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
panic(err)
}
fmt.Println("key", val)
val2, err := rdb.Get(ctx, "key2").Result()
if err == redis.Nil {
fmt.Println("key2 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("key2", val2)
}
// Output: key value
// key2 does not exist
}
```
The above can be modified to specify the version of the RESP protocol by adding the `protocol`
option to the `Options` struct:
```go
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
Protocol: 3, // specify 2 for RESP 2 or 3 for RESP 3
})
```
### Connecting via a redis url
go-redis also supports connecting via the
[redis uri specification](https://github.com/redis/redis-specifications/tree/master/uri/redis.txt).
The example below demonstrates how the connection can easily be configured using a string, adhering
to this specification.
```go
import (
"github.com/redis/go-redis/v9"
)
func ExampleClient() *redis.Client {
url := "redis://user:password@localhost:6379/0?protocol=3"
opts, err := redis.ParseURL(url)
if err != nil {
panic(err)
}
return redis.NewClient(opts)
}
```
### Advanced Configuration
go-redis supports extending the client identification phase to allow projects to send their own custom client identification.
#### Default Client Identification
By default, go-redis automatically sends the client library name and version during the connection process. This feature is available in redis-server as of version 7.2. As a result, the command is "fire and forget", meaning it should fail silently, in the case that the redis server does not support this feature.
#### Disabling Identity Verification
When connection identity verification is not required or needs to be explicitly disabled, a `DisableIndentity` configuration option exists. In V10 of this library, `DisableIndentity` will become `DisableIdentity` in order to fix the associated typo.
To disable verification, set the `DisableIndentity` option to `true` in the Redis client options:
```go
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
DisableIndentity: true, // Disable set-info on connect
})
```
## Contributing
Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library!
## Look and feel
Some corner cases:
```go
// SET key value EX 10 NX
set, err := rdb.SetNX(ctx, "key", "value", 10*time.Second).Result()
// SET key value keepttl NX
set, err := rdb.SetNX(ctx, "key", "value", redis.KeepTTL).Result()
// SORT list LIMIT 0 2 ASC
vals, err := rdb.Sort(ctx, "list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result()
// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2
vals, err := rdb.ZRangeByScoreWithScores(ctx, "zset", &redis.ZRangeBy{
Min: "-inf",
Max: "+inf",
Offset: 0,
Count: 2,
}).Result()
// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM
vals, err := rdb.ZInterStore(ctx, "out", &redis.ZStore{
Keys: []string{"zset1", "zset2"},
Weights: []int64{2, 3}
}).Result()
// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello"
vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result()
// custom command
res, err := rdb.Do(ctx, "set", "key", "value").Result()
```
## Run the test
go-redis will start a redis-server and run the test cases.
The paths of redis-server bin file and redis config file are defined in `main_test.go`:
```go
var (
redisServerBin, _ = filepath.Abs(filepath.Join("testdata", "redis", "src", "redis-server"))
redisServerConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "redis.conf"))
)
```
For local testing, you can change the variables to refer to your local files, or create a soft link
to the corresponding folder for redis-server and copy the config file to `testdata/redis/`:
```shell
ln -s /usr/bin/redis-server ./go-redis/testdata/redis/src
cp ./go-redis/testdata/redis.conf ./go-redis/testdata/redis/
```
Lastly, run:
```shell
go test
```
Another option is to run your specific tests with an already running redis. The example below, tests
against a redis running on port 9999.:
```shell
REDIS_PORT=9999 go test <your options>
```
## See also
- [Golang ORM](https://bun.uptrace.dev) for PostgreSQL, MySQL, MSSQL, and SQLite
- [Golang PostgreSQL](https://bun.uptrace.dev/postgres/)
- [Golang HTTP router](https://bunrouter.uptrace.dev/)
- [Golang ClickHouse ORM](https://github.com/uptrace/go-clickhouse)
## Contributors
Thanks to all the people who already contributed!
<a href="https://github.com/redis/go-redis/graphs/contributors">
<img src="https://contributors-img.web.app/image?repo=redis/go-redis" />
</a>

15
RELEASING.md Normal file
View File

@ -0,0 +1,15 @@
# Releasing
1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub:
```shell
TAG=v1.0.0 ./scripts/release.sh
```
2. Open a pull request and wait for the build to finish.
3. Merge the pull request and run `tag.sh` to create tags for packages:
```shell
TAG=v1.0.0 ./scripts/tag.sh
```

35
acl_commands.go Normal file
View File

@ -0,0 +1,35 @@
package redis
import "context"
type ACLCmdable interface {
ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd
ACLLog(ctx context.Context, count int64) *ACLLogCmd
ACLLogReset(ctx context.Context) *StatusCmd
}
func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd {
args := make([]interface{}, 0, 3+len(command))
args = append(args, "acl", "dryrun", username)
args = append(args, command...)
cmd := NewStringCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLLog(ctx context.Context, count int64) *ACLLogCmd {
args := make([]interface{}, 0, 3)
args = append(args, "acl", "log")
if count > 0 {
args = append(args, count)
}
cmd := NewACLLogCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "acl", "log", "reset")
_ = c(ctx, cmd)
return cmd
}

316
bench_decode_test.go Normal file
View File

@ -0,0 +1,316 @@
package redis
import (
"context"
"fmt"
"io"
"net"
"testing"
"time"
"github.com/redis/go-redis/v9/internal/proto"
)
var ctx = context.TODO()
type ClientStub struct {
Cmdable
resp []byte
}
var initHello = []byte("%1\r\n+proto\r\n:3\r\n")
func NewClientStub(resp []byte) *ClientStub {
stub := &ClientStub{
resp: resp,
}
stub.Cmdable = NewClient(&Options{
PoolSize: 128,
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return stub.stubConn(initHello), nil
},
DisableIndentity: true,
})
return stub
}
func NewClusterClientStub(resp []byte) *ClientStub {
stub := &ClientStub{
resp: resp,
}
client := NewClusterClient(&ClusterOptions{
PoolSize: 128,
Addrs: []string{":6379"},
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return stub.stubConn(initHello), nil
},
DisableIndentity: true,
ClusterSlots: func(_ context.Context) ([]ClusterSlot, error) {
return []ClusterSlot{
{
Start: 0,
End: 16383,
Nodes: []ClusterNode{{Addr: "127.0.0.1:6379"}},
},
}, nil
},
})
stub.Cmdable = client
return stub
}
func (c *ClientStub) stubConn(init []byte) *ConnStub {
return &ConnStub{
init: init,
resp: c.resp,
}
}
type ConnStub struct {
init []byte
resp []byte
pos int
}
func (c *ConnStub) Read(b []byte) (n int, err error) {
// Return conn.init()
if len(c.init) > 0 {
n = copy(b, c.init)
c.init = c.init[n:]
return n, nil
}
if len(c.resp) == 0 {
return 0, io.EOF
}
if c.pos >= len(c.resp) {
c.pos = 0
}
n = copy(b, c.resp[c.pos:])
c.pos += n
return n, nil
}
func (c *ConnStub) Write(b []byte) (n int, err error) { return len(b), nil }
func (c *ConnStub) Close() error { return nil }
func (c *ConnStub) LocalAddr() net.Addr { return nil }
func (c *ConnStub) RemoteAddr() net.Addr { return nil }
func (c *ConnStub) SetDeadline(_ time.Time) error { return nil }
func (c *ConnStub) SetReadDeadline(_ time.Time) error { return nil }
func (c *ConnStub) SetWriteDeadline(_ time.Time) error { return nil }
type ClientStubFunc func([]byte) *ClientStub
func BenchmarkDecode(b *testing.B) {
type Benchmark struct {
name string
stub ClientStubFunc
}
benchmarks := []Benchmark{
{"server", NewClientStub},
{"cluster", NewClusterClientStub},
}
for _, bench := range benchmarks {
b.Run(fmt.Sprintf("RespError-%s", bench.name), func(b *testing.B) {
respError(b, bench.stub)
})
b.Run(fmt.Sprintf("RespStatus-%s", bench.name), func(b *testing.B) {
respStatus(b, bench.stub)
})
b.Run(fmt.Sprintf("RespInt-%s", bench.name), func(b *testing.B) {
respInt(b, bench.stub)
})
b.Run(fmt.Sprintf("RespString-%s", bench.name), func(b *testing.B) {
respString(b, bench.stub)
})
b.Run(fmt.Sprintf("RespArray-%s", bench.name), func(b *testing.B) {
respArray(b, bench.stub)
})
b.Run(fmt.Sprintf("RespPipeline-%s", bench.name), func(b *testing.B) {
respPipeline(b, bench.stub)
})
b.Run(fmt.Sprintf("RespTxPipeline-%s", bench.name), func(b *testing.B) {
respTxPipeline(b, bench.stub)
})
// goroutine
b.Run(fmt.Sprintf("DynamicGoroutine-%s-pool=5", bench.name), func(b *testing.B) {
dynamicGoroutine(b, bench.stub, 5)
})
b.Run(fmt.Sprintf("DynamicGoroutine-%s-pool=20", bench.name), func(b *testing.B) {
dynamicGoroutine(b, bench.stub, 20)
})
b.Run(fmt.Sprintf("DynamicGoroutine-%s-pool=50", bench.name), func(b *testing.B) {
dynamicGoroutine(b, bench.stub, 50)
})
b.Run(fmt.Sprintf("DynamicGoroutine-%s-pool=100", bench.name), func(b *testing.B) {
dynamicGoroutine(b, bench.stub, 100)
})
b.Run(fmt.Sprintf("StaticGoroutine-%s-pool=5", bench.name), func(b *testing.B) {
staticGoroutine(b, bench.stub, 5)
})
b.Run(fmt.Sprintf("StaticGoroutine-%s-pool=20", bench.name), func(b *testing.B) {
staticGoroutine(b, bench.stub, 20)
})
b.Run(fmt.Sprintf("StaticGoroutine-%s-pool=50", bench.name), func(b *testing.B) {
staticGoroutine(b, bench.stub, 50)
})
b.Run(fmt.Sprintf("StaticGoroutine-%s-pool=100", bench.name), func(b *testing.B) {
staticGoroutine(b, bench.stub, 100)
})
}
}
func respError(b *testing.B, stub ClientStubFunc) {
rdb := stub([]byte("-ERR test error\r\n"))
respErr := proto.RedisError("ERR test error")
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := rdb.Get(ctx, "key").Err(); err != respErr {
b.Fatalf("response error, got %q, want %q", err, respErr)
}
}
}
func respStatus(b *testing.B, stub ClientStubFunc) {
rdb := stub([]byte("+OK\r\n"))
var val string
b.ResetTimer()
for i := 0; i < b.N; i++ {
if val = rdb.Set(ctx, "key", "value", 0).Val(); val != "OK" {
b.Fatalf("response error, got %q, want OK", val)
}
}
}
func respInt(b *testing.B, stub ClientStubFunc) {
rdb := stub([]byte(":10\r\n"))
var val int64
b.ResetTimer()
for i := 0; i < b.N; i++ {
if val = rdb.Incr(ctx, "key").Val(); val != 10 {
b.Fatalf("response error, got %q, want 10", val)
}
}
}
func respString(b *testing.B, stub ClientStubFunc) {
rdb := stub([]byte("$5\r\nhello\r\n"))
var val string
b.ResetTimer()
for i := 0; i < b.N; i++ {
if val = rdb.Get(ctx, "key").Val(); val != "hello" {
b.Fatalf("response error, got %q, want hello", val)
}
}
}
func respArray(b *testing.B, stub ClientStubFunc) {
rdb := stub([]byte("*3\r\n$5\r\nhello\r\n:10\r\n+OK\r\n"))
var val []interface{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if val = rdb.MGet(ctx, "key").Val(); len(val) != 3 {
b.Fatalf("response error, got len(%d), want len(3)", len(val))
}
}
}
func respPipeline(b *testing.B, stub ClientStubFunc) {
rdb := stub([]byte("+OK\r\n$5\r\nhello\r\n:1\r\n"))
var pipe Pipeliner
b.ResetTimer()
for i := 0; i < b.N; i++ {
pipe = rdb.Pipeline()
set := pipe.Set(ctx, "key", "value", 0)
get := pipe.Get(ctx, "key")
del := pipe.Del(ctx, "key")
_, err := pipe.Exec(ctx)
if err != nil {
b.Fatalf("response error, got %q, want nil", err)
}
if set.Val() != "OK" || get.Val() != "hello" || del.Val() != 1 {
b.Fatal("response error")
}
}
}
func respTxPipeline(b *testing.B, stub ClientStubFunc) {
rdb := stub([]byte("+OK\r\n+QUEUED\r\n+QUEUED\r\n+QUEUED\r\n*3\r\n+OK\r\n$5\r\nhello\r\n:1\r\n"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
var set *StatusCmd
var get *StringCmd
var del *IntCmd
_, err := rdb.TxPipelined(ctx, func(pipe Pipeliner) error {
set = pipe.Set(ctx, "key", "value", 0)
get = pipe.Get(ctx, "key")
del = pipe.Del(ctx, "key")
return nil
})
if err != nil {
b.Fatalf("response error, got %q, want nil", err)
}
if set.Val() != "OK" || get.Val() != "hello" || del.Val() != 1 {
b.Fatal("response error")
}
}
}
func dynamicGoroutine(b *testing.B, stub ClientStubFunc, concurrency int) {
rdb := stub([]byte("$5\r\nhello\r\n"))
c := make(chan struct{}, concurrency)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c <- struct{}{}
go func() {
if val := rdb.Get(ctx, "key").Val(); val != "hello" {
panic(fmt.Sprintf("response error, got %q, want hello", val))
}
<-c
}()
}
// Here no longer wait for all goroutines to complete, it will not affect the test results.
close(c)
}
func staticGoroutine(b *testing.B, stub ClientStubFunc, concurrency int) {
rdb := stub([]byte("$5\r\nhello\r\n"))
c := make(chan struct{}, concurrency)
b.ResetTimer()
for i := 0; i < concurrency; i++ {
go func() {
for {
_, ok := <-c
if !ok {
return
}
if val := rdb.Get(ctx, "key").Val(); val != "hello" {
panic(fmt.Sprintf("response error, got %q, want hello", val))
}
}
}()
}
for i := 0; i < b.N; i++ {
c <- struct{}{}
}
close(c)
}

442
bench_test.go Normal file
View File

@ -0,0 +1,442 @@
package redis_test
import (
"bytes"
"context"
"fmt"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/redis/go-redis/v9"
)
func benchmarkRedisClient(ctx context.Context, poolSize int) *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: ":6379",
DialTimeout: time.Second,
ReadTimeout: time.Second,
WriteTimeout: time.Second,
PoolSize: poolSize,
})
if err := client.FlushDB(ctx).Err(); err != nil {
panic(err)
}
return client
}
func BenchmarkRedisPing(b *testing.B) {
ctx := context.Background()
rdb := benchmarkRedisClient(ctx, 10)
defer rdb.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if err := rdb.Ping(ctx).Err(); err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkSetGoroutines(b *testing.B) {
ctx := context.Background()
rdb := benchmarkRedisClient(ctx, 10)
defer rdb.Close()
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := rdb.Set(ctx, "hello", "world", 0).Err()
if err != nil {
panic(err)
}
}()
}
wg.Wait()
}
}
func BenchmarkRedisGetNil(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, 10)
defer client.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if err := client.Get(ctx, "key").Err(); err != redis.Nil {
b.Fatal(err)
}
}
})
}
type setStringBenchmark struct {
poolSize int
valueSize int
}
func (bm setStringBenchmark) String() string {
return fmt.Sprintf("pool=%d value=%d", bm.poolSize, bm.valueSize)
}
func BenchmarkRedisSetString(b *testing.B) {
benchmarks := []setStringBenchmark{
{10, 64},
{10, 1024},
{10, 64 * 1024},
{10, 1024 * 1024},
{10, 10 * 1024 * 1024},
{100, 64},
{100, 1024},
{100, 64 * 1024},
{100, 1024 * 1024},
{100, 10 * 1024 * 1024},
}
for _, bm := range benchmarks {
b.Run(bm.String(), func(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, bm.poolSize)
defer client.Close()
value := strings.Repeat("1", bm.valueSize)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Set(ctx, "key", value, 0).Err()
if err != nil {
b.Fatal(err)
}
}
})
})
}
}
func BenchmarkRedisSetGetBytes(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, 10)
defer client.Close()
value := bytes.Repeat([]byte{'1'}, 10000)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if err := client.Set(ctx, "key", value, 0).Err(); err != nil {
b.Fatal(err)
}
got, err := client.Get(ctx, "key").Bytes()
if err != nil {
b.Fatal(err)
}
if !bytes.Equal(got, value) {
b.Fatalf("got != value")
}
}
})
}
func BenchmarkRedisMGet(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, 10)
defer client.Close()
if err := client.MSet(ctx, "key1", "hello1", "key2", "hello2").Err(); err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if err := client.MGet(ctx, "key1", "key2").Err(); err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkSetExpire(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, 10)
defer client.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if err := client.Set(ctx, "key", "hello", 0).Err(); err != nil {
b.Fatal(err)
}
if err := client.Expire(ctx, "key", time.Second).Err(); err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkPipeline(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, 10)
defer client.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Set(ctx, "key", "hello", 0)
pipe.Expire(ctx, "key", time.Second)
return nil
})
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkZAdd(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, 10)
defer client.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.ZAdd(ctx, "key", redis.Z{
Score: float64(1),
Member: "hello",
}).Err()
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkXRead(b *testing.B) {
ctx := context.Background()
client := benchmarkRedisClient(ctx, 10)
defer client.Close()
args := redis.XAddArgs{
Stream: "1",
ID: "*",
Values: map[string]string{"uno": "dos"},
}
lenStreams := 16
streams := make([]string, 0, lenStreams)
for i := 0; i < lenStreams; i++ {
streams = append(streams, strconv.Itoa(i))
}
for i := 0; i < lenStreams; i++ {
streams = append(streams, "0")
}
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
client.XAdd(ctx, &args)
err := client.XRead(ctx, &redis.XReadArgs{
Streams: streams,
Count: 1,
Block: time.Second,
}).Err()
if err != nil {
b.Fatal(err)
}
}
})
}
//------------------------------------------------------------------------------
func newClusterScenario() *clusterScenario {
return &clusterScenario{
ports: []string{"8220", "8221", "8222", "8223", "8224", "8225"},
nodeIDs: make([]string, 6),
processes: make(map[string]*redisProcess, 6),
clients: make(map[string]*redis.Client, 6),
}
}
func BenchmarkClusterPing(b *testing.B) {
if testing.Short() {
b.Skip("skipping in short mode")
}
ctx := context.Background()
cluster := newClusterScenario()
if err := startCluster(ctx, cluster); err != nil {
b.Fatal(err)
}
defer cluster.Close()
client := cluster.newClusterClient(ctx, redisClusterOptions())
defer client.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Ping(ctx).Err()
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkClusterDoInt(b *testing.B) {
if testing.Short() {
b.Skip("skipping in short mode")
}
ctx := context.Background()
cluster := newClusterScenario()
if err := startCluster(ctx, cluster); err != nil {
b.Fatal(err)
}
defer cluster.Close()
client := cluster.newClusterClient(ctx, redisClusterOptions())
defer client.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Do(ctx, "SET", 10, 10).Err()
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkClusterSetString(b *testing.B) {
if testing.Short() {
b.Skip("skipping in short mode")
}
ctx := context.Background()
cluster := newClusterScenario()
if err := startCluster(ctx, cluster); err != nil {
b.Fatal(err)
}
defer cluster.Close()
client := cluster.newClusterClient(ctx, redisClusterOptions())
defer client.Close()
value := string(bytes.Repeat([]byte{'1'}, 10000))
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err := client.Set(ctx, "key", value, 0).Err()
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkExecRingSetAddrsCmd(b *testing.B) {
const (
ringShard1Name = "ringShardOne"
ringShard2Name = "ringShardTwo"
)
for _, port := range []string{ringShard1Port, ringShard2Port} {
if _, err := startRedis(port); err != nil {
b.Fatal(err)
}
}
b.Cleanup(func() {
for _, p := range processes {
if err := p.Close(); err != nil {
b.Errorf("Failed to stop redis process: %v", err)
}
}
processes = nil
})
ring := redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"ringShardOne": ":" + ringShard1Port,
},
NewClient: func(opt *redis.Options) *redis.Client {
// Simulate slow shard creation
time.Sleep(100 * time.Millisecond)
return redis.NewClient(opt)
},
})
defer ring.Close()
if _, err := ring.Ping(context.Background()).Result(); err != nil {
b.Fatal(err)
}
// Continuously update addresses by adding and removing one address
updatesDone := make(chan struct{})
defer func() { close(updatesDone) }()
go func() {
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for i := 0; ; i++ {
select {
case <-ticker.C:
if i%2 == 0 {
ring.SetAddrs(map[string]string{
ringShard1Name: ":" + ringShard1Port,
})
} else {
ring.SetAddrs(map[string]string{
ringShard1Name: ":" + ringShard1Port,
ringShard2Name: ":" + ringShard2Port,
})
}
case <-updatesDone:
return
}
}
}()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := ring.Ping(context.Background()).Result(); err != nil {
if err == redis.ErrClosed {
// The shard client could be closed while ping command is in progress
continue
} else {
b.Fatal(err)
}
}
}
}

161
bitmap_commands.go Normal file
View File

@ -0,0 +1,161 @@
package redis
import (
"context"
"errors"
)
type BitMapCmdable interface {
GetBit(ctx context.Context, key string, offset int64) *IntCmd
SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd
BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd
BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd
BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd
BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd
BitOpNot(ctx context.Context, destKey string, key string) *IntCmd
BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd
BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd
BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd
BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd
}
func (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd {
cmd := NewIntCmd(ctx, "getbit", key, offset)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd {
cmd := NewIntCmd(
ctx,
"setbit",
key,
offset,
value,
)
_ = c(ctx, cmd)
return cmd
}
type BitCount struct {
Start, End int64
Unit string // BYTE(default) | BIT
}
const BitCountIndexByte string = "BYTE"
const BitCountIndexBit string = "BIT"
func (c cmdable) BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd {
args := make([]any, 2, 5)
args[0] = "bitcount"
args[1] = key
if bitCount != nil {
args = append(args, bitCount.Start, bitCount.End)
if bitCount.Unit != "" {
if bitCount.Unit != BitCountIndexByte && bitCount.Unit != BitCountIndexBit {
cmd := NewIntCmd(ctx)
cmd.SetErr(errors.New("redis: invalid bitcount index"))
return cmd
}
args = append(args, bitCount.Unit)
}
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) *IntCmd {
args := make([]interface{}, 3+len(keys))
args[0] = "bitop"
args[1] = op
args[2] = destKey
for i, key := range keys {
args[3+i] = key
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd {
return c.bitOp(ctx, "and", destKey, keys...)
}
func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd {
return c.bitOp(ctx, "or", destKey, keys...)
}
func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd {
return c.bitOp(ctx, "xor", destKey, keys...)
}
func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd {
return c.bitOp(ctx, "not", destKey, key)
}
// BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end
// if you need the `byte | bit` parameter, please use `BitPosSpan`.
func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd {
args := make([]interface{}, 3+len(pos))
args[0] = "bitpos"
args[1] = key
args[2] = bit
switch len(pos) {
case 0:
case 1:
args[3] = pos[0]
case 2:
args[3] = pos[0]
args[4] = pos[1]
default:
panic("too many arguments")
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// BitPosSpan supports the `byte | bit` parameters in redis version 7.0,
// the bitpos command defaults to using byte type for the `start-end` range,
// which means it counts in bytes from start to end. you can set the value
// of "span" to determine the type of `start-end`.
// span = "bit", cmd: bitpos key bit start end bit
// span = "byte", cmd: bitpos key bit start end byte
func (c cmdable) BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd {
cmd := NewIntCmd(ctx, "bitpos", key, bit, start, end, span)
_ = c(ctx, cmd)
return cmd
}
// BitField accepts multiple values:
// - BitField("set", "i1", "offset1", "value1","cmd2", "type2", "offset2", "value2")
// - BitField([]string{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"})
// - BitField([]interface{}{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"})
func (c cmdable) BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd {
args := make([]interface{}, 2, 2+len(values))
args[0] = "bitfield"
args[1] = key
args = appendArgs(args, values)
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// BitFieldRO - Read-only variant of the BITFIELD command.
// It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas.
// - BitFieldRO(ctx, key, "<Encoding0>", "<Offset0>", "<Encoding1>","<Offset1>")
func (c cmdable) BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd {
args := make([]interface{}, 2, 2+len(values))
args[0] = "BITFIELD_RO"
args[1] = key
if len(values)%2 != 0 {
panic("BitFieldRO: invalid number of arguments, must be even")
}
for i := 0; i < len(values); i += 2 {
args = append(args, "GET", values[i], values[i+1])
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}

98
bitmap_commands_test.go Normal file
View File

@ -0,0 +1,98 @@
package redis_test
import (
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
"github.com/redis/go-redis/v9"
)
type bitCountExpected struct {
Start int64
End int64
Expected int64
}
var _ = Describe("BitCountBite", func() {
var client *redis.Client
key := "bit_count_test"
BeforeEach(func() {
client = redis.NewClient(redisOptions())
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
values := []int{0, 1, 0, 0, 1, 0, 1, 0, 1, 1}
for i, v := range values {
cmd := client.SetBit(ctx, key, int64(i), v)
Expect(cmd.Err()).NotTo(HaveOccurred())
}
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
It("bit count bite", func() {
var expected = []bitCountExpected{
{0, 0, 0},
{0, 1, 1},
{0, 2, 1},
{0, 3, 1},
{0, 4, 2},
{0, 5, 2},
{0, 6, 3},
{0, 7, 3},
{0, 8, 4},
{0, 9, 5},
}
for _, e := range expected {
cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexBit})
Expect(cmd.Err()).NotTo(HaveOccurred())
Expect(cmd.Val()).To(Equal(e.Expected))
}
})
})
var _ = Describe("BitCountByte", func() {
var client *redis.Client
key := "bit_count_test"
BeforeEach(func() {
client = redis.NewClient(redisOptions())
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
values := []int{0, 0, 0, 0, 0, 0, 0, 1, 1, 1}
for i, v := range values {
cmd := client.SetBit(ctx, key, int64(i), v)
Expect(cmd.Err()).NotTo(HaveOccurred())
}
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
It("bit count byte", func() {
var expected = []bitCountExpected{
{0, 0, 1},
{0, 1, 3},
}
for _, e := range expected {
cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexByte})
Expect(cmd.Err()).NotTo(HaveOccurred())
Expect(cmd.Val()).To(Equal(e.Expected))
}
})
It("bit count byte with no unit specified", func() {
var expected = []bitCountExpected{
{0, 0, 1},
{0, 1, 3},
}
for _, e := range expected {
cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End})
Expect(cmd.Err()).NotTo(HaveOccurred())
Expect(cmd.Val()).To(Equal(e.Expected))
}
})
})

192
cluster_commands.go Normal file
View File

@ -0,0 +1,192 @@
package redis
import "context"
type ClusterCmdable interface {
ClusterMyShardID(ctx context.Context) *StringCmd
ClusterSlots(ctx context.Context) *ClusterSlotsCmd
ClusterShards(ctx context.Context) *ClusterShardsCmd
ClusterLinks(ctx context.Context) *ClusterLinksCmd
ClusterNodes(ctx context.Context) *StringCmd
ClusterMeet(ctx context.Context, host, port string) *StatusCmd
ClusterForget(ctx context.Context, nodeID string) *StatusCmd
ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd
ClusterResetSoft(ctx context.Context) *StatusCmd
ClusterResetHard(ctx context.Context) *StatusCmd
ClusterInfo(ctx context.Context) *StringCmd
ClusterKeySlot(ctx context.Context, key string) *IntCmd
ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd
ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd
ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd
ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd
ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd
ClusterSaveConfig(ctx context.Context) *StatusCmd
ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd
ClusterFailover(ctx context.Context) *StatusCmd
ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd
ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd
ReadOnly(ctx context.Context) *StatusCmd
ReadWrite(ctx context.Context) *StatusCmd
}
func (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "cluster", "myshardid")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterShards(ctx context.Context) *ClusterShardsCmd {
cmd := NewClusterShardsCmd(ctx, "cluster", "shards")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterLinks(ctx context.Context) *ClusterLinksCmd {
cmd := NewClusterLinksCmd(ctx, "cluster", "links")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterNodes(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "cluster", "nodes")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterMeet(ctx context.Context, host, port string) *StatusCmd {
cmd := NewStatusCmd(ctx, "cluster", "meet", host, port)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterForget(ctx context.Context, nodeID string) *StatusCmd {
cmd := NewStatusCmd(ctx, "cluster", "forget", nodeID)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd {
cmd := NewStatusCmd(ctx, "cluster", "replicate", nodeID)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterResetSoft(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "cluster", "reset", "soft")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterResetHard(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "cluster", "reset", "hard")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterInfo(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "cluster", "info")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterKeySlot(ctx context.Context, key string) *IntCmd {
cmd := NewIntCmd(ctx, "cluster", "keyslot", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", slot, count)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd {
cmd := NewIntCmd(ctx, "cluster", "count-failure-reports", nodeID)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd {
cmd := NewIntCmd(ctx, "cluster", "countkeysinslot", slot)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd {
args := make([]interface{}, 2+len(slots))
args[0] = "cluster"
args[1] = "delslots"
for i, slot := range slots {
args[2+i] = slot
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd {
size := max - min + 1
slots := make([]int, size)
for i := 0; i < size; i++ {
slots[i] = min + i
}
return c.ClusterDelSlots(ctx, slots...)
}
func (c cmdable) ClusterSaveConfig(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "cluster", "saveconfig")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "cluster", "slaves", nodeID)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterFailover(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "cluster", "failover")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd {
args := make([]interface{}, 2+len(slots))
args[0] = "cluster"
args[1] = "addslots"
for i, num := range slots {
args[2+i] = num
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd {
size := max - min + 1
slots := make([]int, size)
for i := 0; i < size; i++ {
slots[i] = min + i
}
return c.ClusterAddSlots(ctx, slots...)
}
func (c cmdable) ReadOnly(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "readonly")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ReadWrite(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "readwrite")
_ = c(ctx, cmd)
return cmd
}

5489
command.go Normal file

File diff suppressed because it is too large Load Diff

96
command_test.go Normal file
View File

@ -0,0 +1,96 @@
package redis_test
import (
"errors"
"time"
"github.com/redis/go-redis/v9"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
)
var _ = Describe("Cmd", func() {
var client *redis.Client
BeforeEach(func() {
client = redis.NewClient(redisOptions())
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
It("implements Stringer", func() {
set := client.Set(ctx, "foo", "bar", 0)
Expect(set.String()).To(Equal("set foo bar: OK"))
get := client.Get(ctx, "foo")
Expect(get.String()).To(Equal("get foo: bar"))
})
It("has val/err", func() {
set := client.Set(ctx, "key", "hello", 0)
Expect(set.Err()).NotTo(HaveOccurred())
Expect(set.Val()).To(Equal("OK"))
get := client.Get(ctx, "key")
Expect(get.Err()).NotTo(HaveOccurred())
Expect(get.Val()).To(Equal("hello"))
Expect(set.Err()).NotTo(HaveOccurred())
Expect(set.Val()).To(Equal("OK"))
})
It("has helpers", func() {
set := client.Set(ctx, "key", "10", 0)
Expect(set.Err()).NotTo(HaveOccurred())
n, err := client.Get(ctx, "key").Int64()
Expect(err).NotTo(HaveOccurred())
Expect(n).To(Equal(int64(10)))
un, err := client.Get(ctx, "key").Uint64()
Expect(err).NotTo(HaveOccurred())
Expect(un).To(Equal(uint64(10)))
f, err := client.Get(ctx, "key").Float64()
Expect(err).NotTo(HaveOccurred())
Expect(f).To(Equal(float64(10)))
})
It("supports float32", func() {
f := float32(66.97)
err := client.Set(ctx, "float_key", f, 0).Err()
Expect(err).NotTo(HaveOccurred())
val, err := client.Get(ctx, "float_key").Float32()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(Equal(f))
})
It("supports time.Time", func() {
tm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC)
err := client.Set(ctx, "time_key", tm, 0).Err()
Expect(err).NotTo(HaveOccurred())
s, err := client.Get(ctx, "time_key").Result()
Expect(err).NotTo(HaveOccurred())
Expect(s).To(Equal("2019-01-01T09:45:10.000222125Z"))
tm2, err := client.Get(ctx, "time_key").Time()
Expect(err).NotTo(HaveOccurred())
Expect(tm2).To(BeTemporally("==", tm))
})
It("allows to set custom error", func() {
e := errors.New("custom error")
cmd := redis.Cmd{}
cmd.SetErr(e)
_, err := cmd.Result()
Expect(err).To(Equal(e))
})
})

718
commands.go Normal file
View File

@ -0,0 +1,718 @@
package redis
import (
"context"
"encoding"
"errors"
"fmt"
"io"
"net"
"reflect"
"runtime"
"strings"
"time"
"github.com/redis/go-redis/v9/internal"
)
// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,
// otherwise you will receive an error: (error) ERR syntax error.
// For example:
//
// rdb.Set(ctx, key, value, redis.KeepTTL)
const KeepTTL = -1
func usePrecise(dur time.Duration) bool {
return dur < time.Second || dur%time.Second != 0
}
func formatMs(ctx context.Context, dur time.Duration) int64 {
if dur > 0 && dur < time.Millisecond {
internal.Logger.Printf(
ctx,
"specified duration is %s, but minimal supported value is %s - truncating to 1ms",
dur, time.Millisecond,
)
return 1
}
return int64(dur / time.Millisecond)
}
func formatSec(ctx context.Context, dur time.Duration) int64 {
if dur > 0 && dur < time.Second {
internal.Logger.Printf(
ctx,
"specified duration is %s, but minimal supported value is %s - truncating to 1s",
dur, time.Second,
)
return 1
}
return int64(dur / time.Second)
}
func appendArgs(dst, src []interface{}) []interface{} {
if len(src) == 1 {
return appendArg(dst, src[0])
}
dst = append(dst, src...)
return dst
}
func appendArg(dst []interface{}, arg interface{}) []interface{} {
switch arg := arg.(type) {
case []string:
for _, s := range arg {
dst = append(dst, s)
}
return dst
case []interface{}:
dst = append(dst, arg...)
return dst
case map[string]interface{}:
for k, v := range arg {
dst = append(dst, k, v)
}
return dst
case map[string]string:
for k, v := range arg {
dst = append(dst, k, v)
}
return dst
case time.Time, time.Duration, encoding.BinaryMarshaler, net.IP:
return append(dst, arg)
default:
// scan struct field
v := reflect.ValueOf(arg)
if v.Type().Kind() == reflect.Ptr {
if v.IsNil() {
// error: arg is not a valid object
return dst
}
v = v.Elem()
}
if v.Type().Kind() == reflect.Struct {
return appendStructField(dst, v)
}
return append(dst, arg)
}
}
// appendStructField appends the field and value held by the structure v to dst, and returns the appended dst.
func appendStructField(dst []interface{}, v reflect.Value) []interface{} {
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
tag := typ.Field(i).Tag.Get("redis")
if tag == "" || tag == "-" {
continue
}
name, opt, _ := strings.Cut(tag, ",")
if name == "" {
continue
}
field := v.Field(i)
// miss field
if omitEmpty(opt) && isEmptyValue(field) {
continue
}
if field.CanInterface() {
dst = append(dst, name, field.Interface())
}
}
return dst
}
func omitEmpty(opt string) bool {
for opt != "" {
var name string
name, opt, _ = strings.Cut(opt, ",")
if name == "omitempty" {
return true
}
}
return false
}
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Pointer:
return v.IsNil()
}
return false
}
type Cmdable interface {
Pipeline() Pipeliner
Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error)
TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error)
TxPipeline() Pipeliner
Command(ctx context.Context) *CommandsInfoCmd
CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd
CommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd
CommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd
ClientGetName(ctx context.Context) *StringCmd
Echo(ctx context.Context, message interface{}) *StringCmd
Ping(ctx context.Context) *StatusCmd
Quit(ctx context.Context) *StatusCmd
Unlink(ctx context.Context, keys ...string) *IntCmd
BgRewriteAOF(ctx context.Context) *StatusCmd
BgSave(ctx context.Context) *StatusCmd
ClientKill(ctx context.Context, ipPort string) *StatusCmd
ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd
ClientList(ctx context.Context) *StringCmd
ClientInfo(ctx context.Context) *ClientInfoCmd
ClientPause(ctx context.Context, dur time.Duration) *BoolCmd
ClientUnpause(ctx context.Context) *BoolCmd
ClientID(ctx context.Context) *IntCmd
ClientUnblock(ctx context.Context, id int64) *IntCmd
ClientUnblockWithError(ctx context.Context, id int64) *IntCmd
ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd
ConfigResetStat(ctx context.Context) *StatusCmd
ConfigSet(ctx context.Context, parameter, value string) *StatusCmd
ConfigRewrite(ctx context.Context) *StatusCmd
DBSize(ctx context.Context) *IntCmd
FlushAll(ctx context.Context) *StatusCmd
FlushAllAsync(ctx context.Context) *StatusCmd
FlushDB(ctx context.Context) *StatusCmd
FlushDBAsync(ctx context.Context) *StatusCmd
Info(ctx context.Context, section ...string) *StringCmd
LastSave(ctx context.Context) *IntCmd
Save(ctx context.Context) *StatusCmd
Shutdown(ctx context.Context) *StatusCmd
ShutdownSave(ctx context.Context) *StatusCmd
ShutdownNoSave(ctx context.Context) *StatusCmd
SlaveOf(ctx context.Context, host, port string) *StatusCmd
SlowLogGet(ctx context.Context, num int64) *SlowLogCmd
Time(ctx context.Context) *TimeCmd
DebugObject(ctx context.Context, key string) *StringCmd
MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd
ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd
ACLCmdable
BitMapCmdable
ClusterCmdable
GearsCmdable
GenericCmdable
GeoCmdable
HashCmdable
HyperLogLogCmdable
ListCmdable
ProbabilisticCmdable
PubSubCmdable
ScriptingFunctionsCmdable
SetCmdable
SortedSetCmdable
StringCmdable
StreamCmdable
TimeseriesCmdable
JSONCmdable
}
type StatefulCmdable interface {
Cmdable
Auth(ctx context.Context, password string) *StatusCmd
AuthACL(ctx context.Context, username, password string) *StatusCmd
Select(ctx context.Context, index int) *StatusCmd
SwapDB(ctx context.Context, index1, index2 int) *StatusCmd
ClientSetName(ctx context.Context, name string) *BoolCmd
ClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd
Hello(ctx context.Context, ver int, username, password, clientName string) *MapStringInterfaceCmd
}
var (
_ Cmdable = (*Client)(nil)
_ Cmdable = (*Tx)(nil)
_ Cmdable = (*Ring)(nil)
_ Cmdable = (*ClusterClient)(nil)
)
type cmdable func(ctx context.Context, cmd Cmder) error
type statefulCmdable func(ctx context.Context, cmd Cmder) error
//------------------------------------------------------------------------------
func (c statefulCmdable) Auth(ctx context.Context, password string) *StatusCmd {
cmd := NewStatusCmd(ctx, "auth", password)
_ = c(ctx, cmd)
return cmd
}
// AuthACL Perform an AUTH command, using the given user and pass.
// Should be used to authenticate the current connection with one of the connections defined in the ACL list
// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
func (c statefulCmdable) AuthACL(ctx context.Context, username, password string) *StatusCmd {
cmd := NewStatusCmd(ctx, "auth", username, password)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Wait(ctx context.Context, numSlaves int, timeout time.Duration) *IntCmd {
cmd := NewIntCmd(ctx, "wait", numSlaves, int(timeout/time.Millisecond))
cmd.setReadTimeout(timeout)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) WaitAOF(ctx context.Context, numLocal, numSlaves int, timeout time.Duration) *IntCmd {
cmd := NewIntCmd(ctx, "waitAOF", numLocal, numSlaves, int(timeout/time.Millisecond))
cmd.setReadTimeout(timeout)
_ = c(ctx, cmd)
return cmd
}
func (c statefulCmdable) Select(ctx context.Context, index int) *StatusCmd {
cmd := NewStatusCmd(ctx, "select", index)
_ = c(ctx, cmd)
return cmd
}
func (c statefulCmdable) SwapDB(ctx context.Context, index1, index2 int) *StatusCmd {
cmd := NewStatusCmd(ctx, "swapdb", index1, index2)
_ = c(ctx, cmd)
return cmd
}
// ClientSetName assigns a name to the connection.
func (c statefulCmdable) ClientSetName(ctx context.Context, name string) *BoolCmd {
cmd := NewBoolCmd(ctx, "client", "setname", name)
_ = c(ctx, cmd)
return cmd
}
// ClientSetInfo sends a CLIENT SETINFO command with the provided info.
func (c statefulCmdable) ClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd {
err := info.Validate()
if err != nil {
panic(err.Error())
}
var cmd *StatusCmd
if info.LibName != nil {
libName := fmt.Sprintf("go-redis(%s,%s)", *info.LibName, internal.ReplaceSpaces(runtime.Version()))
cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-NAME", libName)
} else {
cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-VER", *info.LibVer)
}
_ = c(ctx, cmd)
return cmd
}
// Validate checks if only one field in the struct is non-nil.
func (info LibraryInfo) Validate() error {
if info.LibName != nil && info.LibVer != nil {
return errors.New("both LibName and LibVer cannot be set at the same time")
}
if info.LibName == nil && info.LibVer == nil {
return errors.New("at least one of LibName and LibVer should be set")
}
return nil
}
// Hello Set the resp protocol used.
func (c statefulCmdable) Hello(ctx context.Context,
ver int, username, password, clientName string,
) *MapStringInterfaceCmd {
args := make([]interface{}, 0, 7)
args = append(args, "hello", ver)
if password != "" {
if username != "" {
args = append(args, "auth", username, password)
} else {
args = append(args, "auth", "default", password)
}
}
if clientName != "" {
args = append(args, "setname", clientName)
}
cmd := NewMapStringInterfaceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
//------------------------------------------------------------------------------
func (c cmdable) Command(ctx context.Context) *CommandsInfoCmd {
cmd := NewCommandsInfoCmd(ctx, "command")
_ = c(ctx, cmd)
return cmd
}
// FilterBy is used for the `CommandList` command parameter.
type FilterBy struct {
Module string
ACLCat string
Pattern string
}
func (c cmdable) CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd {
args := make([]interface{}, 0, 5)
args = append(args, "command", "list")
if filter != nil {
if filter.Module != "" {
args = append(args, "filterby", "module", filter.Module)
} else if filter.ACLCat != "" {
args = append(args, "filterby", "aclcat", filter.ACLCat)
} else if filter.Pattern != "" {
args = append(args, "filterby", "pattern", filter.Pattern)
}
}
cmd := NewStringSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) CommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd {
args := make([]interface{}, 2+len(commands))
args[0] = "command"
args[1] = "getkeys"
copy(args[2:], commands)
cmd := NewStringSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) CommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd {
args := make([]interface{}, 2+len(commands))
args[0] = "command"
args[1] = "getkeysandflags"
copy(args[2:], commands)
cmd := NewKeyFlagsCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ClientGetName returns the name of the connection.
func (c cmdable) ClientGetName(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "client", "getname")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Echo(ctx context.Context, message interface{}) *StringCmd {
cmd := NewStringCmd(ctx, "echo", message)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Ping(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "ping")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Quit(_ context.Context) *StatusCmd {
panic("not implemented")
}
//------------------------------------------------------------------------------
func (c cmdable) BgRewriteAOF(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "bgrewriteaof")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) BgSave(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "bgsave")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientKill(ctx context.Context, ipPort string) *StatusCmd {
cmd := NewStatusCmd(ctx, "client", "kill", ipPort)
_ = c(ctx, cmd)
return cmd
}
// ClientKillByFilter is new style syntax, while the ClientKill is old
//
// CLIENT KILL <option> [value] ... <option> [value]
func (c cmdable) ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd {
args := make([]interface{}, 2+len(keys))
args[0] = "client"
args[1] = "kill"
for i, key := range keys {
args[2+i] = key
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientList(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "client", "list")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientPause(ctx context.Context, dur time.Duration) *BoolCmd {
cmd := NewBoolCmd(ctx, "client", "pause", formatMs(ctx, dur))
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientUnpause(ctx context.Context) *BoolCmd {
cmd := NewBoolCmd(ctx, "client", "unpause")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientID(ctx context.Context) *IntCmd {
cmd := NewIntCmd(ctx, "client", "id")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientUnblock(ctx context.Context, id int64) *IntCmd {
cmd := NewIntCmd(ctx, "client", "unblock", id)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientUnblockWithError(ctx context.Context, id int64) *IntCmd {
cmd := NewIntCmd(ctx, "client", "unblock", id, "error")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClientInfo(ctx context.Context) *ClientInfoCmd {
cmd := NewClientInfoCmd(ctx, "client", "info")
_ = c(ctx, cmd)
return cmd
}
// ------------------------------------------------------------------------------------------------
func (c cmdable) ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd {
cmd := NewMapStringStringCmd(ctx, "config", "get", parameter)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ConfigResetStat(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "config", "resetstat")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ConfigSet(ctx context.Context, parameter, value string) *StatusCmd {
cmd := NewStatusCmd(ctx, "config", "set", parameter, value)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ConfigRewrite(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "config", "rewrite")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) DBSize(ctx context.Context) *IntCmd {
cmd := NewIntCmd(ctx, "dbsize")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) FlushAll(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "flushall")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) FlushAllAsync(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "flushall", "async")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) FlushDB(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "flushdb")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) FlushDBAsync(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "flushdb", "async")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Info(ctx context.Context, sections ...string) *StringCmd {
args := make([]interface{}, 1+len(sections))
args[0] = "info"
for i, section := range sections {
args[i+1] = section
}
cmd := NewStringCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) InfoMap(ctx context.Context, sections ...string) *InfoCmd {
args := make([]interface{}, 1+len(sections))
args[0] = "info"
for i, section := range sections {
args[i+1] = section
}
cmd := NewInfoCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) LastSave(ctx context.Context) *IntCmd {
cmd := NewIntCmd(ctx, "lastsave")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Save(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "save")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) shutdown(ctx context.Context, modifier string) *StatusCmd {
var args []interface{}
if modifier == "" {
args = []interface{}{"shutdown"}
} else {
args = []interface{}{"shutdown", modifier}
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
if err := cmd.Err(); err != nil {
if err == io.EOF {
// Server quit as expected.
cmd.err = nil
}
} else {
// Server did not quit. String reply contains the reason.
cmd.err = errors.New(cmd.val)
cmd.val = ""
}
return cmd
}
func (c cmdable) Shutdown(ctx context.Context) *StatusCmd {
return c.shutdown(ctx, "")
}
func (c cmdable) ShutdownSave(ctx context.Context) *StatusCmd {
return c.shutdown(ctx, "save")
}
func (c cmdable) ShutdownNoSave(ctx context.Context) *StatusCmd {
return c.shutdown(ctx, "nosave")
}
func (c cmdable) SlaveOf(ctx context.Context, host, port string) *StatusCmd {
cmd := NewStatusCmd(ctx, "slaveof", host, port)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) SlowLogGet(ctx context.Context, num int64) *SlowLogCmd {
cmd := NewSlowLogCmd(context.Background(), "slowlog", "get", num)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Sync(_ context.Context) {
panic("not implemented")
}
func (c cmdable) Time(ctx context.Context) *TimeCmd {
cmd := NewTimeCmd(ctx, "time")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) DebugObject(ctx context.Context, key string) *StringCmd {
cmd := NewStringCmd(ctx, "debug", "object", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd {
args := []interface{}{"memory", "usage", key}
if len(samples) > 0 {
if len(samples) != 1 {
panic("MemoryUsage expects single sample count")
}
args = append(args, "SAMPLES", samples[0])
}
cmd := NewIntCmd(ctx, args...)
cmd.SetFirstKeyPos(2)
_ = c(ctx, cmd)
return cmd
}
//------------------------------------------------------------------------------
// ModuleLoadexConfig struct is used to specify the arguments for the MODULE LOADEX command of redis.
// `MODULE LOADEX path [CONFIG name value [CONFIG name value ...]] [ARGS args [args ...]]`
type ModuleLoadexConfig struct {
Path string
Conf map[string]interface{}
Args []interface{}
}
func (c *ModuleLoadexConfig) toArgs() []interface{} {
args := make([]interface{}, 3, 3+len(c.Conf)*3+len(c.Args)*2)
args[0] = "MODULE"
args[1] = "LOADEX"
args[2] = c.Path
for k, v := range c.Conf {
args = append(args, "CONFIG", k, v)
}
for _, arg := range c.Args {
args = append(args, "ARGS", arg)
}
return args
}
// ModuleLoadex Redis `MODULE LOADEX path [CONFIG name value [CONFIG name value ...]] [ARGS args [args ...]]` command.
func (c cmdable) ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd {
cmd := NewStringCmd(ctx, conf.toArgs()...)
_ = c(ctx, cmd)
return cmd
}
/*
Monitor - represents a Redis MONITOR command, allowing the user to capture
and process all commands sent to a Redis server. This mimics the behavior of
MONITOR in the redis-cli.
Notes:
- Using MONITOR blocks the connection to the server for itself. It needs a dedicated connection
- The user should create a channel of type string
- This runs concurrently in the background. Trigger via the Start and Stop functions
See further: Redis MONITOR command: https://redis.io/commands/monitor
*/
func (c cmdable) Monitor(ctx context.Context, ch chan string) *MonitorCmd {
cmd := newMonitorCmd(ctx, ch)
_ = c(ctx, cmd)
return cmd
}

7216
commands_test.go Normal file

File diff suppressed because it is too large Load Diff

4
doc.go Normal file
View File

@ -0,0 +1,4 @@
/*
Package redis implements a Redis client.
*/
package redis

22
doctests/README.md Normal file
View File

@ -0,0 +1,22 @@
# Command examples for redis.io
These examples appear on the [Redis documentation](https://redis.io) site as part of the tabbed examples interface.
## How to add examples
- Create a Go test file with a meaningful name in the current folder.
- Create a single method prefixed with `Example` and write your test in it.
- Determine the id for the example you're creating and add it as the first line of the file: `// EXAMPLE: set_and_get`.
- We're using the [Testable Examples](https://go.dev/blog/examples) feature of Go to test the desired output has been written to stdout.
### Special markup
See https://github.com/redis-stack/redis-stack-website#readme for more details.
## How to test the examples
- Start a Redis server locally on port 6379
- CD into the `doctests` directory
- Run `go test` to test all examples in the directory.
- Run `go test filename.go` to test a single file

View File

@ -0,0 +1,48 @@
// EXAMPLE: lpush_and_lrange
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func ExampleClient_LPush_and_lrange() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// HIDE_END
// REMOVE_START
errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test
if errFlush != nil {
panic(errFlush)
}
// REMOVE_END
listSize, err := rdb.LPush(ctx, "my_bikes", "bike:1", "bike:2").Result()
if err != nil {
panic(err)
}
fmt.Println(listSize)
value, err := rdb.LRange(ctx, "my_bikes", 0, -1).Result()
if err != nil {
panic(err)
}
fmt.Println(value)
// HIDE_START
// Output: 2
// [bike:2 bike:1]
}
// HIDE_END

48
doctests/set_get_test.go Normal file
View File

@ -0,0 +1,48 @@
// EXAMPLE: set_and_get
// HIDE_START
package example_commands_test
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func ExampleClient_Set_and_get() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})
// HIDE_END
// REMOVE_START
errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test
if errFlush != nil {
panic(errFlush)
}
// REMOVE_END
err := rdb.Set(ctx, "bike:1", "Process 134", 0).Err()
if err != nil {
panic(err)
}
fmt.Println("OK")
value, err := rdb.Get(ctx, "bike:1").Result()
if err != nil {
panic(err)
}
fmt.Printf("The name of the bike is %s", value)
// HIDE_START
// Output: OK
// The name of the bike is Process 134
}
// HIDE_END

159
error.go Normal file
View File

@ -0,0 +1,159 @@
package redis
import (
"context"
"errors"
"io"
"net"
"strings"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
)
// ErrClosed performs any operation on the closed client will return this error.
var ErrClosed = pool.ErrClosed
// HasErrorPrefix checks if the err is a Redis error and the message contains a prefix.
func HasErrorPrefix(err error, prefix string) bool {
var rErr Error
if !errors.As(err, &rErr) {
return false
}
msg := rErr.Error()
msg = strings.TrimPrefix(msg, "ERR ") // KVRocks adds such prefix
return strings.HasPrefix(msg, prefix)
}
type Error interface {
error
// RedisError is a no-op function but
// serves to distinguish types that are Redis
// errors from ordinary errors: a type is a
// Redis error if it has a RedisError method.
RedisError()
}
var _ Error = proto.RedisError("")
func shouldRetry(err error, retryTimeout bool) bool {
switch err {
case io.EOF, io.ErrUnexpectedEOF:
return true
case nil, context.Canceled, context.DeadlineExceeded:
return false
}
if v, ok := err.(timeoutError); ok {
if v.Timeout() {
return retryTimeout
}
return true
}
s := err.Error()
if s == "ERR max number of clients reached" {
return true
}
if strings.HasPrefix(s, "LOADING ") {
return true
}
if strings.HasPrefix(s, "READONLY ") {
return true
}
if strings.HasPrefix(s, "CLUSTERDOWN ") {
return true
}
if strings.HasPrefix(s, "TRYAGAIN ") {
return true
}
return false
}
func isRedisError(err error) bool {
_, ok := err.(proto.RedisError)
return ok
}
func isBadConn(err error, allowTimeout bool, addr string) bool {
switch err {
case nil:
return false
case context.Canceled, context.DeadlineExceeded:
return true
}
if isRedisError(err) {
switch {
case isReadOnlyError(err):
// Close connections in read only state in case domain addr is used
// and domain resolves to a different Redis Server. See #790.
return true
case isMovedSameConnAddr(err, addr):
// Close connections when we are asked to move to the same addr
// of the connection. Force a DNS resolution when all connections
// of the pool are recycled
return true
default:
return false
}
}
if allowTimeout {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return false
}
}
return true
}
func isMovedError(err error) (moved bool, ask bool, addr string) {
if !isRedisError(err) {
return
}
s := err.Error()
switch {
case strings.HasPrefix(s, "MOVED "):
moved = true
case strings.HasPrefix(s, "ASK "):
ask = true
default:
return
}
ind := strings.LastIndex(s, " ")
if ind == -1 {
return false, false, ""
}
addr = s[ind+1:]
addr = internal.GetAddr(addr)
return
}
func isLoadingError(err error) bool {
return strings.HasPrefix(err.Error(), "LOADING ")
}
func isReadOnlyError(err error) bool {
return strings.HasPrefix(err.Error(), "READONLY ")
}
func isMovedSameConnAddr(err error, addr string) bool {
redisError := err.Error()
if !strings.HasPrefix(redisError, "MOVED ") {
return false
}
return strings.HasSuffix(redisError, " "+addr)
}
//------------------------------------------------------------------------------
type timeoutError interface {
Timeout() bool
}

View File

@ -0,0 +1,11 @@
# Delete keys without a ttl
This example demonstrates how to use `SCAN` and pipelines to efficiently delete keys without a TTL.
To run this example:
```shell
go run .
```
See [documentation](https://redis.uptrace.dev/guide/get-all-keys.html) for more details.

View File

@ -0,0 +1,17 @@
module github.com/redis/go-redis/example/del-keys-without-ttl
go 1.18
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/redis/go-redis/v9 v9.5.3
go.uber.org/zap v1.24.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
)

View File

@ -0,0 +1,19 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -0,0 +1,155 @@
package main
import (
"context"
"fmt"
"sync"
"time"
"go.uber.org/zap"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.Set(ctx, "key_with_ttl", "bar", time.Minute).Err()
_ = rdb.Set(ctx, "key_without_ttl_1", "", 0).Err()
_ = rdb.Set(ctx, "key_without_ttl_2", "", 0).Err()
checker := NewKeyChecker(rdb, 100)
start := time.Now()
checker.Start(ctx)
iter := rdb.Scan(ctx, 0, "", 0).Iterator()
for iter.Next(ctx) {
checker.Add(iter.Val())
}
if err := iter.Err(); err != nil {
panic(err)
}
deleted := checker.Stop()
fmt.Println("deleted", deleted, "keys", "in", time.Since(start))
}
type KeyChecker struct {
rdb *redis.Client
batchSize int
ch chan string
delCh chan string
wg sync.WaitGroup
deleted int
logger *zap.Logger
}
func NewKeyChecker(rdb *redis.Client, batchSize int) *KeyChecker {
return &KeyChecker{
rdb: rdb,
batchSize: batchSize,
ch: make(chan string, batchSize),
delCh: make(chan string, batchSize),
logger: zap.L(),
}
}
func (c *KeyChecker) Add(key string) {
c.ch <- key
}
func (c *KeyChecker) Start(ctx context.Context) {
c.wg.Add(1)
go func() {
defer c.wg.Done()
if err := c.del(ctx); err != nil {
panic(err)
}
}()
c.wg.Add(1)
go func() {
defer c.wg.Done()
defer close(c.delCh)
keys := make([]string, 0, c.batchSize)
for key := range c.ch {
keys = append(keys, key)
if len(keys) < cap(keys) {
continue
}
if err := c.checkKeys(ctx, keys); err != nil {
c.logger.Error("checkKeys failed", zap.Error(err))
}
keys = keys[:0]
}
if len(keys) > 0 {
if err := c.checkKeys(ctx, keys); err != nil {
c.logger.Error("checkKeys failed", zap.Error(err))
}
keys = nil
}
}()
}
func (c *KeyChecker) Stop() int {
close(c.ch)
c.wg.Wait()
return c.deleted
}
func (c *KeyChecker) checkKeys(ctx context.Context, keys []string) error {
cmds, err := c.rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for _, key := range keys {
pipe.TTL(ctx, key)
}
return nil
})
if err != nil {
return err
}
for i, cmd := range cmds {
d, err := cmd.(*redis.DurationCmd).Result()
if err != nil {
return err
}
if d == -1 {
c.delCh <- keys[i]
}
}
return nil
}
func (c *KeyChecker) del(ctx context.Context) error {
pipe := c.rdb.Pipeline()
for key := range c.delCh {
fmt.Printf("deleting %s...\n", key)
pipe.Del(ctx, key)
c.deleted++
if pipe.Len() < c.batchSize {
continue
}
if _, err := pipe.Exec(ctx); err != nil {
return err
}
}
if _, err := pipe.Exec(ctx); err != nil {
return err
}
return nil
}

10
example/hll/README.md Normal file
View File

@ -0,0 +1,10 @@
# Redis HyperLogLog example
To run this example:
```shell
go run .
```
See [Using HyperLogLog command with go-redis](https://redis.uptrace.dev/guide/go-redis-hll.html) for
details.

12
example/hll/go.mod Normal file
View File

@ -0,0 +1,12 @@
module github.com/redis/go-redis/example/hll
go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.5.3
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

6
example/hll/go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

30
example/hll/main.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.FlushDB(ctx).Err()
for i := 0; i < 10; i++ {
if err := rdb.PFAdd(ctx, "myset", fmt.Sprint(i)).Err(); err != nil {
panic(err)
}
}
card, err := rdb.PFCount(ctx, "myset").Result()
if err != nil {
panic(err)
}
fmt.Println("set cardinality", card)
}

View File

@ -0,0 +1,8 @@
# Redis Lua scripting example
This is an example for [Redis Lua scripting](https://redis.uptrace.dev/guide/lua-scripting.html)
article. To run it:
```shell
go run .
```

View File

@ -0,0 +1,12 @@
module github.com/redis/go-redis/example/lua-scripting
go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.5.3
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

View File

@ -0,0 +1,6 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

View File

@ -0,0 +1,66 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.FlushDB(ctx).Err()
fmt.Printf("# INCR BY\n")
for _, change := range []int{+1, +5, 0} {
num, err := incrBy.Run(ctx, rdb, []string{"my_counter"}, change).Int()
if err != nil {
panic(err)
}
fmt.Printf("incr by %d: %d\n", change, num)
}
fmt.Printf("\n# SUM\n")
sum, err := sum.Run(ctx, rdb, []string{"my_sum"}, 1, 2, 3).Int()
if err != nil {
panic(err)
}
fmt.Printf("sum is: %d\n", sum)
}
var incrBy = redis.NewScript(`
local key = KEYS[1]
local change = ARGV[1]
local value = redis.call("GET", key)
if not value then
value = 0
end
value = value + change
redis.call("SET", key, value)
return value
`)
var sum = redis.NewScript(`
local key = KEYS[1]
local sum = redis.call("GET", key)
if not sum then
sum = 0
end
local num_arg = #ARGV
for i = 1, num_arg do
sum = sum + ARGV[i]
end
redis.call("SET", key, sum)
return sum
`)

59
example/otel/README.md Normal file
View File

@ -0,0 +1,59 @@
# Example for go-redis OpenTelemetry instrumentation
This example demonstrates how to monitor Redis using OpenTelemetry and
[Uptrace](https://github.com/uptrace/uptrace). It requires Docker to start Redis Server and Uptrace.
See
[Monitoring Go Redis Performance and Errors](https://redis.uptrace.dev/guide/go-redis-monitoring.html)
for details.
**Step 1**. Download the example using Git:
```shell
git clone https://github.com/redis/go-redis.git
cd example/otel
```
**Step 2**. Start the services using Docker:
```shell
docker-compose up -d
```
**Step 3**. Make sure Uptrace is running:
```shell
docker-compose logs uptrace
```
**Step 4**. Run the Redis client example and Follow the link to view the trace:
```shell
go run client.go
trace: http://localhost:14318/traces/ee029d8782242c8ed38b16d961093b35
```
![Redis trace](./image/redis-trace.png)
You can also open Uptrace UI at [http://localhost:14318](http://localhost:14318) to view available
spans, logs, and metrics.
## Redis monitoring
You can also [monitor Redis performance](https://uptrace.dev/opentelemetry/redis-monitoring.html)
metrics By installing OpenTelemetry Collector.
[OpenTelemetry Collector](https://uptrace.dev/opentelemetry/collector.html) is an agent that pulls
telemetry data from systems you want to monitor and sends it to APM tools using the OpenTelemetry
protocol (OTLP).
When telemetry data reaches Uptrace, it automatically generates a Redis dashboard from a pre-defined
template.
![Redis dashboard](./image/metrics.png)
## Links
- [Uptrace open-source APM](https://uptrace.dev/get/open-source-apm.html)
- [OpenTelemetry Go instrumentations](https://uptrace.dev/opentelemetry/instrumentations/?lang=go)
- [OpenTelemetry Go Tracing API](https://uptrace.dev/opentelemetry/go-tracing.html)

91
example/otel/client.go Normal file
View File

@ -0,0 +1,91 @@
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/uptrace/uptrace-go/uptrace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"github.com/redis/go-redis/extra/redisotel/v9"
"github.com/redis/go-redis/v9"
)
var tracer = otel.Tracer("github.com/redis/go-redis/example/otel")
func main() {
ctx := context.Background()
uptrace.ConfigureOpentelemetry(
// copy your project DSN here or use UPTRACE_DSN env var
uptrace.WithDSN("http://project2_secret_token@localhost:14317/2"),
uptrace.WithServiceName("myservice"),
uptrace.WithServiceVersion("v1.0.0"),
)
defer uptrace.Shutdown(ctx)
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
if err := redisotel.InstrumentTracing(rdb); err != nil {
panic(err)
}
if err := redisotel.InstrumentMetrics(rdb); err != nil {
panic(err)
}
for i := 0; i < 1e6; i++ {
ctx, rootSpan := tracer.Start(ctx, "handleRequest")
if err := handleRequest(ctx, rdb); err != nil {
rootSpan.RecordError(err)
rootSpan.SetStatus(codes.Error, err.Error())
}
rootSpan.End()
if i == 0 {
fmt.Printf("view trace: %s\n", uptrace.TraceURL(rootSpan))
}
time.Sleep(time.Second)
}
}
func handleRequest(ctx context.Context, rdb *redis.Client) error {
if err := rdb.Set(ctx, "First value", "value_1", 0).Err(); err != nil {
return err
}
if err := rdb.Set(ctx, "Second value", "value_2", 0).Err(); err != nil {
return err
}
var group sync.WaitGroup
for i := 0; i < 20; i++ {
group.Add(1)
go func() {
defer group.Done()
val := rdb.Get(ctx, "Second value").Val()
if val != "value_2" {
log.Printf("%q != %q", val, "value_2")
}
}()
}
group.Wait()
if err := rdb.Del(ctx, "First value").Err(); err != nil {
return err
}
if err := rdb.Del(ctx, "Second value").Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,68 @@
extensions:
health_check:
pprof:
endpoint: 0.0.0.0:1777
zpages:
endpoint: 0.0.0.0:55679
receivers:
otlp:
protocols:
grpc:
http:
hostmetrics:
collection_interval: 10s
scrapers:
cpu:
disk:
load:
filesystem:
memory:
network:
paging:
redis:
endpoint: 'redis-server:6379'
collection_interval: 10s
jaeger:
protocols:
grpc:
processors:
resourcedetection:
detectors: ['system']
cumulativetodelta:
batch:
send_batch_size: 10000
timeout: 10s
exporters:
otlp/uptrace:
endpoint: http://uptrace:14317
tls:
insecure: true
headers: { 'uptrace-dsn': 'http://project2_secret_token@localhost:14317/2' }
debug:
service:
# telemetry:
# logs:
# level: DEBUG
pipelines:
traces:
receivers: [otlp, jaeger]
processors: [batch]
exporters: [otlp/uptrace]
metrics:
receivers: [otlp]
processors: [cumulativetodelta, batch]
exporters: [otlp/uptrace]
metrics/hostmetrics:
receivers: [hostmetrics, redis]
processors: [cumulativetodelta, batch, resourcedetection]
exporters: [otlp/uptrace]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp/uptrace]
extensions: [health_check, pprof, zpages]

View File

@ -0,0 +1,35 @@
[sources.syslog_logs]
type = "demo_logs"
format = "syslog"
[sources.apache_common_logs]
type = "demo_logs"
format = "apache_common"
[sources.apache_error_logs]
type = "demo_logs"
format = "apache_error"
[sources.json_logs]
type = "demo_logs"
format = "json"
# Parse Syslog logs
# See the Vector Remap Language reference for more info: https://vrl.dev
[transforms.parse_logs]
type = "remap"
inputs = ["syslog_logs"]
source = '''
. = parse_syslog!(string!(.message))
'''
# Export data to Uptrace.
[sinks.uptrace]
type = "http"
inputs = ["parse_logs", "apache_common_logs", "apache_error_logs", "json_logs"]
encoding.codec = "json"
framing.method = "newline_delimited"
compression = "gzip"
uri = "http://uptrace:14318/api/v1/vector/logs"
#uri = "https://api.uptrace.dev/api/v1/vector/logs"
headers.uptrace-dsn = "http://project2_secret_token@localhost:14317/2"

View File

@ -0,0 +1,82 @@
version: '3'
services:
clickhouse:
image: clickhouse/clickhouse-server:23.7
restart: on-failure
environment:
CLICKHOUSE_DB: uptrace
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'localhost:8123/ping']
interval: 1s
timeout: 1s
retries: 30
volumes:
- ch_data2:/var/lib/clickhouse
ports:
- '8123:8123'
- '9000:9000'
postgres:
image: postgres:15-alpine
restart: on-failure
environment:
PGDATA: /var/lib/postgresql/data/pgdata
POSTGRES_USER: uptrace
POSTGRES_PASSWORD: uptrace
POSTGRES_DB: uptrace
healthcheck:
test: ['CMD-SHELL', 'pg_isready', '-U', 'uptrace', '-d', 'uptrace']
interval: 1s
timeout: 1s
retries: 30
volumes:
- 'pg_data2:/var/lib/postgresql/data/pgdata'
ports:
- '5432:5432'
uptrace:
image: 'uptrace/uptrace:1.6.2'
#image: 'uptrace/uptrace-dev:latest'
restart: on-failure
volumes:
- ./uptrace.yml:/etc/uptrace/uptrace.yml
#environment:
# - DEBUG=2
ports:
- '14317:14317'
- '14318:14318'
depends_on:
clickhouse:
condition: service_healthy
otelcol:
image: otel/opentelemetry-collector-contrib:0.91.0
restart: on-failure
volumes:
- ./config/otel-collector.yaml:/etc/otelcol-contrib/config.yaml
ports:
- '4317:4317'
- '4318:4318'
vector:
image: timberio/vector:0.28.X-alpine
volumes:
- ./config/vector.toml:/etc/vector/vector.toml:ro
mailhog:
image: mailhog/mailhog:v1.0.1
restart: on-failure
ports:
- '8025:8025'
redis-server:
image: redis
ports:
- '6379:6379'
redis-cli:
image: redis
volumes:
ch_data2:
pg_data2:

45
example/otel/go.mod Normal file
View File

@ -0,0 +1,45 @@
module github.com/redis/go-redis/example/otel
go 1.19
replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd
require (
github.com/redis/go-redis/extra/redisotel/v9 v9.5.3
github.com/redis/go-redis/v9 v9.5.3
github.com/uptrace/uptrace-go v1.21.0
go.opentelemetry.io/otel v1.22.0
)
require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/sdk v1.22.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/grpc v1.60.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)

68
example/otel/go.sum Normal file
View File

@ -0,0 +1,68 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/uptrace/uptrace-go v1.21.0 h1:oJoUjhiVT7aiuoG6B3ClVHtJozLn3cK9hQt8U5dQO1M=
github.com/uptrace/uptrace-go v1.21.0/go.mod h1:/aXAFGKOqeAFBqWa1xtzLnGX2xJm1GScqz9NJ0TJjLM=
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 h1:m9ReioVPIffxjJlGNRd0d5poy+9oTro3D+YbiEzUDOc=
go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1/go.mod h1:CANkrsXNzqOKXfOomu2zhOmc1/J5UZK9SGjrat6ZCG0=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs=
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo=
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

265
example/otel/uptrace.yml Normal file
View File

@ -0,0 +1,265 @@
##
## Uptrace configuration file.
## See https://uptrace.dev/get/config.html for details.
##
## You can use environment variables anywhere in this file, for example:
##
## foo: $FOO
## bar: ${BAR}
## baz: ${BAZ:default}
##
## To escape `$`, use `$$`, for example:
##
## foo: $$FOO_BAR
##
##
## ClickHouse database credentials.
##
ch:
addr: clickhouse:9000
user: default
password:
database: uptrace
# Maximum query execution time.
max_execution_time: 30s
# TLS configuration. Uncomment to enable.
# tls:
# insecure_skip_verify: true
# TLS configuration. Uncomment to enable.
# tls:
# insecure_skip_verify: true # only for self-signed certificates
##
## PostgreSQL db that is used to store metadata such us metric names, dashboards, alerts,
## and so on.
##
pg:
addr: postgres:5432
user: uptrace
password: uptrace
database: uptrace
##
## A list of pre-configured projects. Each project is fully isolated.
##
projects:
# Conventionally, the first project is used to monitor Uptrace itself.
- id: 1
name: Uptrace
# Token grants write access to the project. Keep a secret.
token: project1_secret_token
pinned_attrs:
- service_name
- host_name
- deployment_environment
# Group spans by deployment.environment attribute.
group_by_env: false
# Group funcs spans by service.name attribute.
group_funcs_by_service: false
# Enable prom_compat if you want to use the project as a Prometheus datasource in Grafana.
prom_compat: true
# Other projects can be used to monitor your applications.
# To monitor micro-services or multiple related services, use a single project.
- id: 2
name: My project
token: project2_secret_token
pinned_attrs:
- service_name
- host_name
- deployment_environment
group_by_env: false
group_funcs_by_service: false
prom_compat: true
##
## Create metrics from spans and events.
##
metrics_from_spans:
- name: uptrace.tracing.spans
description: Spans duration (excluding events)
instrument: histogram
unit: microseconds
value: _duration / 1000
attrs:
- _system
- _group_id
- service_name
- host_name
- _status_code
annotations:
- display_name
where: _event_name = ''
- name: uptrace.tracing.events
description: Events count (excluding spans)
instrument: counter
unit: 1
value: _count
attrs:
- _system
- _group_id
- _name
- host_name
annotations:
- display_name
where: _is_event = 1
##
## To require authentication, uncomment one of the following sections.
##
auth:
users:
- name: Anonymous
email: uptrace@localhost
password: uptrace
notify_by_email: true
# Cloudflare Zero Trust Access (Identity)
# See https://developers.cloudflare.com/cloudflare-one/identity/ for more info.
# cloudflare:
# # The base URL of the Cloudflare Zero Trust team.
# - team_url: https://myteam.cloudflareaccess.com
# # The Application Audience (AUD) Tag for this application.
# # You can retrieve this from the Cloudflare Zero Trust 'Access' Dashboard.
# audience: bea6df23b944e4a0cd178609ba1bb64dc98dfe1f66ae7b918e563f6cf28b37e0
# OpenID Connect (Single Sign-On)
oidc:
# # The ID is used in API endpoints, for example, in redirect URL
# # `http://<uptrace-host>/api/v1/sso/<oidc-id>/callback`.
# - id: keycloak
# # Display name for the button in the login form.
# # Default to 'OpenID Connect'
# display_name: Keycloak
# # The base URL for the OIDC provider.
# issuer_url: http://localhost:8080/realms/uptrace
# # The OAuth 2.0 Client ID
# client_id: uptrace
# # The OAuth 2.0 Client Secret
# client_secret: ogbhd8Q0X0e5AZFGSG3m9oirPvnetqkA
# # Additional OAuth 2.0 scopes to request from the OIDC provider.
# # Defaults to 'profile'. 'openid' is requested by default and need not be specified.
# scopes:
# - profile
##
## Various options to tweak ClickHouse schema.
## For changes to take effect, you need reset the ClickHouse database with `ch reset`.
##
ch_schema:
# Compression codec, for example, LZ4, ZSTD(3), or Default.
compression: ZSTD(3)
# Whether to use ReplicatedMergeTree instead of MergeTree.
replicated: false
# Cluster name for Distributed tables and ON CLUSTER clause.
#cluster: uptrace1
spans:
# Delete spans data after 30 days.
ttl_delete: 7 DAY
storage_policy: 'default'
metrics:
# Delete metrics data after 90 days.
ttl_delete: 30 DAY
storage_policy: 'default'
##
## Addresses on which Uptrace receives gRPC and HTTP requests.
##
listen:
# OTLP/gRPC API.
grpc:
addr: ':14317'
# OTLP/HTTP API and Uptrace API with UI.
http:
addr: ':14318'
# tls:
# cert_file: config/tls/uptrace.crt
# key_file: config/tls/uptrace.key
##
## Various options for Uptrace UI.
##
site:
# Overrides public URL for Vue-powered UI in case you put Uptrace behind a proxy.
#addr: 'https://uptrace.mydomain.com'
##
## Spans processing options.
##
spans:
# The size of the Go chan used to buffer incoming spans.
# If the buffer is full, Uptrace starts to drop spans.
#buffer_size: 100000
# The number of spans to insert in a single query.
#batch_size: 10000
##
## Metrics processing options.
##
metrics:
# List of attributes to drop for being noisy.
drop_attrs:
- telemetry_sdk_language
- telemetry_sdk_name
- telemetry_sdk_version
# The size of the Go chan used to buffer incoming measures.
# If the buffer is full, Uptrace starts to drop measures.
#buffer_size: 100000
# The number of measures to insert in a single query.
#batch_size: 10000
##
## uptrace-go client configuration.
## Uptrace sends internal telemetry here. Defaults to listen.grpc.addr.
##
uptrace_go:
# Enabled by default.
#disabled: true
# Defaults to the first projects.
# dsn: http://project1_secret_token@localhost:14317/1
# tls:
# cert_file: config/tls/uptrace.crt
# key_file: config/tls/uptrace.key
# insecure_skip_verify: true
##
## SMTP settings to send emails.
## https://uptrace.dev/get/alerting.html
##
smtp_mailer:
enabled: true
host: mailhog
port: 1025
username: mailhog
password: mailhog
from: 'uptrace@localhost'
##
## Logging configuration.
##
logging:
# Zap minimal logging level.
# Valid values: DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL.
level: INFO
# Secret key that is used to sign JWT tokens etc.
secret_key: 102c1a557c314fc28198acd017960843
# Enable to log HTTP requests and database queries.
debug: false

View File

@ -0,0 +1,12 @@
# RedisBloom example for go-redis
This is an example for
[Bloom, Cuckoo, Count-Min, Top-K](https://redis.uptrace.dev/guide/bloom-cuckoo-count-min-top-k.html)
article.
To run it, you need to compile and install
[RedisBloom](https://oss.redis.com/redisbloom/Quick_Start/#building) module:
```shell
go run .
```

View File

@ -0,0 +1,12 @@
module github.com/redis/go-redis/example/redis-bloom
go 1.18
replace github.com/redis/go-redis/v9 => ../..
require github.com/redis/go-redis/v9 v9.5.3
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

View File

@ -0,0 +1,6 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

155
example/redis-bloom/main.go Normal file
View File

@ -0,0 +1,155 @@
package main
import (
"context"
"fmt"
"math/rand"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.FlushDB(ctx).Err()
fmt.Printf("# BLOOM\n")
bloomFilter(ctx, rdb)
fmt.Printf("\n# CUCKOO\n")
cuckooFilter(ctx, rdb)
fmt.Printf("\n# COUNT-MIN\n")
countMinSketch(ctx, rdb)
fmt.Printf("\n# TOP-K\n")
topK(ctx, rdb)
}
func bloomFilter(ctx context.Context, rdb *redis.Client) {
inserted, err := rdb.Do(ctx, "BF.ADD", "bf_key", "item0").Bool()
if err != nil {
panic(err)
}
if inserted {
fmt.Println("item0 was inserted")
} else {
fmt.Println("item0 already exists")
}
for _, item := range []string{"item0", "item1"} {
exists, err := rdb.Do(ctx, "BF.EXISTS", "bf_key", item).Bool()
if err != nil {
panic(err)
}
if exists {
fmt.Printf("%s does exist\n", item)
} else {
fmt.Printf("%s does not exist\n", item)
}
}
bools, err := rdb.Do(ctx, "BF.MADD", "bf_key", "item1", "item2", "item3").BoolSlice()
if err != nil {
panic(err)
}
fmt.Println("adding multiple items:", bools)
}
func cuckooFilter(ctx context.Context, rdb *redis.Client) {
inserted, err := rdb.Do(ctx, "CF.ADDNX", "cf_key", "item0").Bool()
if err != nil {
panic(err)
}
if inserted {
fmt.Println("item0 was inserted")
} else {
fmt.Println("item0 already exists")
}
for _, item := range []string{"item0", "item1"} {
exists, err := rdb.Do(ctx, "CF.EXISTS", "cf_key", item).Bool()
if err != nil {
panic(err)
}
if exists {
fmt.Printf("%s does exist\n", item)
} else {
fmt.Printf("%s does not exist\n", item)
}
}
deleted, err := rdb.Do(ctx, "CF.DEL", "cf_key", "item0").Bool()
if err != nil {
panic(err)
}
if deleted {
fmt.Println("item0 was deleted")
}
}
func countMinSketch(ctx context.Context, rdb *redis.Client) {
if err := rdb.Do(ctx, "CMS.INITBYPROB", "count_min", 0.001, 0.01).Err(); err != nil {
panic(err)
}
items := []string{"item1", "item2", "item3", "item4", "item5"}
counts := make(map[string]int, len(items))
for i := 0; i < 10000; i++ {
n := rand.Intn(len(items))
item := items[n]
if err := rdb.Do(ctx, "CMS.INCRBY", "count_min", item, 1).Err(); err != nil {
panic(err)
}
counts[item]++
}
for item, count := range counts {
ns, err := rdb.Do(ctx, "CMS.QUERY", "count_min", item).Int64Slice()
if err != nil {
panic(err)
}
fmt.Printf("%s: count-min=%d actual=%d\n", item, ns[0], count)
}
}
func topK(ctx context.Context, rdb *redis.Client) {
if err := rdb.Do(ctx, "TOPK.RESERVE", "top_items", 3).Err(); err != nil {
panic(err)
}
counts := map[string]int{
"item1": 1000,
"item2": 2000,
"item3": 3000,
"item4": 4000,
"item5": 5000,
"item6": 6000,
}
for item, count := range counts {
for i := 0; i < count; i++ {
if err := rdb.Do(ctx, "TOPK.INCRBY", "top_items", item, 1).Err(); err != nil {
panic(err)
}
}
}
items, err := rdb.Do(ctx, "TOPK.LIST", "top_items").StringSlice()
if err != nil {
panic(err)
}
for _, item := range items {
ns, err := rdb.Do(ctx, "TOPK.COUNT", "top_items", item).Int64Slice()
if err != nil {
panic(err)
}
fmt.Printf("%s: top-k=%d actual=%d\n", item, ns[0], counts[item])
}
}

View File

@ -0,0 +1,11 @@
# Example for scanning hash fields into a struct
To run this example:
```shell
go run .
```
See
[Redis: Scanning hash fields into a struct](https://redis.uptrace.dev/guide/scanning-hash-fields.html)
for details.

View File

@ -0,0 +1,15 @@
module github.com/redis/go-redis/example/scan-struct
go 1.18
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/davecgh/go-spew v1.1.1
github.com/redis/go-redis/v9 v9.5.3
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

View File

@ -0,0 +1,8 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

View File

@ -0,0 +1,77 @@
package main
import (
"context"
"github.com/davecgh/go-spew/spew"
"github.com/redis/go-redis/v9"
)
type Model struct {
Str1 string `redis:"str1"`
Str2 string `redis:"str2"`
Bytes []byte `redis:"bytes"`
Int int `redis:"int"`
Bool bool `redis:"bool"`
Ignored struct{} `redis:"-"`
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.FlushDB(ctx).Err()
// Set some fields.
if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {
rdb.HSet(ctx, "key", "str1", "hello")
rdb.HSet(ctx, "key", "str2", "world")
rdb.HSet(ctx, "key", "int", 123)
rdb.HSet(ctx, "key", "bool", 1)
rdb.HSet(ctx, "key", "bytes", []byte("this is bytes !"))
return nil
}); err != nil {
panic(err)
}
var model1, model2 Model
// Scan all fields into the model.
if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil {
panic(err)
}
// Or scan a subset of the fields.
if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil {
panic(err)
}
spew.Dump(model1)
// Output:
// (main.Model) {
// Str1: (string) (len=5) "hello",
// Str2: (string) (len=5) "world",
// Bytes: ([]uint8) (len=15 cap=16) {
// 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !|
// },
// Int: (int) 123,
// Bool: (bool) true,
// Ignored: (struct {}) {
// }
// }
spew.Dump(model2)
// Output:
// (main.Model) {
// Str1: (string) (len=5) "hello",
// Str2: (string) "",
// Bytes: ([]uint8) <nil>,
// Int: (int) 123,
// Bool: (bool) false,
// Ignored: (struct {}) {
// }
// }
}

View File

@ -0,0 +1,94 @@
package redis_test
import (
"context"
"fmt"
"net"
"github.com/redis/go-redis/v9"
)
type redisHook struct{}
var _ redis.Hook = redisHook{}
func (redisHook) DialHook(hook redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
fmt.Printf("dialing %s %s\n", network, addr)
conn, err := hook(ctx, network, addr)
fmt.Printf("finished dialing %s %s\n", network, addr)
return conn, err
}
}
func (redisHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
fmt.Printf("starting processing: <%s>\n", cmd)
err := hook(ctx, cmd)
fmt.Printf("finished processing: <%s>\n", cmd)
return err
}
}
func (redisHook) ProcessPipelineHook(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
fmt.Printf("pipeline starting processing: %v\n", cmds)
err := hook(ctx, cmds)
fmt.Printf("pipeline finished processing: %v\n", cmds)
return err
}
}
func Example_instrumentation() {
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
rdb.AddHook(redisHook{})
rdb.Ping(ctx)
// Output: starting processing: <ping: >
// dialing tcp :6379
// finished dialing tcp :6379
// finished processing: <ping: PONG>
}
func ExamplePipeline_instrumentation() {
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
rdb.AddHook(redisHook{})
rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Ping(ctx)
pipe.Ping(ctx)
return nil
})
// Output: pipeline starting processing: [ping: ping: ]
// dialing tcp :6379
// finished dialing tcp :6379
// pipeline finished processing: [ping: PONG ping: PONG]
}
func ExampleClient_Watch_instrumentation() {
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
rdb.AddHook(redisHook{})
rdb.Watch(ctx, func(tx *redis.Tx) error {
tx.Ping(ctx)
tx.Ping(ctx)
return nil
}, "foo")
// Output:
// starting processing: <watch foo: >
// dialing tcp :6379
// finished dialing tcp :6379
// finished processing: <watch foo: OK>
// starting processing: <ping: >
// finished processing: <ping: PONG>
// starting processing: <ping: >
// finished processing: <ping: PONG>
// starting processing: <unwatch: >
// finished processing: <unwatch: OK>
}

705
example_test.go Normal file
View File

@ -0,0 +1,705 @@
package redis_test
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/redis/go-redis/v9"
)
var (
ctx = context.Background()
rdb *redis.Client
)
func init() {
rdb = redis.NewClient(&redis.Options{
Addr: ":6379",
DialTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
PoolSize: 10,
PoolTimeout: 30 * time.Second,
})
}
func ExampleNewClient() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // use default Addr
Password: "", // no password set
DB: 0, // use default DB
})
pong, err := rdb.Ping(ctx).Result()
fmt.Println(pong, err)
// Output: PONG <nil>
}
func ExampleParseURL() {
opt, err := redis.ParseURL("redis://:qwerty@localhost:6379/1?dial_timeout=5s")
if err != nil {
panic(err)
}
fmt.Println("addr is", opt.Addr)
fmt.Println("db is", opt.DB)
fmt.Println("password is", opt.Password)
fmt.Println("dial timeout is", opt.DialTimeout)
// Create client as usually.
_ = redis.NewClient(opt)
// Output: addr is localhost:6379
// db is 1
// password is qwerty
// dial timeout is 5s
}
func ExampleNewFailoverClient() {
// See http://redis.io/topics/sentinel for instructions how to
// setup Redis Sentinel.
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "master",
SentinelAddrs: []string{":26379"},
})
rdb.Ping(ctx)
}
func ExampleNewClusterClient() {
// See http://redis.io/topics/cluster-tutorial for instructions
// how to setup Redis Cluster.
rdb := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
})
rdb.Ping(ctx)
}
// Following example creates a cluster from 2 master nodes and 2 slave nodes
// without using cluster mode or Redis Sentinel.
func ExampleNewClusterClient_manualSetup() {
// clusterSlots returns cluster slots information.
// It can use service like ZooKeeper to maintain configuration information
// and Cluster.ReloadState to manually trigger state reloading.
clusterSlots := func(ctx context.Context) ([]redis.ClusterSlot, error) {
slots := []redis.ClusterSlot{
// First node with 1 master and 1 slave.
{
Start: 0,
End: 8191,
Nodes: []redis.ClusterNode{{
Addr: ":7000", // master
}, {
Addr: ":8000", // 1st slave
}},
},
// Second node with 1 master and 1 slave.
{
Start: 8192,
End: 16383,
Nodes: []redis.ClusterNode{{
Addr: ":7001", // master
}, {
Addr: ":8001", // 1st slave
}},
},
}
return slots, nil
}
rdb := redis.NewClusterClient(&redis.ClusterOptions{
ClusterSlots: clusterSlots,
RouteRandomly: true,
})
rdb.Ping(ctx)
// ReloadState reloads cluster state. It calls ClusterSlots func
// to get cluster slots information.
rdb.ReloadState(ctx)
}
func ExampleNewRing() {
rdb := redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"shard1": ":7000",
"shard2": ":7001",
"shard3": ":7002",
},
})
rdb.Ping(ctx)
}
func ExampleClient() {
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
panic(err)
}
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
panic(err)
}
fmt.Println("key", val)
val2, err := rdb.Get(ctx, "missing_key").Result()
if err == redis.Nil {
fmt.Println("missing_key does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("missing_key", val2)
}
// Output: key value
// missing_key does not exist
}
func ExampleConn_name() {
conn := rdb.Conn()
err := conn.ClientSetName(ctx, "foobar").Err()
if err != nil {
panic(err)
}
// Open other connections.
for i := 0; i < 10; i++ {
go rdb.Ping(ctx)
}
s, err := conn.ClientGetName(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println(s)
// Output: foobar
}
func ExampleConn_client_setInfo_libraryVersion() {
conn := rdb.Conn()
err := conn.ClientSetInfo(ctx, redis.WithLibraryVersion("1.2.3")).Err()
if err != nil {
panic(err)
}
// Open other connections.
for i := 0; i < 10; i++ {
go rdb.Ping(ctx)
}
s, err := conn.ClientInfo(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println(s.LibVer)
// Output: 1.2.3
}
func ExampleClient_Set() {
// Last argument is expiration. Zero means the key has no
// expiration time.
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
panic(err)
}
// key2 will expire in an hour.
err = rdb.Set(ctx, "key2", "value", time.Hour).Err()
if err != nil {
panic(err)
}
}
func ExampleClient_SetEx() {
err := rdb.SetEx(ctx, "key", "value", time.Hour).Err()
if err != nil {
panic(err)
}
}
func ExampleClient_HSet() {
// Set "redis" tag for hash key
type ExampleUser struct {
Name string `redis:"name"`
Age int `redis:"age"`
}
items := ExampleUser{"jane", 22}
err := rdb.HSet(ctx, "user:1", items).Err()
if err != nil {
panic(err)
}
}
func ExampleClient_Incr() {
result, err := rdb.Incr(ctx, "counter").Result()
if err != nil {
panic(err)
}
fmt.Println(result)
// Output: 1
}
func ExampleClient_BLPop() {
if err := rdb.RPush(ctx, "queue", "message").Err(); err != nil {
panic(err)
}
// use `rdb.BLPop(ctx, 0, "queue")` for infinite waiting time
result, err := rdb.BLPop(ctx, 1*time.Second, "queue").Result()
if err != nil {
panic(err)
}
fmt.Println(result[0], result[1])
// Output: queue message
}
func ExampleClient_Scan() {
rdb.FlushDB(ctx)
for i := 0; i < 33; i++ {
err := rdb.Set(ctx, fmt.Sprintf("key%d", i), "value", 0).Err()
if err != nil {
panic(err)
}
}
var cursor uint64
var n int
for {
var keys []string
var err error
keys, cursor, err = rdb.Scan(ctx, cursor, "key*", 10).Result()
if err != nil {
panic(err)
}
n += len(keys)
if cursor == 0 {
break
}
}
fmt.Printf("found %d keys\n", n)
// Output: found 33 keys
}
func ExampleClient_ScanType() {
rdb.FlushDB(ctx)
for i := 0; i < 33; i++ {
err := rdb.Set(ctx, fmt.Sprintf("key%d", i), "value", 0).Err()
if err != nil {
panic(err)
}
}
var cursor uint64
var n int
for {
var keys []string
var err error
keys, cursor, err = rdb.ScanType(ctx, cursor, "key*", 10, "string").Result()
if err != nil {
panic(err)
}
n += len(keys)
if cursor == 0 {
break
}
}
fmt.Printf("found %d keys\n", n)
// Output: found 33 keys
}
// ExampleClient_ScanType_hashType uses the keyType "hash".
func ExampleClient_ScanType_hashType() {
rdb.FlushDB(ctx)
for i := 0; i < 33; i++ {
err := rdb.HSet(context.TODO(), fmt.Sprintf("key%d", i), "value", "foo").Err()
if err != nil {
panic(err)
}
}
var allKeys []string
var cursor uint64
var err error
for {
var keysFromScan []string
keysFromScan, cursor, err = rdb.ScanType(context.TODO(), cursor, "key*", 10, "hash").Result()
if err != nil {
panic(err)
}
allKeys = append(allKeys, keysFromScan...)
if cursor == 0 {
break
}
}
fmt.Printf("%d keys ready for use", len(allKeys))
// Output: 33 keys ready for use
}
// ExampleMapStringStringCmd_Scan shows how to scan the results of a map fetch
// into a struct.
func ExampleMapStringStringCmd_Scan() {
rdb.FlushDB(ctx)
err := rdb.HMSet(ctx, "map",
"name", "hello",
"count", 123,
"correct", true).Err()
if err != nil {
panic(err)
}
// Get the map. The same approach works for HmGet().
res := rdb.HGetAll(ctx, "map")
if res.Err() != nil {
panic(err)
}
type data struct {
Name string `redis:"name"`
Count int `redis:"count"`
Correct bool `redis:"correct"`
}
// Scan the results into the struct.
var d data
if err := res.Scan(&d); err != nil {
panic(err)
}
fmt.Println(d)
// Output: {hello 123 true}
}
// ExampleSliceCmd_Scan shows how to scan the results of a multi key fetch
// into a struct.
func ExampleSliceCmd_Scan() {
rdb.FlushDB(ctx)
err := rdb.MSet(ctx,
"name", "hello",
"count", 123,
"correct", true).Err()
if err != nil {
panic(err)
}
res := rdb.MGet(ctx, "name", "count", "empty", "correct")
if res.Err() != nil {
panic(err)
}
type data struct {
Name string `redis:"name"`
Count int `redis:"count"`
Correct bool `redis:"correct"`
}
// Scan the results into the struct.
var d data
if err := res.Scan(&d); err != nil {
panic(err)
}
fmt.Println(d)
// Output: {hello 123 true}
}
func ExampleClient_Pipelined() {
var incr *redis.IntCmd
_, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
incr = pipe.Incr(ctx, "pipelined_counter")
pipe.Expire(ctx, "pipelined_counter", time.Hour)
return nil
})
fmt.Println(incr.Val(), err)
// Output: 1 <nil>
}
func ExampleClient_Pipeline() {
pipe := rdb.Pipeline()
incr := pipe.Incr(ctx, "pipeline_counter")
pipe.Expire(ctx, "pipeline_counter", time.Hour)
// Execute
//
// INCR pipeline_counter
// EXPIRE pipeline_counts 3600
//
// using one rdb-server roundtrip.
_, err := pipe.Exec(ctx)
fmt.Println(incr.Val(), err)
// Output: 1 <nil>
}
func ExampleClient_TxPipelined() {
var incr *redis.IntCmd
_, err := rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
incr = pipe.Incr(ctx, "tx_pipelined_counter")
pipe.Expire(ctx, "tx_pipelined_counter", time.Hour)
return nil
})
fmt.Println(incr.Val(), err)
// Output: 1 <nil>
}
func ExampleClient_TxPipeline() {
pipe := rdb.TxPipeline()
incr := pipe.Incr(ctx, "tx_pipeline_counter")
pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)
// Execute
//
// MULTI
// INCR pipeline_counter
// EXPIRE pipeline_counts 3600
// EXEC
//
// using one rdb-server roundtrip.
_, err := pipe.Exec(ctx)
fmt.Println(incr.Val(), err)
// Output: 1 <nil>
}
func ExampleClient_Watch() {
const maxRetries = 10000
// Increment transactionally increments key using GET and SET commands.
increment := func(key string) error {
// Transactional function.
txf := func(tx *redis.Tx) error {
// Get current value or zero.
n, err := tx.Get(ctx, key).Int()
if err != nil && err != redis.Nil {
return err
}
// Actual operation (local in optimistic lock).
n++
// Operation is committed only if the watched keys remain unchanged.
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Set(ctx, key, n, 0)
return nil
})
return err
}
for i := 0; i < maxRetries; i++ {
err := rdb.Watch(ctx, txf, key)
if err == nil {
// Success.
return nil
}
if err == redis.TxFailedErr {
// Optimistic lock lost. Retry.
continue
}
// Return any other error.
return err
}
return errors.New("increment reached maximum number of retries")
}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := increment("counter3"); err != nil {
fmt.Println("increment error:", err)
}
}()
}
wg.Wait()
n, err := rdb.Get(ctx, "counter3").Int()
fmt.Println("ended with", n, err)
// Output: ended with 100 <nil>
}
func ExamplePubSub() {
pubsub := rdb.Subscribe(ctx, "mychannel1")
// Wait for confirmation that subscription is created before publishing anything.
_, err := pubsub.Receive(ctx)
if err != nil {
panic(err)
}
// Go channel which receives messages.
ch := pubsub.Channel()
// Publish a message.
err = rdb.Publish(ctx, "mychannel1", "hello").Err()
if err != nil {
panic(err)
}
time.AfterFunc(time.Second, func() {
// When pubsub is closed channel is closed too.
_ = pubsub.Close()
})
// Consume messages.
for msg := range ch {
fmt.Println(msg.Channel, msg.Payload)
}
// Output: mychannel1 hello
}
func ExamplePubSub_Receive() {
pubsub := rdb.Subscribe(ctx, "mychannel2")
defer pubsub.Close()
for i := 0; i < 2; i++ {
// ReceiveTimeout is a low level API. Use ReceiveMessage instead.
msgi, err := pubsub.ReceiveTimeout(ctx, time.Second)
if err != nil {
break
}
switch msg := msgi.(type) {
case *redis.Subscription:
fmt.Println("subscribed to", msg.Channel)
_, err := rdb.Publish(ctx, "mychannel2", "hello").Result()
if err != nil {
panic(err)
}
case *redis.Message:
fmt.Println("received", msg.Payload, "from", msg.Channel)
default:
panic("unreached")
}
}
// sent message to 1 rdb
// received hello from mychannel2
}
func ExampleScript() {
IncrByXX := redis.NewScript(`
if redis.call("GET", KEYS[1]) ~= false then
return redis.call("INCRBY", KEYS[1], ARGV[1])
end
return false
`)
n, err := IncrByXX.Run(ctx, rdb, []string{"xx_counter"}, 2).Result()
fmt.Println(n, err)
err = rdb.Set(ctx, "xx_counter", "40", 0).Err()
if err != nil {
panic(err)
}
n, err = IncrByXX.Run(ctx, rdb, []string{"xx_counter"}, 2).Result()
fmt.Println(n, err)
// Output: <nil> redis: nil
// 42 <nil>
}
func Example_customCommand() {
Get := func(ctx context.Context, rdb *redis.Client, key string) *redis.StringCmd {
cmd := redis.NewStringCmd(ctx, "get", key)
rdb.Process(ctx, cmd)
return cmd
}
v, err := Get(ctx, rdb, "key_does_not_exist").Result()
fmt.Printf("%q %s", v, err)
// Output: "" redis: nil
}
func Example_customCommand2() {
v, err := rdb.Do(ctx, "get", "key_does_not_exist").Text()
fmt.Printf("%q %s", v, err)
// Output: "" redis: nil
}
func ExampleScanIterator() {
iter := rdb.Scan(ctx, 0, "", 0).Iterator()
for iter.Next(ctx) {
fmt.Println(iter.Val())
}
if err := iter.Err(); err != nil {
panic(err)
}
}
func ExampleScanCmd_Iterator() {
iter := rdb.Scan(ctx, 0, "", 0).Iterator()
for iter.Next(ctx) {
fmt.Println(iter.Val())
}
if err := iter.Err(); err != nil {
panic(err)
}
}
func ExampleNewUniversalClient_simple() {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: []string{":6379"},
})
defer rdb.Close()
rdb.Ping(ctx)
}
func ExampleNewUniversalClient_failover() {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
MasterName: "master",
Addrs: []string{":26379"},
})
defer rdb.Close()
rdb.Ping(ctx)
}
func ExampleNewUniversalClient_cluster() {
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
})
defer rdb.Close()
rdb.Ping(ctx)
}
func ExampleClient_SlowLogGet() {
if RECluster {
// skip slowlog test for cluster
fmt.Println(2)
return
}
const key = "slowlog-log-slower-than"
old := rdb.ConfigGet(ctx, key).Val()
rdb.ConfigSet(ctx, key, "0")
defer rdb.ConfigSet(ctx, key, old[key])
if err := rdb.Do(ctx, "slowlog", "reset").Err(); err != nil {
panic(err)
}
rdb.Set(ctx, "test", "true", 0)
result, err := rdb.SlowLogGet(ctx, -1).Result()
if err != nil {
panic(err)
}
fmt.Println(len(result))
// Output: 2
}

104
export_test.go Normal file
View File

@ -0,0 +1,104 @@
package redis
import (
"context"
"fmt"
"net"
"strings"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/hashtag"
"github.com/redis/go-redis/v9/internal/pool"
)
func (c *baseClient) Pool() pool.Pooler {
return c.connPool
}
func (c *PubSub) SetNetConn(netConn net.Conn) {
c.cn = pool.NewConn(netConn)
}
func (c *ClusterClient) LoadState(ctx context.Context) (*clusterState, error) {
// return c.state.Reload(ctx)
return c.loadState(ctx)
}
func (c *ClusterClient) SlotAddrs(ctx context.Context, slot int) []string {
state, err := c.state.Get(ctx)
if err != nil {
panic(err)
}
var addrs []string
for _, n := range state.slotNodes(slot) {
addrs = append(addrs, n.Client.getAddr())
}
return addrs
}
func (c *ClusterClient) Nodes(ctx context.Context, key string) ([]*clusterNode, error) {
state, err := c.state.Reload(ctx)
if err != nil {
return nil, err
}
slot := hashtag.Slot(key)
nodes := state.slotNodes(slot)
if len(nodes) != 2 {
return nil, fmt.Errorf("slot=%d does not have enough nodes: %v", slot, nodes)
}
return nodes, nil
}
func (c *ClusterClient) SwapNodes(ctx context.Context, key string) error {
nodes, err := c.Nodes(ctx, key)
if err != nil {
return err
}
nodes[0], nodes[1] = nodes[1], nodes[0]
return nil
}
func (c *clusterState) IsConsistent(ctx context.Context) bool {
if len(c.Masters) < 3 {
return false
}
for _, master := range c.Masters {
s := master.Client.Info(ctx, "replication").Val()
if !strings.Contains(s, "role:master") {
return false
}
}
if len(c.Slaves) < 3 {
return false
}
for _, slave := range c.Slaves {
s := slave.Client.Info(ctx, "replication").Val()
if !strings.Contains(s, "role:slave") {
return false
}
}
return true
}
func GetSlavesAddrByName(ctx context.Context, c *SentinelClient, name string) []string {
addrs, err := c.Replicas(ctx, name).Result()
if err != nil {
internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
name, err)
return []string{}
}
return parseReplicaAddrs(addrs, false)
}
func (c *Ring) ShardByName(name string) *ringShard {
shard, _ := c.sharding.GetByName(name)
return shard
}
func (c *ModuleLoadexConfig) ToArgs() []interface{} {
return c.toArgs()
}

19
extra/rediscensus/go.mod Normal file
View File

@ -0,0 +1,19 @@
module github.com/redis/go-redis/extra/rediscensus/v9
go 1.19
replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd
require (
github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3
github.com/redis/go-redis/v9 v9.5.3
go.opencensus.io v0.24.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
)

100
extra/rediscensus/go.sum Normal file
View File

@ -0,0 +1,100 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -0,0 +1,76 @@
package rediscensus
import (
"context"
"net"
"go.opencensus.io/trace"
"github.com/redis/go-redis/extra/rediscmd/v9"
"github.com/redis/go-redis/v9"
)
type TracingHook struct{}
var _ redis.Hook = (*TracingHook)(nil)
func NewTracingHook() *TracingHook {
return new(TracingHook)
}
func (TracingHook) DialHook(next redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
ctx, span := trace.StartSpan(ctx, "dial")
defer span.End()
span.AddAttributes(
trace.StringAttribute("db.system", "redis"),
trace.StringAttribute("network", network),
trace.StringAttribute("addr", addr),
)
conn, err := next(ctx, network, addr)
if err != nil {
recordErrorOnOCSpan(ctx, span, err)
return nil, err
}
return conn, nil
}
}
func (TracingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
ctx, span := trace.StartSpan(ctx, cmd.FullName())
defer span.End()
span.AddAttributes(
trace.StringAttribute("db.system", "redis"),
trace.StringAttribute("redis.cmd", rediscmd.CmdString(cmd)),
)
err := next(ctx, cmd)
if err != nil {
recordErrorOnOCSpan(ctx, span, err)
return err
}
if err = cmd.Err(); err != nil {
recordErrorOnOCSpan(ctx, span, err)
}
return nil
}
}
func (TracingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return next
}
func recordErrorOnOCSpan(ctx context.Context, span *trace.Span, err error) {
if err != redis.Nil {
span.AddAttributes(trace.BoolAttribute("error", true))
span.Annotate([]trace.Attribute{trace.StringAttribute("Error", "redis error")}, err.Error())
}
}

16
extra/rediscmd/go.mod Normal file
View File

@ -0,0 +1,16 @@
module github.com/redis/go-redis/extra/rediscmd/v9
go 1.19
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/bsm/ginkgo/v2 v2.12.0
github.com/bsm/gomega v1.27.10
github.com/redis/go-redis/v9 v9.5.3
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

8
extra/rediscmd/go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

149
extra/rediscmd/rediscmd.go Normal file
View File

@ -0,0 +1,149 @@
package rediscmd
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9"
)
func CmdString(cmd redis.Cmder) string {
b := make([]byte, 0, 32)
b = AppendCmd(b, cmd)
return String(b)
}
func CmdsString(cmds []redis.Cmder) (string, string) {
const numCmdLimit = 100
const numNameLimit = 10
seen := make(map[string]struct{}, numNameLimit)
unqNames := make([]string, 0, numNameLimit)
b := make([]byte, 0, 32*len(cmds))
for i, cmd := range cmds {
if i > numCmdLimit {
break
}
if i > 0 {
b = append(b, '\n')
}
b = AppendCmd(b, cmd)
if len(unqNames) >= numNameLimit {
continue
}
name := cmd.FullName()
if _, ok := seen[name]; !ok {
seen[name] = struct{}{}
unqNames = append(unqNames, name)
}
}
summary := strings.Join(unqNames, " ")
return summary, String(b)
}
func AppendCmd(b []byte, cmd redis.Cmder) []byte {
const numArgLimit = 32
for i, arg := range cmd.Args() {
if i > numArgLimit {
break
}
if i > 0 {
b = append(b, ' ')
}
b = appendArg(b, arg)
}
if err := cmd.Err(); err != nil {
b = append(b, ": "...)
b = append(b, err.Error()...)
}
return b
}
func appendArg(b []byte, v interface{}) []byte {
const argLenLimit = 64
switch v := v.(type) {
case nil:
return append(b, "<nil>"...)
case string:
if len(v) > argLenLimit {
v = v[:argLenLimit]
}
return appendUTF8String(b, Bytes(v))
case []byte:
if len(v) > argLenLimit {
v = v[:argLenLimit]
}
return appendUTF8String(b, v)
case int:
return strconv.AppendInt(b, int64(v), 10)
case int8:
return strconv.AppendInt(b, int64(v), 10)
case int16:
return strconv.AppendInt(b, int64(v), 10)
case int32:
return strconv.AppendInt(b, int64(v), 10)
case int64:
return strconv.AppendInt(b, v, 10)
case uint:
return strconv.AppendUint(b, uint64(v), 10)
case uint8:
return strconv.AppendUint(b, uint64(v), 10)
case uint16:
return strconv.AppendUint(b, uint64(v), 10)
case uint32:
return strconv.AppendUint(b, uint64(v), 10)
case uint64:
return strconv.AppendUint(b, v, 10)
case float32:
return strconv.AppendFloat(b, float64(v), 'f', -1, 64)
case float64:
return strconv.AppendFloat(b, v, 'f', -1, 64)
case bool:
if v {
return append(b, "true"...)
}
return append(b, "false"...)
case time.Time:
return v.AppendFormat(b, time.RFC3339Nano)
default:
return append(b, fmt.Sprint(v)...)
}
}
func appendUTF8String(dst []byte, src []byte) []byte {
if isSimple(src) {
dst = append(dst, src...)
return dst
}
s := len(dst)
dst = append(dst, make([]byte, hex.EncodedLen(len(src)))...)
hex.Encode(dst[s:], src)
return dst
}
func isSimple(b []byte) bool {
for _, c := range b {
if !isSimpleByte(c) {
return false
}
}
return true
}
func isSimpleByte(c byte) bool {
return c >= 0x21 && c <= 0x7e
}

View File

@ -0,0 +1,31 @@
package rediscmd
import (
"testing"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
)
func TestGinkgo(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "redisext")
}
var _ = Describe("AppendArg", func() {
DescribeTable("...",
func(src string, wanted string) {
b := appendArg(nil, src)
Expect(string(b)).To(Equal(wanted))
},
Entry("", "-inf", "-inf"),
Entry("", "+inf", "+inf"),
Entry("", "foo.bar", "foo.bar"),
Entry("", "foo:bar", "foo:bar"),
Entry("", "foo{bar}", "foo{bar}"),
Entry("", "foo-123_BAR", "foo-123_BAR"),
Entry("", "foo\nbar", "666f6f0a626172"),
Entry("", "\000", "00"),
)
})

12
extra/rediscmd/safe.go Normal file
View File

@ -0,0 +1,12 @@
//go:build appengine
// +build appengine
package rediscmd
func String(b []byte) string {
return string(b)
}
func Bytes(s string) []byte {
return []byte(s)
}

21
extra/rediscmd/unsafe.go Normal file
View File

@ -0,0 +1,21 @@
//go:build !appengine
// +build !appengine
package rediscmd
import "unsafe"
// String converts byte slice to string.
func String(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// Bytes converts string to byte slice.
func Bytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
}

34
extra/redisotel/README.md Normal file
View File

@ -0,0 +1,34 @@
# OpenTelemetry instrumentation for go-redis
## Installation
```bash
go get github.com/redis/go-redis/extra/redisotel/v9
```
## Usage
Tracing is enabled by adding a hook:
```go
import (
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/extra/redisotel/v9"
)
rdb := rdb.NewClient(&rdb.Options{...})
// Enable tracing instrumentation.
if err := redisotel.InstrumentTracing(rdb); err != nil {
panic(err)
}
// Enable metrics instrumentation.
if err := redisotel.InstrumentMetrics(rdb); err != nil {
panic(err)
}
```
See [example](../../example/otel) and
[Monitoring Go Redis Performance and Errors](https://redis.uptrace.dev/guide/go-redis-monitoring.html)
for details.

138
extra/redisotel/config.go Normal file
View File

@ -0,0 +1,138 @@
package redisotel
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)
type config struct {
// Common options.
dbSystem string
attrs []attribute.KeyValue
// Tracing options.
tp trace.TracerProvider
tracer trace.Tracer
dbStmtEnabled bool
// Metrics options.
mp metric.MeterProvider
meter metric.Meter
poolName string
}
type baseOption interface {
apply(conf *config)
}
type Option interface {
baseOption
tracing()
metrics()
}
type option func(conf *config)
func (fn option) apply(conf *config) {
fn(conf)
}
func (fn option) tracing() {}
func (fn option) metrics() {}
func newConfig(opts ...baseOption) *config {
conf := &config{
dbSystem: "redis",
attrs: []attribute.KeyValue{},
tp: otel.GetTracerProvider(),
mp: otel.GetMeterProvider(),
dbStmtEnabled: true,
}
for _, opt := range opts {
opt.apply(conf)
}
conf.attrs = append(conf.attrs, semconv.DBSystemKey.String(conf.dbSystem))
return conf
}
func WithDBSystem(dbSystem string) Option {
return option(func(conf *config) {
conf.dbSystem = dbSystem
})
}
// WithAttributes specifies additional attributes to be added to the span.
func WithAttributes(attrs ...attribute.KeyValue) Option {
return option(func(conf *config) {
conf.attrs = append(conf.attrs, attrs...)
})
}
//------------------------------------------------------------------------------
type TracingOption interface {
baseOption
tracing()
}
type tracingOption func(conf *config)
var _ TracingOption = (*tracingOption)(nil)
func (fn tracingOption) apply(conf *config) {
fn(conf)
}
func (fn tracingOption) tracing() {}
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
// If none is specified, the global provider is used.
func WithTracerProvider(provider trace.TracerProvider) TracingOption {
return tracingOption(func(conf *config) {
conf.tp = provider
})
}
// WithDBStatement tells the tracing hook not to log raw redis commands.
func WithDBStatement(on bool) TracingOption {
return tracingOption(func(conf *config) {
conf.dbStmtEnabled = on
})
}
//------------------------------------------------------------------------------
type MetricsOption interface {
baseOption
metrics()
}
type metricsOption func(conf *config)
var _ MetricsOption = (*metricsOption)(nil)
func (fn metricsOption) apply(conf *config) {
fn(conf)
}
func (fn metricsOption) metrics() {}
// WithMeterProvider configures a metric.Meter used to create instruments.
func WithMeterProvider(mp metric.MeterProvider) MetricsOption {
return metricsOption(func(conf *config) {
conf.mp = mp
})
}

24
extra/redisotel/go.mod Normal file
View File

@ -0,0 +1,24 @@
module github.com/redis/go-redis/extra/redisotel/v9
go 1.19
replace github.com/redis/go-redis/v9 => ../..
replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd
require (
github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3
github.com/redis/go-redis/v9 v9.5.3
go.opentelemetry.io/otel v1.22.0
go.opentelemetry.io/otel/metric v1.22.0
go.opentelemetry.io/otel/sdk v1.22.0
go.opentelemetry.io/otel/trace v1.22.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
golang.org/x/sys v0.16.0 // indirect
)

26
extra/redisotel/go.sum Normal file
View File

@ -0,0 +1,26 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

256
extra/redisotel/metrics.go Normal file
View File

@ -0,0 +1,256 @@
package redisotel
import (
"context"
"fmt"
"net"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"github.com/redis/go-redis/v9"
)
// InstrumentMetrics starts reporting OpenTelemetry Metrics.
//
// Based on https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/database-metrics.md
func InstrumentMetrics(rdb redis.UniversalClient, opts ...MetricsOption) error {
baseOpts := make([]baseOption, len(opts))
for i, opt := range opts {
baseOpts[i] = opt
}
conf := newConfig(baseOpts...)
if conf.meter == nil {
conf.meter = conf.mp.Meter(
instrumName,
metric.WithInstrumentationVersion("semver:"+redis.Version()),
)
}
switch rdb := rdb.(type) {
case *redis.Client:
if conf.poolName == "" {
opt := rdb.Options()
conf.poolName = opt.Addr
}
conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName))
if err := reportPoolStats(rdb, conf); err != nil {
return err
}
if err := addMetricsHook(rdb, conf); err != nil {
return err
}
return nil
case *redis.ClusterClient:
rdb.OnNewNode(func(rdb *redis.Client) {
if conf.poolName == "" {
opt := rdb.Options()
conf.poolName = opt.Addr
}
conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName))
if err := reportPoolStats(rdb, conf); err != nil {
otel.Handle(err)
}
if err := addMetricsHook(rdb, conf); err != nil {
otel.Handle(err)
}
})
return nil
case *redis.Ring:
rdb.OnNewNode(func(rdb *redis.Client) {
if conf.poolName == "" {
opt := rdb.Options()
conf.poolName = opt.Addr
}
conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName))
if err := reportPoolStats(rdb, conf); err != nil {
otel.Handle(err)
}
if err := addMetricsHook(rdb, conf); err != nil {
otel.Handle(err)
}
})
return nil
default:
return fmt.Errorf("redisotel: %T not supported", rdb)
}
}
func reportPoolStats(rdb *redis.Client, conf *config) error {
labels := conf.attrs
idleAttrs := append(labels, attribute.String("state", "idle"))
usedAttrs := append(labels, attribute.String("state", "used"))
idleMax, err := conf.meter.Int64ObservableUpDownCounter(
"db.client.connections.idle.max",
metric.WithDescription("The maximum number of idle open connections allowed"),
)
if err != nil {
return err
}
idleMin, err := conf.meter.Int64ObservableUpDownCounter(
"db.client.connections.idle.min",
metric.WithDescription("The minimum number of idle open connections allowed"),
)
if err != nil {
return err
}
connsMax, err := conf.meter.Int64ObservableUpDownCounter(
"db.client.connections.max",
metric.WithDescription("The maximum number of open connections allowed"),
)
if err != nil {
return err
}
usage, err := conf.meter.Int64ObservableUpDownCounter(
"db.client.connections.usage",
metric.WithDescription("The number of connections that are currently in state described by the state attribute"),
)
if err != nil {
return err
}
timeouts, err := conf.meter.Int64ObservableUpDownCounter(
"db.client.connections.timeouts",
metric.WithDescription("The number of connection timeouts that have occurred trying to obtain a connection from the pool"),
)
if err != nil {
return err
}
redisConf := rdb.Options()
_, err = conf.meter.RegisterCallback(
func(ctx context.Context, o metric.Observer) error {
stats := rdb.PoolStats()
o.ObserveInt64(idleMax, int64(redisConf.MaxIdleConns), metric.WithAttributes(labels...))
o.ObserveInt64(idleMin, int64(redisConf.MinIdleConns), metric.WithAttributes(labels...))
o.ObserveInt64(connsMax, int64(redisConf.PoolSize), metric.WithAttributes(labels...))
o.ObserveInt64(usage, int64(stats.IdleConns), metric.WithAttributes(idleAttrs...))
o.ObserveInt64(usage, int64(stats.TotalConns-stats.IdleConns), metric.WithAttributes(usedAttrs...))
o.ObserveInt64(timeouts, int64(stats.Timeouts), metric.WithAttributes(labels...))
return nil
},
idleMax,
idleMin,
connsMax,
usage,
timeouts,
)
return err
}
func addMetricsHook(rdb *redis.Client, conf *config) error {
createTime, err := conf.meter.Float64Histogram(
"db.client.connections.create_time",
metric.WithDescription("The time it took to create a new connection."),
metric.WithUnit("ms"),
)
if err != nil {
return err
}
useTime, err := conf.meter.Float64Histogram(
"db.client.connections.use_time",
metric.WithDescription("The time between borrowing a connection and returning it to the pool."),
metric.WithUnit("ms"),
)
if err != nil {
return err
}
rdb.AddHook(&metricsHook{
createTime: createTime,
useTime: useTime,
attrs: conf.attrs,
})
return nil
}
type metricsHook struct {
createTime metric.Float64Histogram
useTime metric.Float64Histogram
attrs []attribute.KeyValue
}
var _ redis.Hook = (*metricsHook)(nil)
func (mh *metricsHook) DialHook(hook redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
start := time.Now()
conn, err := hook(ctx, network, addr)
dur := time.Since(start)
attrs := make([]attribute.KeyValue, 0, len(mh.attrs)+1)
attrs = append(attrs, mh.attrs...)
attrs = append(attrs, statusAttr(err))
mh.createTime.Record(ctx, milliseconds(dur), metric.WithAttributes(attrs...))
return conn, err
}
}
func (mh *metricsHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
start := time.Now()
err := hook(ctx, cmd)
dur := time.Since(start)
attrs := make([]attribute.KeyValue, 0, len(mh.attrs)+2)
attrs = append(attrs, mh.attrs...)
attrs = append(attrs, attribute.String("type", "command"))
attrs = append(attrs, statusAttr(err))
mh.useTime.Record(ctx, milliseconds(dur), metric.WithAttributes(attrs...))
return err
}
}
func (mh *metricsHook) ProcessPipelineHook(
hook redis.ProcessPipelineHook,
) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
start := time.Now()
err := hook(ctx, cmds)
dur := time.Since(start)
attrs := make([]attribute.KeyValue, 0, len(mh.attrs)+2)
attrs = append(attrs, mh.attrs...)
attrs = append(attrs, attribute.String("type", "pipeline"))
attrs = append(attrs, statusAttr(err))
mh.useTime.Record(ctx, milliseconds(dur), metric.WithAttributes(attrs...))
return err
}
}
func milliseconds(d time.Duration) float64 {
return float64(d) / float64(time.Millisecond)
}
func statusAttr(err error) attribute.KeyValue {
if err != nil {
return attribute.String("status", "error")
}
return attribute.String("status", "ok")
}

View File

@ -0,0 +1,61 @@
package redisotel
import (
"context"
"testing"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"github.com/redis/go-redis/v9"
)
type providerFunc func(name string, opts ...trace.TracerOption) trace.TracerProvider
func (fn providerFunc) TracerProvider(name string, opts ...trace.TracerOption) trace.TracerProvider {
return fn(name, opts...)
}
func TestNewWithTracerProvider(t *testing.T) {
invoked := false
tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.TracerProvider {
invoked = true
return otel.GetTracerProvider()
})
_ = newTracingHook("redis-hook", WithTracerProvider(tp.TracerProvider("redis-test")))
if !invoked {
t.Fatalf("did not call custom TraceProvider")
}
}
func TestWithDBStatement(t *testing.T) {
provider := sdktrace.NewTracerProvider()
hook := newTracingHook(
"",
WithTracerProvider(provider),
WithDBStatement(false),
)
ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test")
cmd := redis.NewCmd(ctx, "ping")
defer span.End()
processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error {
attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes()
for _, attr := range attrs {
if attr.Key == semconv.DBStatementKey {
t.Fatal("Attribute with db statement should not exist")
}
}
return nil
})
err := processHook(ctx, cmd)
if err != nil {
t.Fatal(err)
}
}

232
extra/redisotel/tracing.go Normal file
View File

@ -0,0 +1,232 @@
package redisotel
import (
"context"
"fmt"
"net"
"runtime"
"strconv"
"strings"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
"github.com/redis/go-redis/extra/rediscmd/v9"
"github.com/redis/go-redis/v9"
)
const (
instrumName = "github.com/redis/go-redis/extra/redisotel"
)
func InstrumentTracing(rdb redis.UniversalClient, opts ...TracingOption) error {
switch rdb := rdb.(type) {
case *redis.Client:
opt := rdb.Options()
connString := formatDBConnString(opt.Network, opt.Addr)
opts = addServerAttributes(opts, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
return nil
case *redis.ClusterClient:
rdb.AddHook(newTracingHook("", opts...))
rdb.OnNewNode(func(rdb *redis.Client) {
opt := rdb.Options()
opts = addServerAttributes(opts, opt.Addr)
connString := formatDBConnString(opt.Network, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
})
return nil
case *redis.Ring:
rdb.AddHook(newTracingHook("", opts...))
rdb.OnNewNode(func(rdb *redis.Client) {
opt := rdb.Options()
opts = addServerAttributes(opts, opt.Addr)
connString := formatDBConnString(opt.Network, opt.Addr)
rdb.AddHook(newTracingHook(connString, opts...))
})
return nil
default:
return fmt.Errorf("redisotel: %T not supported", rdb)
}
}
type tracingHook struct {
conf *config
spanOpts []trace.SpanStartOption
}
var _ redis.Hook = (*tracingHook)(nil)
func newTracingHook(connString string, opts ...TracingOption) *tracingHook {
baseOpts := make([]baseOption, len(opts))
for i, opt := range opts {
baseOpts[i] = opt
}
conf := newConfig(baseOpts...)
if conf.tracer == nil {
conf.tracer = conf.tp.Tracer(
instrumName,
trace.WithInstrumentationVersion("semver:"+redis.Version()),
)
}
if connString != "" {
conf.attrs = append(conf.attrs, semconv.DBConnectionString(connString))
}
return &tracingHook{
conf: conf,
spanOpts: []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(conf.attrs...),
},
}
}
func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
ctx, span := th.conf.tracer.Start(ctx, "redis.dial", th.spanOpts...)
defer span.End()
conn, err := hook(ctx, network, addr)
if err != nil {
recordError(span, err)
return nil, err
}
return conn, nil
}
}
func (th *tracingHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
fn, file, line := funcFileLine("github.com/redis/go-redis")
attrs := make([]attribute.KeyValue, 0, 8)
attrs = append(attrs,
semconv.CodeFunction(fn),
semconv.CodeFilepath(file),
semconv.CodeLineNumber(line),
)
if th.conf.dbStmtEnabled {
cmdString := rediscmd.CmdString(cmd)
attrs = append(attrs, semconv.DBStatement(cmdString))
}
opts := th.spanOpts
opts = append(opts, trace.WithAttributes(attrs...))
ctx, span := th.conf.tracer.Start(ctx, cmd.FullName(), opts...)
defer span.End()
if err := hook(ctx, cmd); err != nil {
recordError(span, err)
return err
}
return nil
}
}
func (th *tracingHook) ProcessPipelineHook(
hook redis.ProcessPipelineHook,
) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
fn, file, line := funcFileLine("github.com/redis/go-redis")
attrs := make([]attribute.KeyValue, 0, 8)
attrs = append(attrs,
semconv.CodeFunction(fn),
semconv.CodeFilepath(file),
semconv.CodeLineNumber(line),
attribute.Int("db.redis.num_cmd", len(cmds)),
)
summary, cmdsString := rediscmd.CmdsString(cmds)
if th.conf.dbStmtEnabled {
attrs = append(attrs, semconv.DBStatement(cmdsString))
}
opts := th.spanOpts
opts = append(opts, trace.WithAttributes(attrs...))
ctx, span := th.conf.tracer.Start(ctx, "redis.pipeline "+summary, opts...)
defer span.End()
if err := hook(ctx, cmds); err != nil {
recordError(span, err)
return err
}
return nil
}
}
func recordError(span trace.Span, err error) {
if err != redis.Nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
}
func formatDBConnString(network, addr string) string {
if network == "tcp" {
network = "redis"
}
return fmt.Sprintf("%s://%s", network, addr)
}
func funcFileLine(pkg string) (string, string, int) {
const depth = 16
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
ff := runtime.CallersFrames(pcs[:n])
var fn, file string
var line int
for {
f, ok := ff.Next()
if !ok {
break
}
fn, file, line = f.Function, f.File, f.Line
if !strings.Contains(fn, pkg) {
break
}
}
if ind := strings.LastIndexByte(fn, '/'); ind != -1 {
fn = fn[ind+1:]
}
return fn, file, line
}
// Database span attributes semantic conventions recommended server address and port
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/#connection-level-attributes
func addServerAttributes(opts []TracingOption, addr string) []TracingOption {
host, portString, err := net.SplitHostPort(addr)
if err != nil {
return opts
}
opts = append(opts, WithAttributes(
semconv.ServerAddress(host),
))
// Parse the port string to an integer
port, err := strconv.Atoi(portString)
if err != nil {
return opts
}
opts = append(opts, WithAttributes(
semconv.ServerPort(port),
))
return opts
}

View File

@ -0,0 +1,26 @@
# Prometheus Metric Collector
This package implements a [`prometheus.Collector`](https://pkg.go.dev/github.com/prometheus/client_golang@v1.12.2/prometheus#Collector)
for collecting metrics about the connection pool used by the various redis clients.
Supported clients are `redis.Client`, `redis.ClusterClient`, `redis.Ring` and `redis.UniversalClient`.
### Example
```go
client := redis.NewClient(options)
collector := redisprometheus.NewCollector(namespace, subsystem, client)
prometheus.MustRegister(collector)
```
### Metrics
| Name | Type | Description |
|---------------------------|----------------|-----------------------------------------------------------------------------|
| `pool_hit_total` | Counter metric | number of times a connection was found in the pool |
| `pool_miss_total` | Counter metric | number of times a connection was not found in the pool |
| `pool_timeout_total` | Counter metric | number of times a timeout occurred when getting a connection from the pool |
| `pool_conn_total_current` | Gauge metric | current number of connections in the pool |
| `pool_conn_idle_current` | Gauge metric | current number of idle connections in the pool |
| `pool_conn_stale_total` | Counter metric | number of times a connection was removed from the pool because it was stale |

View File

@ -0,0 +1,117 @@
package redisprometheus
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/redis/go-redis/v9"
)
// StatGetter provides a method to get pool statistics.
type StatGetter interface {
PoolStats() *redis.PoolStats
}
// Collector collects statistics from a redis client.
// It implements the prometheus.Collector interface.
type Collector struct {
getter StatGetter
hitDesc *prometheus.Desc
missDesc *prometheus.Desc
timeoutDesc *prometheus.Desc
totalDesc *prometheus.Desc
idleDesc *prometheus.Desc
staleDesc *prometheus.Desc
}
var _ prometheus.Collector = (*Collector)(nil)
// NewCollector returns a new Collector based on the provided StatGetter.
// The given namespace and subsystem are used to build the fully qualified metric name,
// i.e. "{namespace}_{subsystem}_{metric}".
// The provided metrics are:
// - pool_hit_total
// - pool_miss_total
// - pool_timeout_total
// - pool_conn_total_current
// - pool_conn_idle_current
// - pool_conn_stale_total
func NewCollector(namespace, subsystem string, getter StatGetter) *Collector {
return &Collector{
getter: getter,
hitDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "pool_hit_total"),
"Number of times a connection was found in the pool",
nil, nil,
),
missDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "pool_miss_total"),
"Number of times a connection was not found in the pool",
nil, nil,
),
timeoutDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "pool_timeout_total"),
"Number of times a timeout occurred when looking for a connection in the pool",
nil, nil,
),
totalDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "pool_conn_total_current"),
"Current number of connections in the pool",
nil, nil,
),
idleDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "pool_conn_idle_current"),
"Current number of idle connections in the pool",
nil, nil,
),
staleDesc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "pool_conn_stale_total"),
"Number of times a connection was removed from the pool because it was stale",
nil, nil,
),
}
}
// Describe implements the prometheus.Collector interface.
func (s *Collector) Describe(descs chan<- *prometheus.Desc) {
descs <- s.hitDesc
descs <- s.missDesc
descs <- s.timeoutDesc
descs <- s.totalDesc
descs <- s.idleDesc
descs <- s.staleDesc
}
// Collect implements the prometheus.Collector interface.
func (s *Collector) Collect(metrics chan<- prometheus.Metric) {
stats := s.getter.PoolStats()
metrics <- prometheus.MustNewConstMetric(
s.hitDesc,
prometheus.CounterValue,
float64(stats.Hits),
)
metrics <- prometheus.MustNewConstMetric(
s.missDesc,
prometheus.CounterValue,
float64(stats.Misses),
)
metrics <- prometheus.MustNewConstMetric(
s.timeoutDesc,
prometheus.CounterValue,
float64(stats.Timeouts),
)
metrics <- prometheus.MustNewConstMetric(
s.totalDesc,
prometheus.GaugeValue,
float64(stats.TotalConns),
)
metrics <- prometheus.MustNewConstMetric(
s.idleDesc,
prometheus.GaugeValue,
float64(stats.IdleConns),
)
metrics <- prometheus.MustNewConstMetric(
s.staleDesc,
prometheus.CounterValue,
float64(stats.StaleConns),
)
}

View File

@ -0,0 +1,23 @@
module github.com/redis/go-redis/extra/redisprometheus/v9
go 1.19
replace github.com/redis/go-redis/v9 => ../..
require (
github.com/prometheus/client_golang v1.14.0
github.com/redis/go-redis/v9 v9.5.3
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
golang.org/x/sys v0.4.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)

View File

@ -0,0 +1,33 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=

49
fuzz/fuzz.go Normal file
View File

@ -0,0 +1,49 @@
//go:build gofuzz
// +build gofuzz
package fuzz
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
var (
ctx = context.Background()
rdb *redis.Client
)
func init() {
rdb = redis.NewClient(&redis.Options{
Addr: ":6379",
DialTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
PoolSize: 10,
PoolTimeout: 10 * time.Second,
})
}
func Fuzz(data []byte) int {
arrayLen := len(data)
if arrayLen < 4 {
return -1
}
maxIter := int(uint(data[0]))
for i := 0; i < maxIter && i < arrayLen; i++ {
n := i % arrayLen
if n == 0 {
_ = rdb.Set(ctx, string(data[i:]), string(data[i:]), 0).Err()
} else if n == 1 {
_, _ = rdb.Get(ctx, string(data[i:])).Result()
} else if n == 2 {
_, _ = rdb.Incr(ctx, string(data[i:])).Result()
} else if n == 3 {
var cursor uint64
_, _, _ = rdb.Scan(ctx, cursor, string(data[i:]), 10).Result()
}
}
return 1
}

149
gears_commands.go Normal file
View File

@ -0,0 +1,149 @@
package redis
import (
"context"
"fmt"
"strings"
)
type GearsCmdable interface {
TFunctionLoad(ctx context.Context, lib string) *StatusCmd
TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd
TFunctionDelete(ctx context.Context, libName string) *StatusCmd
TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd
TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd
TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
}
type TFunctionLoadOptions struct {
Replace bool
Config string
}
type TFunctionListOptions struct {
Withcode bool
Verbose int
Library string
}
type TFCallOptions struct {
Keys []string
Arguments []string
}
// TFunctionLoad - load a new JavaScript library into Redis.
// For more information - https://redis.io/commands/tfunction-load/
func (c cmdable) TFunctionLoad(ctx context.Context, lib string) *StatusCmd {
args := []interface{}{"TFUNCTION", "LOAD", lib}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd {
args := []interface{}{"TFUNCTION", "LOAD"}
if options != nil {
if options.Replace {
args = append(args, "REPLACE")
}
if options.Config != "" {
args = append(args, "CONFIG", options.Config)
}
}
args = append(args, lib)
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFunctionDelete - delete a JavaScript library from Redis.
// For more information - https://redis.io/commands/tfunction-delete/
func (c cmdable) TFunctionDelete(ctx context.Context, libName string) *StatusCmd {
args := []interface{}{"TFUNCTION", "DELETE", libName}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFunctionList - list the functions with additional information about each function.
// For more information - https://redis.io/commands/tfunction-list/
func (c cmdable) TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd {
args := []interface{}{"TFUNCTION", "LIST"}
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd {
args := []interface{}{"TFUNCTION", "LIST"}
if options != nil {
if options.Withcode {
args = append(args, "WITHCODE")
}
if options.Verbose != 0 {
v := strings.Repeat("v", options.Verbose)
args = append(args, v)
}
if options.Library != "" {
args = append(args, "LIBRARY", options.Library)
}
}
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFCall - invoke a function.
// For more information - https://redis.io/commands/tfcall/
func (c cmdable) TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
lf := libName + "." + funcName
args := []interface{}{"TFCALL", lf, numKeys}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
lf := libName + "." + funcName
args := []interface{}{"TFCALL", lf, numKeys}
if options != nil {
for _, key := range options.Keys {
args = append(args, key)
}
for _, key := range options.Arguments {
args = append(args, key)
}
}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFCallASYNC - invoke an asynchronous JavaScript function (coroutine).
// For more information - https://redis.io/commands/TFCallASYNC/
func (c cmdable) TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
lf := fmt.Sprintf("%s.%s", libName, funcName)
args := []interface{}{"TFCALLASYNC", lf, numKeys}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
lf := fmt.Sprintf("%s.%s", libName, funcName)
args := []interface{}{"TFCALLASYNC", lf, numKeys}
if options != nil {
for _, key := range options.Keys {
args = append(args, key)
}
for _, key := range options.Arguments {
args = append(args, key)
}
}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}

114
gears_commands_test.go Normal file
View File

@ -0,0 +1,114 @@
package redis_test
import (
"context"
"fmt"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
"github.com/redis/go-redis/v9"
)
func libCode(libName string) string {
return fmt.Sprintf("#!js api_version=1.0 name=%s\n redis.registerFunction('foo', ()=>{{return 'bar'}})", libName)
}
func libCodeWithConfig(libName string) string {
lib := `#!js api_version=1.0 name=%s
var last_update_field_name = "__last_update__"
if (redis.config.last_update_field_name !== undefined) {
if (typeof redis.config.last_update_field_name != 'string') {
throw "last_update_field_name must be a string";
}
last_update_field_name = redis.config.last_update_field_name
}
redis.registerFunction("hset", function(client, key, field, val){
// get the current time in ms
var curr_time = client.call("time")[0];
return client.call('hset', key, field, val, last_update_field_name, curr_time);
});`
return fmt.Sprintf(lib, libName)
}
var _ = Describe("RedisGears commands", Label("gears"), func() {
ctx := context.TODO()
var client *redis.Client
BeforeEach(func() {
client = redis.NewClient(&redis.Options{Addr: ":6379"})
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
client.TFunctionDelete(ctx, "lib1")
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
It("should TFunctionLoad, TFunctionLoadArgs and TFunctionDelete ", Label("gears", "tfunctionload"), func() {
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
opt := &redis.TFunctionLoadOptions{Replace: true, Config: `{"last_update_field_name":"last_update"}`}
resultAdd, err = client.TFunctionLoadArgs(ctx, libCodeWithConfig("lib1"), opt).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
})
It("should TFunctionList", Label("gears", "tfunctionlist"), func() {
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
resultList, err := client.TFunctionList(ctx).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultList[0]["engine"]).To(BeEquivalentTo("js"))
opt := &redis.TFunctionListOptions{Withcode: true, Verbose: 2}
resultListArgs, err := client.TFunctionListArgs(ctx, opt).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultListArgs[0]["code"]).NotTo(BeEquivalentTo(""))
})
It("should TFCall", Label("gears", "tfcall"), func() {
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
resultAdd, err = client.TFCall(ctx, "lib1", "foo", 0).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("bar"))
})
It("should TFCallArgs", Label("gears", "tfcallargs"), func() {
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}}
resultAdd, err = client.TFCallArgs(ctx, "lib1", "foo", 0, opt).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("bar"))
})
It("should TFCallASYNC", Label("gears", "TFCallASYNC"), func() {
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
resultAdd, err = client.TFCallASYNC(ctx, "lib1", "foo", 0).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("bar"))
})
It("should TFCallASYNCArgs", Label("gears", "TFCallASYNCargs"), func() {
var resultAdd interface{}
resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("OK"))
opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}}
resultAdd, err = client.TFCallASYNCArgs(ctx, "lib1", "foo", 0, opt).Result()
Expect(err).NotTo(HaveOccurred())
Expect(resultAdd).To(BeEquivalentTo("bar"))
})
})

384
generic_commands.go Normal file
View File

@ -0,0 +1,384 @@
package redis
import (
"context"
"time"
)
type GenericCmdable interface {
Del(ctx context.Context, keys ...string) *IntCmd
Dump(ctx context.Context, key string) *StringCmd
Exists(ctx context.Context, keys ...string) *IntCmd
Expire(ctx context.Context, key string, expiration time.Duration) *BoolCmd
ExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd
ExpireTime(ctx context.Context, key string) *DurationCmd
ExpireNX(ctx context.Context, key string, expiration time.Duration) *BoolCmd
ExpireXX(ctx context.Context, key string, expiration time.Duration) *BoolCmd
ExpireGT(ctx context.Context, key string, expiration time.Duration) *BoolCmd
ExpireLT(ctx context.Context, key string, expiration time.Duration) *BoolCmd
Keys(ctx context.Context, pattern string) *StringSliceCmd
Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd
Move(ctx context.Context, key string, db int) *BoolCmd
ObjectFreq(ctx context.Context, key string) *IntCmd
ObjectRefCount(ctx context.Context, key string) *IntCmd
ObjectEncoding(ctx context.Context, key string) *StringCmd
ObjectIdleTime(ctx context.Context, key string) *DurationCmd
Persist(ctx context.Context, key string) *BoolCmd
PExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd
PExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd
PExpireTime(ctx context.Context, key string) *DurationCmd
PTTL(ctx context.Context, key string) *DurationCmd
RandomKey(ctx context.Context) *StringCmd
Rename(ctx context.Context, key, newkey string) *StatusCmd
RenameNX(ctx context.Context, key, newkey string) *BoolCmd
Restore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd
RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd
Sort(ctx context.Context, key string, sort *Sort) *StringSliceCmd
SortRO(ctx context.Context, key string, sort *Sort) *StringSliceCmd
SortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd
SortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd
Touch(ctx context.Context, keys ...string) *IntCmd
TTL(ctx context.Context, key string) *DurationCmd
Type(ctx context.Context, key string) *StatusCmd
Copy(ctx context.Context, sourceKey string, destKey string, db int, replace bool) *IntCmd
Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd
ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *ScanCmd
}
func (c cmdable) Del(ctx context.Context, keys ...string) *IntCmd {
args := make([]interface{}, 1+len(keys))
args[0] = "del"
for i, key := range keys {
args[1+i] = key
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Unlink(ctx context.Context, keys ...string) *IntCmd {
args := make([]interface{}, 1+len(keys))
args[0] = "unlink"
for i, key := range keys {
args[1+i] = key
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Dump(ctx context.Context, key string) *StringCmd {
cmd := NewStringCmd(ctx, "dump", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Exists(ctx context.Context, keys ...string) *IntCmd {
args := make([]interface{}, 1+len(keys))
args[0] = "exists"
for i, key := range keys {
args[1+i] = key
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Expire(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
return c.expire(ctx, key, expiration, "")
}
func (c cmdable) ExpireNX(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
return c.expire(ctx, key, expiration, "NX")
}
func (c cmdable) ExpireXX(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
return c.expire(ctx, key, expiration, "XX")
}
func (c cmdable) ExpireGT(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
return c.expire(ctx, key, expiration, "GT")
}
func (c cmdable) ExpireLT(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
return c.expire(ctx, key, expiration, "LT")
}
func (c cmdable) expire(
ctx context.Context, key string, expiration time.Duration, mode string,
) *BoolCmd {
args := make([]interface{}, 3, 4)
args[0] = "expire"
args[1] = key
args[2] = formatSec(ctx, expiration)
if mode != "" {
args = append(args, mode)
}
cmd := NewBoolCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd {
cmd := NewBoolCmd(ctx, "expireat", key, tm.Unix())
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ExpireTime(ctx context.Context, key string) *DurationCmd {
cmd := NewDurationCmd(ctx, time.Second, "expiretime", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Keys(ctx context.Context, pattern string) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "keys", pattern)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd {
cmd := NewStatusCmd(
ctx,
"migrate",
host,
port,
key,
db,
formatMs(ctx, timeout),
)
cmd.setReadTimeout(timeout)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Move(ctx context.Context, key string, db int) *BoolCmd {
cmd := NewBoolCmd(ctx, "move", key, db)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ObjectFreq(ctx context.Context, key string) *IntCmd {
cmd := NewIntCmd(ctx, "object", "freq", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ObjectRefCount(ctx context.Context, key string) *IntCmd {
cmd := NewIntCmd(ctx, "object", "refcount", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ObjectEncoding(ctx context.Context, key string) *StringCmd {
cmd := NewStringCmd(ctx, "object", "encoding", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ObjectIdleTime(ctx context.Context, key string) *DurationCmd {
cmd := NewDurationCmd(ctx, time.Second, "object", "idletime", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Persist(ctx context.Context, key string) *BoolCmd {
cmd := NewBoolCmd(ctx, "persist", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) PExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
cmd := NewBoolCmd(ctx, "pexpire", key, formatMs(ctx, expiration))
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) PExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd {
cmd := NewBoolCmd(
ctx,
"pexpireat",
key,
tm.UnixNano()/int64(time.Millisecond),
)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) PExpireTime(ctx context.Context, key string) *DurationCmd {
cmd := NewDurationCmd(ctx, time.Millisecond, "pexpiretime", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) PTTL(ctx context.Context, key string) *DurationCmd {
cmd := NewDurationCmd(ctx, time.Millisecond, "pttl", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) RandomKey(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "randomkey")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Rename(ctx context.Context, key, newkey string) *StatusCmd {
cmd := NewStatusCmd(ctx, "rename", key, newkey)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) RenameNX(ctx context.Context, key, newkey string) *BoolCmd {
cmd := NewBoolCmd(ctx, "renamenx", key, newkey)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Restore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd {
cmd := NewStatusCmd(
ctx,
"restore",
key,
formatMs(ctx, ttl),
value,
)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd {
cmd := NewStatusCmd(
ctx,
"restore",
key,
formatMs(ctx, ttl),
value,
"replace",
)
_ = c(ctx, cmd)
return cmd
}
type Sort struct {
By string
Offset, Count int64
Get []string
Order string
Alpha bool
}
func (sort *Sort) args(command, key string) []interface{} {
args := []interface{}{command, key}
if sort.By != "" {
args = append(args, "by", sort.By)
}
if sort.Offset != 0 || sort.Count != 0 {
args = append(args, "limit", sort.Offset, sort.Count)
}
for _, get := range sort.Get {
args = append(args, "get", get)
}
if sort.Order != "" {
args = append(args, sort.Order)
}
if sort.Alpha {
args = append(args, "alpha")
}
return args
}
func (c cmdable) SortRO(ctx context.Context, key string, sort *Sort) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, sort.args("sort_ro", key)...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Sort(ctx context.Context, key string, sort *Sort) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, sort.args("sort", key)...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) SortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd {
args := sort.args("sort", key)
if store != "" {
args = append(args, "store", store)
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) SortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd {
cmd := NewSliceCmd(ctx, sort.args("sort", key)...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Touch(ctx context.Context, keys ...string) *IntCmd {
args := make([]interface{}, len(keys)+1)
args[0] = "touch"
for i, key := range keys {
args[i+1] = key
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TTL(ctx context.Context, key string) *DurationCmd {
cmd := NewDurationCmd(ctx, time.Second, "ttl", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Type(ctx context.Context, key string) *StatusCmd {
cmd := NewStatusCmd(ctx, "type", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Copy(ctx context.Context, sourceKey, destKey string, db int, replace bool) *IntCmd {
args := []interface{}{"copy", sourceKey, destKey, "DB", db}
if replace {
args = append(args, "REPLACE")
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
//------------------------------------------------------------------------------
func (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd {
args := []interface{}{"scan", cursor}
if match != "" {
args = append(args, "match", match)
}
if count > 0 {
args = append(args, "count", count)
}
cmd := NewScanCmd(ctx, c, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *ScanCmd {
args := []interface{}{"scan", cursor}
if match != "" {
args = append(args, "match", match)
}
if count > 0 {
args = append(args, "count", count)
}
if keyType != "" {
args = append(args, "type", keyType)
}
cmd := NewScanCmd(ctx, c, args...)
_ = c(ctx, cmd)
return cmd
}

155
geo_commands.go Normal file
View File

@ -0,0 +1,155 @@
package redis
import (
"context"
"errors"
)
type GeoCmdable interface {
GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd
GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd
GeoRadius(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd
GeoRadiusStore(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *IntCmd
GeoRadiusByMember(ctx context.Context, key, member string, query *GeoRadiusQuery) *GeoLocationCmd
GeoRadiusByMemberStore(ctx context.Context, key, member string, query *GeoRadiusQuery) *IntCmd
GeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd
GeoSearchLocation(ctx context.Context, key string, q *GeoSearchLocationQuery) *GeoSearchLocationCmd
GeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd
GeoDist(ctx context.Context, key string, member1, member2, unit string) *FloatCmd
GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd
}
func (c cmdable) GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd {
args := make([]interface{}, 2+3*len(geoLocation))
args[0] = "geoadd"
args[1] = key
for i, eachLoc := range geoLocation {
args[2+3*i] = eachLoc.Longitude
args[2+3*i+1] = eachLoc.Latitude
args[2+3*i+2] = eachLoc.Name
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// GeoRadius is a read-only GEORADIUS_RO command.
func (c cmdable) GeoRadius(
ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery,
) *GeoLocationCmd {
cmd := NewGeoLocationCmd(ctx, query, "georadius_ro", key, longitude, latitude)
if query.Store != "" || query.StoreDist != "" {
cmd.SetErr(errors.New("GeoRadius does not support Store or StoreDist"))
return cmd
}
_ = c(ctx, cmd)
return cmd
}
// GeoRadiusStore is a writing GEORADIUS command.
func (c cmdable) GeoRadiusStore(
ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery,
) *IntCmd {
args := geoLocationArgs(query, "georadius", key, longitude, latitude)
cmd := NewIntCmd(ctx, args...)
if query.Store == "" && query.StoreDist == "" {
cmd.SetErr(errors.New("GeoRadiusStore requires Store or StoreDist"))
return cmd
}
_ = c(ctx, cmd)
return cmd
}
// GeoRadiusByMember is a read-only GEORADIUSBYMEMBER_RO command.
func (c cmdable) GeoRadiusByMember(
ctx context.Context, key, member string, query *GeoRadiusQuery,
) *GeoLocationCmd {
cmd := NewGeoLocationCmd(ctx, query, "georadiusbymember_ro", key, member)
if query.Store != "" || query.StoreDist != "" {
cmd.SetErr(errors.New("GeoRadiusByMember does not support Store or StoreDist"))
return cmd
}
_ = c(ctx, cmd)
return cmd
}
// GeoRadiusByMemberStore is a writing GEORADIUSBYMEMBER command.
func (c cmdable) GeoRadiusByMemberStore(
ctx context.Context, key, member string, query *GeoRadiusQuery,
) *IntCmd {
args := geoLocationArgs(query, "georadiusbymember", key, member)
cmd := NewIntCmd(ctx, args...)
if query.Store == "" && query.StoreDist == "" {
cmd.SetErr(errors.New("GeoRadiusByMemberStore requires Store or StoreDist"))
return cmd
}
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) GeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd {
args := make([]interface{}, 0, 13)
args = append(args, "geosearch", key)
args = geoSearchArgs(q, args)
cmd := NewStringSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) GeoSearchLocation(
ctx context.Context, key string, q *GeoSearchLocationQuery,
) *GeoSearchLocationCmd {
args := make([]interface{}, 0, 16)
args = append(args, "geosearch", key)
args = geoSearchLocationArgs(q, args)
cmd := NewGeoSearchLocationCmd(ctx, q, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) GeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd {
args := make([]interface{}, 0, 15)
args = append(args, "geosearchstore", store, key)
args = geoSearchArgs(&q.GeoSearchQuery, args)
if q.StoreDist {
args = append(args, "storedist")
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) GeoDist(
ctx context.Context, key string, member1, member2, unit string,
) *FloatCmd {
if unit == "" {
unit = "km"
}
cmd := NewFloatCmd(ctx, "geodist", key, member1, member2, unit)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd {
args := make([]interface{}, 2+len(members))
args[0] = "geohash"
args[1] = key
for i, member := range members {
args[2+i] = member
}
cmd := NewStringSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd {
args := make([]interface{}, 2+len(members))
args[0] = "geopos"
args[1] = key
for i, member := range members {
args[2+i] = member
}
cmd := NewGeoPosCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module github.com/redis/go-redis/v9
go 1.18
require (
github.com/bsm/ginkgo/v2 v2.12.0
github.com/bsm/gomega v1.27.10
github.com/cespare/xxhash/v2 v2.2.0
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=

437
hash_commands.go Normal file
View File

@ -0,0 +1,437 @@
package redis
import (
"context"
"time"
)
type HashCmdable interface {
HDel(ctx context.Context, key string, fields ...string) *IntCmd
HExists(ctx context.Context, key, field string) *BoolCmd
HGet(ctx context.Context, key, field string) *StringCmd
HGetAll(ctx context.Context, key string) *MapStringStringCmd
HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd
HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd
HKeys(ctx context.Context, key string) *StringSliceCmd
HLen(ctx context.Context, key string) *IntCmd
HMGet(ctx context.Context, key string, fields ...string) *SliceCmd
HSet(ctx context.Context, key string, values ...interface{}) *IntCmd
HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd
HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd
HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
HVals(ctx context.Context, key string) *StringSliceCmd
HRandField(ctx context.Context, key string, count int) *StringSliceCmd
HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd
}
func (c cmdable) HDel(ctx context.Context, key string, fields ...string) *IntCmd {
args := make([]interface{}, 2+len(fields))
args[0] = "hdel"
args[1] = key
for i, field := range fields {
args[2+i] = field
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HExists(ctx context.Context, key, field string) *BoolCmd {
cmd := NewBoolCmd(ctx, "hexists", key, field)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HGet(ctx context.Context, key, field string) *StringCmd {
cmd := NewStringCmd(ctx, "hget", key, field)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HGetAll(ctx context.Context, key string) *MapStringStringCmd {
cmd := NewMapStringStringCmd(ctx, "hgetall", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd {
cmd := NewIntCmd(ctx, "hincrby", key, field, incr)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd {
cmd := NewFloatCmd(ctx, "hincrbyfloat", key, field, incr)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HKeys(ctx context.Context, key string) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "hkeys", key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HLen(ctx context.Context, key string) *IntCmd {
cmd := NewIntCmd(ctx, "hlen", key)
_ = c(ctx, cmd)
return cmd
}
// HMGet returns the values for the specified fields in the hash stored at key.
// It returns an interface{} to distinguish between empty string and nil value.
func (c cmdable) HMGet(ctx context.Context, key string, fields ...string) *SliceCmd {
args := make([]interface{}, 2+len(fields))
args[0] = "hmget"
args[1] = key
for i, field := range fields {
args[2+i] = field
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HSet accepts values in following formats:
//
// - HSet("myhash", "key1", "value1", "key2", "value2")
//
// - HSet("myhash", []string{"key1", "value1", "key2", "value2"})
//
// - HSet("myhash", map[string]interface{}{"key1": "value1", "key2": "value2"})
//
// Playing struct With "redis" tag.
// type MyHash struct { Key1 string `redis:"key1"`; Key2 int `redis:"key2"` }
//
// - HSet("myhash", MyHash{"value1", "value2"}) Warn: redis-server >= 4.0
//
// For struct, can be a structure pointer type, we only parse the field whose tag is redis.
// if you don't want the field to be read, you can use the `redis:"-"` flag to ignore it,
// or you don't need to set the redis tag.
// For the type of structure field, we only support simple data types:
// string, int/uint(8,16,32,64), float(32,64), time.Time(to RFC3339Nano), time.Duration(to Nanoseconds ),
// if you are other more complex or custom data types, please implement the encoding.BinaryMarshaler interface.
//
// Note that in older versions of Redis server(redis-server < 4.0), HSet only supports a single key-value pair.
// redis-docs: https://redis.io/commands/hset (Starting with Redis version 4.0.0: Accepts multiple field and value arguments.)
// If you are using a Struct type and the number of fields is greater than one,
// you will receive an error similar to "ERR wrong number of arguments", you can use HMSet as a substitute.
func (c cmdable) HSet(ctx context.Context, key string, values ...interface{}) *IntCmd {
args := make([]interface{}, 2, 2+len(values))
args[0] = "hset"
args[1] = key
args = appendArgs(args, values)
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HMSet is a deprecated version of HSet left for compatibility with Redis 3.
func (c cmdable) HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd {
args := make([]interface{}, 2, 2+len(values))
args[0] = "hmset"
args[1] = key
args = appendArgs(args, values)
cmd := NewBoolCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd {
cmd := NewBoolCmd(ctx, "hsetnx", key, field, value)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HVals(ctx context.Context, key string) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "hvals", key)
_ = c(ctx, cmd)
return cmd
}
// HRandField redis-server version >= 6.2.0.
func (c cmdable) HRandField(ctx context.Context, key string, count int) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "hrandfield", key, count)
_ = c(ctx, cmd)
return cmd
}
// HRandFieldWithValues redis-server version >= 6.2.0.
func (c cmdable) HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd {
cmd := NewKeyValueSliceCmd(ctx, "hrandfield", key, count, "withvalues")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
args := []interface{}{"hscan", key, cursor}
if match != "" {
args = append(args, "match", match)
}
if count > 0 {
args = append(args, "count", count)
}
cmd := NewScanCmd(ctx, c, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
args := []interface{}{"hscan", key, cursor}
if match != "" {
args = append(args, "match", match)
}
if count > 0 {
args = append(args, "count", count)
}
args = append(args, "novalues")
cmd := NewScanCmd(ctx, c, args...)
_ = c(ctx, cmd)
return cmd
}
type HExpireArgs struct {
NX bool
XX bool
GT bool
LT bool
}
// HExpire - Sets the expiration time for specified fields in a hash in seconds.
// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
// For more information - https://redis.io/commands/hexpire/
func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRE", key, expiration, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HExpire - Sets the expiration time for specified fields in a hash in seconds.
// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.
// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
// For more information - https://redis.io/commands/hexpire/
func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRE", key, expiration}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPExpire - Sets the expiration time for specified fields in a hash in milliseconds.
// Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields.
// The command modifies the standard time.Duration to milliseconds for the Redis command.
// For more information - https://redis.io/commands/hpexpire/
func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration), "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration)}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds.
// Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields.
// The command sets absolute expiration times based on the UNIX timestamp provided.
// For more information - https://redis.io/commands/hexpireat/
func (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIREAT", key, tm.Unix(), "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIREAT", key, tm.Unix()}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds.
// Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds.
// For more information - https://redis.io/commands/hpexpireat/
func (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond), "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond)}
// only if one argument is true, we can add it to the args
// if more than one argument is true, it will cause an error
if expirationArgs.NX {
args = append(args, "NX")
} else if expirationArgs.XX {
args = append(args, "XX")
} else if expirationArgs.GT {
args = append(args, "GT")
} else if expirationArgs.LT {
args = append(args, "LT")
}
args = append(args, "FIELDS", len(fields))
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPersist - Removes the expiration time from specified fields in a hash.
// Accepts a key and the fields themselves.
// This command ensures that each field specified will have its expiration removed if present.
// For more information - https://redis.io/commands/hpersist/
func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPERSIST", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds.
// Requires a key and the fields themselves to fetch their expiration timestamps.
// This command returns the expiration times for each field or error/status codes for each field as specified.
// For more information - https://redis.io/commands/hexpiretime/
func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRETIME", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds.
// Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters.
// Provides the expiration timestamp for each field in milliseconds.
// For more information - https://redis.io/commands/hexpiretime/
func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRETIME", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds.
// Requires a key and the fields themselves. It returns the TTL for each specified field.
// This command fetches the TTL in seconds for each field or returns error/status codes as appropriate.
// For more information - https://redis.io/commands/httl/
func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HTTL", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds.
// Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields.
// This command provides the TTL in milliseconds for each field or returns error/status codes as needed.
// For more information - https://redis.io/commands/hpttl/
func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPTTL", key, "FIELDS", len(fields)}
for _, field := range fields {
args = append(args, field)
}
cmd := NewIntSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}

Some files were not shown because too many files have changed in this diff Show More