mirror of https://github.com/go-redis/redis.git
Init
This commit is contained in:
commit
0b95fd7fa5
|
@ -0,0 +1 @@
|
|||
doctests/* @dmaier-redislabs
|
|
@ -0,0 +1 @@
|
|||
custom: ['https://uptrace.dev/sponsor']
|
|
@ -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 -->
|
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
|
@ -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
|
||||
|
|
@ -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/**'
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 }}
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -0,0 +1,6 @@
|
|||
*.rdb
|
||||
testdata/*
|
||||
.idea/
|
||||
.DS_Store
|
||||
*.tar.gz
|
||||
*.dic
|
|
@ -0,0 +1,4 @@
|
|||
run:
|
||||
concurrency: 8
|
||||
deadline: 5m
|
||||
tests: false
|
|
@ -0,0 +1,4 @@
|
|||
semi: false
|
||||
singleQuote: true
|
||||
proseWrap: always
|
||||
printWidth: 100
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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))
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
|||
/*
|
||||
Package redis implements a Redis client.
|
||||
*/
|
||||
package redis
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
|
@ -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 .
|
||||
```
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
`)
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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]
|
|
@ -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"
|
|
@ -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:
|
|
@ -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
|
||||
)
|
|
@ -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 |
|
@ -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
|
|
@ -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 .
|
||||
```
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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])
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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 {}) {
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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"),
|
||||
)
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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)},
|
||||
))
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 |
|
||||
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"))
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
Loading…
Reference in New Issue