pax_global_header00006660000000000000000000000064145666341040014523gustar00rootroot0000000000000052 comment=4c535aa56d60a1dddd457a8e63caa463bcb5a70b redigo-1.9.2/000077500000000000000000000000001456663410400130055ustar00rootroot00000000000000redigo-1.9.2/.clog.toml000066400000000000000000000005401456663410400147030ustar00rootroot00000000000000[clog] repository = "https://github.com/gomodule/redigo" subtitle = "Release Notes" from-latest-tag = true [sections] "Refactors" = ["refactor"] "Chores" = ["chore"] "Continuous Integration" = ["ci"] "Improvements" = ["imp", "improvement"] "Features" = ["feat", "feature"] "Legacy" = ["legacy"] "QA" = ["qa", "test"] "Documentation" = ["doc", "docs"] redigo-1.9.2/.github/000077500000000000000000000000001456663410400143455ustar00rootroot00000000000000redigo-1.9.2/.github/CONTRIBUTING.md000066400000000000000000000003201456663410400165710ustar00rootroot00000000000000Ask questions at [StackOverflow](https://stackoverflow.com/questions/ask?tags=go+redis). [Open an issue](https://github.com/gomodule/redigo/issues/new) to discuss your plans before doing any work on Redigo. redigo-1.9.2/.github/ISSUE_TEMPLATE.md000066400000000000000000000001071456663410400170500ustar00rootroot00000000000000Ask questions at https://stackoverflow.com/questions/ask?tags=go+redis redigo-1.9.2/.github/workflows/000077500000000000000000000000001456663410400164025ustar00rootroot00000000000000redigo-1.9.2/.github/workflows/go-test.yml000066400000000000000000000014511456663410400205100ustar00rootroot00000000000000name: go-test on: push: tags: - v* branches: - master pull_request: jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: go-version: - '1.21' - '1.22' os: - 'ubuntu-latest' redis: - '7.2' - '7.0' - '6.2' - '6.0' name: Test go ${{ matrix.go-version }} redis ${{ matrix.redis }} on ${{ matrix.os }} steps: - name: Setup redis uses: shogo82148/actions-setup-redis@v1 with: redis-version: ${{ matrix.redis }} - name: Checkout code uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Go Test run: go test -race ./... redigo-1.9.2/.github/workflows/golangci-lint.yml000066400000000000000000000012741456663410400216600ustar00rootroot00000000000000name: golangci-lint on: push: tags: - v* branches: - master pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup golang uses: actions/setup-go@v4 with: go-version: '1.22' cache: false # Handled by golangci-lint. - name: Validate go mod run: | go mod tidy git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: version: v1.56.2 args: --out-format=colored-line-number redigo-1.9.2/.github/workflows/release-build.yml000066400000000000000000000012001456663410400216330ustar00rootroot00000000000000name: Build Release on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+*' permissions: contents: write jobs: goreleaser: name: Release Go Binary runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} redigo-1.9.2/.goreleaser.yaml000066400000000000000000000026411456663410400161020ustar00rootroot00000000000000# When adding options check the documentation at https://goreleaser.com builds: - skip: true release: header: | ### {{.Tag}} Release Notes ({{.Date}}) footer: | [Full Changelog](https://{{ .ModulePath }}/compare/{{ .PreviousTag }}...{{ .Tag }}) changelog: use: github sort: asc filters: exclude: - Merge pull request - Merge remote-tracking branch - Merge branch # Group commits messages by given regex and title. # Order value defines the order of the groups. # Proving no regex means all commits will be grouped under the default group. # Groups are disabled when using github-native, as it already groups things by itself. # Matches are performed against strings of the form: "[:] ". # # Default is no groups. groups: - title: Features regexp: '^.*?(feat|feature)(\([[:word:]]+\))??!?:.+$' order: 0 - title: 'Bug fixes' regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' order: 1 - title: 'Chores' regexp: '^.*?chore(\([[:word:]]+\))??!?:.+$' order: 2 - title: 'Quality' regexp: '^.*?(qa|test|tests)(\([[:word:]]+\))??!?:.+$' order: 3 - title: 'Documentation' regexp: '^.*?(doc|docs)(\([[:word:]]+\))??!?:.+$' order: 4 - title: 'Continuous Integration' regexp: '^.*?ci(\([[:word:]]+\))??!?:.+$' order: 5 - title: Other order: 999 redigo-1.9.2/.travis.yml000066400000000000000000000004111456663410400151120ustar00rootroot00000000000000language: go services: - redis-server go: - 1.13.x - 1.14.x - master matrix: allow_failures: - go: master script: - go get -t -v ./... - diff -u <(echo -n) <(gofmt -d .) - go vet $(go list ./... | grep -v /vendor/) - go test -v -race ./... redigo-1.9.2/LICENSE000066400000000000000000000236761456663410400140300ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS redigo-1.9.2/README.markdown000066400000000000000000000037461456663410400155200ustar00rootroot00000000000000Redigo ====== [![GoDoc](https://godoc.org/github.com/gomodule/redigo/redis?status.svg)](https://pkg.go.dev/github.com/gomodule/redigo/redis) Redigo is a [Go](http://golang.org/) client for the [Redis](http://redis.io/) database. Features ------- * A [Print-like](https://pkg.go.dev/github.com/gomodule/redigo/redis#hdr-Executing_Commands) API with support for all Redis commands. * [Pipelining](https://pkg.go.dev/github.com/gomodule/redigo/redis#hdr-Pipelining), including pipelined transactions. * [Publish/Subscribe](https://pkg.go.dev/github.com/gomodule/redigo/redis#hdr-Publish_and_Subscribe). * [Connection pooling](https://pkg.go.dev/github.com/gomodule/redigo/redis#Pool). * [Script helper type](https://pkg.go.dev/github.com/gomodule/redigo/redis#Script) with optimistic use of EVALSHA. * [Helper functions](https://pkg.go.dev/github.com/gomodule/redigo/redis#hdr-Reply_Helpers) for working with command replies. Documentation ------------- - [API Reference](https://pkg.go.dev/github.com/gomodule/redigo/redis) - [FAQ](https://github.com/gomodule/redigo/wiki/FAQ) - [Examples](https://pkg.go.dev/github.com/gomodule/redigo/redis#pkg-examples) Installation ------------ Install Redigo using the "go get" command: go get github.com/gomodule/redigo/redis The Go distribution is Redigo's only dependency. Related Projects ---------------- - [rafaeljusto/redigomock](https://pkg.go.dev/github.com/rafaeljusto/redigomock) - A mock library for Redigo. - [chasex/redis-go-cluster](https://github.com/chasex/redis-go-cluster) - A Redis cluster client implementation. - [FZambia/sentinel](https://github.com/FZambia/sentinel) - Redis Sentinel support for Redigo - [mna/redisc](https://github.com/mna/redisc) - Redis Cluster client built on top of Redigo Contributing ------------ See [CONTRIBUTING.md](https://github.com/gomodule/redigo/blob/master/.github/CONTRIBUTING.md). License ------- Redigo is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). redigo-1.9.2/go.mod000066400000000000000000000006711456663410400141170ustar00rootroot00000000000000module github.com/gomodule/redigo go 1.17 require github.com/stretchr/testify v1.8.4 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) retract ( v2.0.0+incompatible // Old development version not maintained or published. v1.8.10 // Incorrect version tag for feature. v0.0.0-do-not-use // Never used only present due to lack of retract. ) redigo-1.9.2/go.sum000066400000000000000000000027561456663410400141520ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= redigo-1.9.2/redis/000077500000000000000000000000001456663410400141135ustar00rootroot00000000000000redigo-1.9.2/redis/commandinfo.go000066400000000000000000000030261456663410400167350ustar00rootroot00000000000000// Copyright 2014 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "strings" ) const ( connectionWatchState = 1 << iota connectionMultiState connectionSubscribeState connectionMonitorState ) type commandInfo struct { // Set or Clear these states on connection. Set, Clear int } var commandInfos = map[string]commandInfo{ "WATCH": {Set: connectionWatchState}, "UNWATCH": {Clear: connectionWatchState}, "MULTI": {Set: connectionMultiState}, "EXEC": {Clear: connectionWatchState | connectionMultiState}, "DISCARD": {Clear: connectionWatchState | connectionMultiState}, "PSUBSCRIBE": {Set: connectionSubscribeState}, "SUBSCRIBE": {Set: connectionSubscribeState}, "MONITOR": {Set: connectionMonitorState}, } func init() { for n, ci := range commandInfos { commandInfos[strings.ToLower(n)] = ci } } func lookupCommandInfo(commandName string) commandInfo { if ci, ok := commandInfos[commandName]; ok { return ci } return commandInfos[strings.ToUpper(commandName)] } redigo-1.9.2/redis/commandinfo_test.go000066400000000000000000000012611456663410400177730ustar00rootroot00000000000000package redis import "testing" func TestLookupCommandInfo(t *testing.T) { for _, n := range []string{"watch", "WATCH", "wAtch"} { if lookupCommandInfo(n) == (commandInfo{}) { t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n) } } } func benchmarkLookupCommandInfo(b *testing.B, names ...string) { for i := 0; i < b.N; i++ { for _, c := range names { lookupCommandInfo(c) } } } func BenchmarkLookupCommandInfoCorrectCase(b *testing.B) { benchmarkLookupCommandInfo(b, "watch", "WATCH", "monitor", "MONITOR") } func BenchmarkLookupCommandInfoMixedCase(b *testing.B) { benchmarkLookupCommandInfo(b, "wAtch", "WeTCH", "monItor", "MONiTOR") } redigo-1.9.2/redis/conn.go000066400000000000000000000522331456663410400154040ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "bufio" "bytes" "context" "crypto/tls" "errors" "fmt" "io" "net" "net/url" "regexp" "strconv" "sync" "time" ) var ( _ ConnWithTimeout = (*conn)(nil) ) // conn is the low-level implementation of Conn type conn struct { // Shared mu sync.Mutex pending int err error conn net.Conn // Read readTimeout time.Duration br *bufio.Reader // Write writeTimeout time.Duration bw *bufio.Writer // Scratch space for formatting argument length. // '*' or '$', length, "\r\n" lenScratch [32]byte // Scratch space for formatting integers and floats. numScratch [40]byte } // DialTimeout acts like Dial but takes timeouts for establishing the // connection to the server, writing a command and reading a reply. // // Deprecated: Use Dial with options instead. func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) { return Dial(network, address, DialConnectTimeout(connectTimeout), DialReadTimeout(readTimeout), DialWriteTimeout(writeTimeout)) } // DialOption specifies an option for dialing a Redis server. type DialOption struct { f func(*dialOptions) } type dialOptions struct { readTimeout time.Duration writeTimeout time.Duration tlsHandshakeTimeout time.Duration dialer *net.Dialer dialContext func(ctx context.Context, network, addr string) (net.Conn, error) db int username string password string clientName string useTLS bool skipVerify bool tlsConfig *tls.Config } // DialTLSHandshakeTimeout specifies the maximum amount of time waiting to // wait for a TLS handshake. Zero means no timeout. // If no DialTLSHandshakeTimeout option is specified then the default is 30 seconds. func DialTLSHandshakeTimeout(d time.Duration) DialOption { return DialOption{func(do *dialOptions) { do.tlsHandshakeTimeout = d }} } // DialReadTimeout specifies the timeout for reading a single command reply. func DialReadTimeout(d time.Duration) DialOption { return DialOption{func(do *dialOptions) { do.readTimeout = d }} } // DialWriteTimeout specifies the timeout for writing a single command. func DialWriteTimeout(d time.Duration) DialOption { return DialOption{func(do *dialOptions) { do.writeTimeout = d }} } // DialConnectTimeout specifies the timeout for connecting to the Redis server when // no DialNetDial option is specified. // If no DialConnectTimeout option is specified then the default is 30 seconds. func DialConnectTimeout(d time.Duration) DialOption { return DialOption{func(do *dialOptions) { do.dialer.Timeout = d }} } // DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server // when no DialNetDial option is specified. // If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then // the default of 5 minutes is used to ensure that half-closed TCP sessions are detected. func DialKeepAlive(d time.Duration) DialOption { return DialOption{func(do *dialOptions) { do.dialer.KeepAlive = d }} } // DialNetDial specifies a custom dial function for creating TCP // connections, otherwise a net.Dialer customized via the other options is used. // DialNetDial overrides DialConnectTimeout and DialKeepAlive. func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { return DialOption{func(do *dialOptions) { do.dialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { return dial(network, addr) } }} } // DialContextFunc specifies a custom dial function with context for creating TCP // connections, otherwise a net.Dialer customized via the other options is used. // DialContextFunc overrides DialConnectTimeout and DialKeepAlive. func DialContextFunc(f func(ctx context.Context, network, addr string) (net.Conn, error)) DialOption { return DialOption{func(do *dialOptions) { do.dialContext = f }} } // DialDatabase specifies the database to select when dialing a connection. func DialDatabase(db int) DialOption { return DialOption{func(do *dialOptions) { do.db = db }} } // DialPassword specifies the password to use when connecting to // the Redis server. func DialPassword(password string) DialOption { return DialOption{func(do *dialOptions) { do.password = password }} } // DialUsername specifies the username to use when connecting to // the Redis server when Redis ACLs are used. // A DialPassword must also be passed otherwise this option will have no effect. func DialUsername(username string) DialOption { return DialOption{func(do *dialOptions) { do.username = username }} } // DialClientName specifies a client name to be used // by the Redis server connection. func DialClientName(name string) DialOption { return DialOption{func(do *dialOptions) { do.clientName = name }} } // DialTLSConfig specifies the config to use when a TLS connection is dialed. // Has no effect when not dialing a TLS connection. func DialTLSConfig(c *tls.Config) DialOption { return DialOption{func(do *dialOptions) { do.tlsConfig = c }} } // DialTLSSkipVerify disables server name verification when connecting over // TLS. Has no effect when not dialing a TLS connection. func DialTLSSkipVerify(skip bool) DialOption { return DialOption{func(do *dialOptions) { do.skipVerify = skip }} } // DialUseTLS specifies whether TLS should be used when connecting to the // server. This option is ignore by DialURL. func DialUseTLS(useTLS bool) DialOption { return DialOption{func(do *dialOptions) { do.useTLS = useTLS }} } // Dial connects to the Redis server at the given network and // address using the specified options. func Dial(network, address string, options ...DialOption) (Conn, error) { return DialContext(context.Background(), network, address, options...) } type tlsHandshakeTimeoutError struct{} func (tlsHandshakeTimeoutError) Timeout() bool { return true } func (tlsHandshakeTimeoutError) Temporary() bool { return true } func (tlsHandshakeTimeoutError) Error() string { return "TLS handshake timeout" } // DialContext connects to the Redis server at the given network and // address using the specified options and context. func DialContext(ctx context.Context, network, address string, options ...DialOption) (Conn, error) { do := dialOptions{ dialer: &net.Dialer{ Timeout: time.Second * 30, KeepAlive: time.Minute * 5, }, tlsHandshakeTimeout: time.Second * 10, } for _, option := range options { option.f(&do) } if do.dialContext == nil { do.dialContext = do.dialer.DialContext } netConn, err := do.dialContext(ctx, network, address) if err != nil { return nil, err } if do.useTLS { var tlsConfig *tls.Config if do.tlsConfig == nil { tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify} } else { tlsConfig = do.tlsConfig.Clone() } if tlsConfig.ServerName == "" { host, _, err := net.SplitHostPort(address) if err != nil { netConn.Close() return nil, err } tlsConfig.ServerName = host } tlsConn := tls.Client(netConn, tlsConfig) errc := make(chan error, 2) // buffered so we don't block timeout or Handshake if d := do.tlsHandshakeTimeout; d != 0 { timer := time.AfterFunc(d, func() { errc <- tlsHandshakeTimeoutError{} }) defer timer.Stop() } go func() { errc <- tlsConn.Handshake() }() if err := <-errc; err != nil { // Timeout or Handshake error. netConn.Close() // nolint: errcheck return nil, err } netConn = tlsConn } c := &conn{ conn: netConn, bw: bufio.NewWriter(netConn), br: bufio.NewReader(netConn), readTimeout: do.readTimeout, writeTimeout: do.writeTimeout, } if do.password != "" { authArgs := make([]interface{}, 0, 2) if do.username != "" { authArgs = append(authArgs, do.username) } authArgs = append(authArgs, do.password) if _, err := c.DoContext(ctx, "AUTH", authArgs...); err != nil { netConn.Close() return nil, err } } if do.clientName != "" { if _, err := c.DoContext(ctx, "CLIENT", "SETNAME", do.clientName); err != nil { netConn.Close() return nil, err } } if do.db != 0 { if _, err := c.DoContext(ctx, "SELECT", do.db); err != nil { netConn.Close() return nil, err } } return c, nil } var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`) // DialURL wraps DialURLContext using context.Background. func DialURL(rawurl string, options ...DialOption) (Conn, error) { ctx := context.Background() return DialURLContext(ctx, rawurl, options...) } // DialURLContext connects to a Redis server at the given URL using the Redis // URI scheme. URLs should follow the draft IANA specification for the // scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) (Conn, error) { u, err := url.Parse(rawurl) if err != nil { return nil, err } if u.Scheme != "redis" && u.Scheme != "rediss" { return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) } if u.Opaque != "" { return nil, fmt.Errorf("invalid redis URL, url is opaque: %s", rawurl) } // As per the IANA draft spec, the host defaults to localhost and // the port defaults to 6379. host, port, err := net.SplitHostPort(u.Host) if err != nil { // assume port is missing host = u.Host port = "6379" } if host == "" { host = "localhost" } address := net.JoinHostPort(host, port) if u.User != nil { password, isSet := u.User.Password() username := u.User.Username() if isSet { if username != "" { // ACL options = append(options, DialUsername(username), DialPassword(password)) } else { // requirepass - user-info username:password with blank username options = append(options, DialPassword(password)) } } else if username != "" { // requirepass - redis-cli compatibility which treats as single arg in user-info as a password options = append(options, DialPassword(username)) } } match := pathDBRegexp.FindStringSubmatch(u.Path) if len(match) == 2 { db := 0 if len(match[1]) > 0 { db, err = strconv.Atoi(match[1]) if err != nil { return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) } } if db != 0 { options = append(options, DialDatabase(db)) } } else if u.Path != "" { return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) } options = append(options, DialUseTLS(u.Scheme == "rediss")) return DialContext(ctx, "tcp", address, options...) } // NewConn returns a new Redigo connection for the given net connection. func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn { return &conn{ conn: netConn, bw: bufio.NewWriter(netConn), br: bufio.NewReader(netConn), readTimeout: readTimeout, writeTimeout: writeTimeout, } } func (c *conn) Close() error { c.mu.Lock() err := c.err if c.err == nil { c.err = errors.New("redigo: closed") err = c.conn.Close() } c.mu.Unlock() return err } func (c *conn) fatal(err error) error { c.mu.Lock() if c.err == nil { c.err = err // Close connection to force errors on subsequent calls and to unblock // other reader or writer. c.conn.Close() } c.mu.Unlock() return err } func (c *conn) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err } func (c *conn) writeLen(prefix byte, n int) error { c.lenScratch[len(c.lenScratch)-1] = '\n' c.lenScratch[len(c.lenScratch)-2] = '\r' i := len(c.lenScratch) - 3 for { c.lenScratch[i] = byte('0' + n%10) i -= 1 n = n / 10 if n == 0 { break } } c.lenScratch[i] = prefix _, err := c.bw.Write(c.lenScratch[i:]) return err } func (c *conn) writeString(s string) error { if err := c.writeLen('$', len(s)); err != nil { return err } if _, err := c.bw.WriteString(s); err != nil { return err } _, err := c.bw.WriteString("\r\n") return err } func (c *conn) writeBytes(p []byte) error { if err := c.writeLen('$', len(p)); err != nil { return err } if _, err := c.bw.Write(p); err != nil { return err } _, err := c.bw.WriteString("\r\n") return err } func (c *conn) writeInt64(n int64) error { return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10)) } func (c *conn) writeFloat64(n float64) error { return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64)) } func (c *conn) writeCommand(cmd string, args []interface{}) error { if err := c.writeLen('*', 1+len(args)); err != nil { return err } if err := c.writeString(cmd); err != nil { return err } for _, arg := range args { if err := c.writeArg(arg, true); err != nil { return err } } return nil } func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) { switch arg := arg.(type) { case string: return c.writeString(arg) case []byte: return c.writeBytes(arg) case int: return c.writeInt64(int64(arg)) case int64: return c.writeInt64(arg) case float64: return c.writeFloat64(arg) case bool: if arg { return c.writeString("1") } else { return c.writeString("0") } case nil: return c.writeString("") case Argument: if argumentTypeOK { return c.writeArg(arg.RedisArg(), false) } // See comment in default clause below. var buf bytes.Buffer fmt.Fprint(&buf, arg) return c.writeBytes(buf.Bytes()) default: // This default clause is intended to handle builtin numeric types. // The function should return an error for other types, but this is not // done for compatibility with previous versions of the package. var buf bytes.Buffer fmt.Fprint(&buf, arg) return c.writeBytes(buf.Bytes()) } } type protocolError string func (pe protocolError) Error() string { return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe)) } // readLine reads a line of input from the RESP stream. func (c *conn) readLine() ([]byte, error) { // To avoid allocations, attempt to read the line using ReadSlice. This // call typically succeeds. The known case where the call fails is when // reading the output from the MONITOR command. p, err := c.br.ReadSlice('\n') if err == bufio.ErrBufferFull { // The line does not fit in the bufio.Reader's buffer. Fall back to // allocating a buffer for the line. buf := append([]byte{}, p...) for err == bufio.ErrBufferFull { p, err = c.br.ReadSlice('\n') buf = append(buf, p...) } p = buf } if err != nil { return nil, err } i := len(p) - 2 if i < 0 || p[i] != '\r' { return nil, protocolError("bad response line terminator") } return p[:i], nil } // parseLen parses bulk string and array lengths. func parseLen(p []byte) (int, error) { if len(p) == 0 { return -1, protocolError("malformed length") } if p[0] == '-' && len(p) == 2 && p[1] == '1' { // handle $-1 and $-1 null replies. return -1, nil } var n int for _, b := range p { n *= 10 if b < '0' || b > '9' { return -1, protocolError("illegal bytes in length") } n += int(b - '0') } return n, nil } // parseInt parses an integer reply. func parseInt(p []byte) (interface{}, error) { if len(p) == 0 { return 0, protocolError("malformed integer") } var negate bool if p[0] == '-' { negate = true p = p[1:] if len(p) == 0 { return 0, protocolError("malformed integer") } } var n int64 for _, b := range p { n *= 10 if b < '0' || b > '9' { return 0, protocolError("illegal bytes in length") } n += int64(b - '0') } if negate { n = -n } return n, nil } var ( okReply interface{} = "OK" pongReply interface{} = "PONG" ) func (c *conn) readReply() (interface{}, error) { line, err := c.readLine() if err != nil { return nil, err } if len(line) == 0 { return nil, protocolError("short response line") } switch line[0] { case '+': switch string(line[1:]) { case "OK": // Avoid allocation for frequent "+OK" response. return okReply, nil case "PONG": // Avoid allocation in PING command benchmarks :) return pongReply, nil default: return string(line[1:]), nil } case '-': return Error(line[1:]), nil case ':': return parseInt(line[1:]) case '$': n, err := parseLen(line[1:]) if n < 0 || err != nil { return nil, err } p := make([]byte, n) _, err = io.ReadFull(c.br, p) if err != nil { return nil, err } if line, err := c.readLine(); err != nil { return nil, err } else if len(line) != 0 { return nil, protocolError("bad bulk string format") } return p, nil case '*': n, err := parseLen(line[1:]) if n < 0 || err != nil { return nil, err } r := make([]interface{}, n) for i := range r { r[i], err = c.readReply() if err != nil { return nil, err } } return r, nil } return nil, protocolError("unexpected response line") } func (c *conn) Send(cmd string, args ...interface{}) error { c.mu.Lock() c.pending += 1 c.mu.Unlock() if c.writeTimeout != 0 { if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil { return c.fatal(err) } } if err := c.writeCommand(cmd, args); err != nil { return c.fatal(err) } return nil } func (c *conn) Flush() error { if c.writeTimeout != 0 { if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil { return c.fatal(err) } } if err := c.bw.Flush(); err != nil { return c.fatal(err) } return nil } func (c *conn) Receive() (interface{}, error) { return c.ReceiveWithTimeout(c.readTimeout) } func (c *conn) ReceiveContext(ctx context.Context) (interface{}, error) { var realTimeout time.Duration if dl, ok := ctx.Deadline(); ok { timeout := time.Until(dl) if timeout >= c.readTimeout && c.readTimeout != 0 { realTimeout = c.readTimeout } else if timeout <= 0 { return nil, c.fatal(context.DeadlineExceeded) } else { realTimeout = timeout } } else { realTimeout = c.readTimeout } endch := make(chan struct{}) var r interface{} var e error go func() { defer close(endch) r, e = c.ReceiveWithTimeout(realTimeout) }() select { case <-ctx.Done(): return nil, c.fatal(ctx.Err()) case <-endch: return r, e } } func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) { var deadline time.Time if timeout != 0 { deadline = time.Now().Add(timeout) } if err := c.conn.SetReadDeadline(deadline); err != nil { return nil, c.fatal(err) } if reply, err = c.readReply(); err != nil { return nil, c.fatal(err) } // When using pub/sub, the number of receives can be greater than the // number of sends. To enable normal use of the connection after // unsubscribing from all channels, we do not decrement pending to a // negative value. // // The pending field is decremented after the reply is read to handle the // case where Receive is called before Send. c.mu.Lock() if c.pending > 0 { c.pending -= 1 } c.mu.Unlock() if err, ok := reply.(Error); ok { return nil, err } return } func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) { return c.DoWithTimeout(c.readTimeout, cmd, args...) } func (c *conn) DoContext(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) { var realTimeout time.Duration if dl, ok := ctx.Deadline(); ok { timeout := time.Until(dl) if timeout >= c.readTimeout && c.readTimeout != 0 { realTimeout = c.readTimeout } else if timeout <= 0 { return nil, c.fatal(context.DeadlineExceeded) } else { realTimeout = timeout } } else { realTimeout = c.readTimeout } endch := make(chan struct{}) var r interface{} var e error go func() { defer close(endch) r, e = c.DoWithTimeout(realTimeout, cmd, args...) }() select { case <-ctx.Done(): return nil, c.fatal(ctx.Err()) case <-endch: return r, e } } func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { c.mu.Lock() pending := c.pending c.pending = 0 c.mu.Unlock() if cmd == "" && pending == 0 { return nil, nil } if c.writeTimeout != 0 { if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil { return nil, c.fatal(err) } } if cmd != "" { if err := c.writeCommand(cmd, args); err != nil { return nil, c.fatal(err) } } if err := c.bw.Flush(); err != nil { return nil, c.fatal(err) } var deadline time.Time if readTimeout != 0 { deadline = time.Now().Add(readTimeout) } if err := c.conn.SetReadDeadline(deadline); err != nil { return nil, c.fatal(err) } if cmd == "" { reply := make([]interface{}, pending) for i := range reply { r, e := c.readReply() if e != nil { return nil, c.fatal(e) } reply[i] = r } return reply, nil } var err error var reply interface{} for i := 0; i <= pending; i++ { var e error if reply, e = c.readReply(); e != nil { return nil, c.fatal(e) } if e, ok := reply.(Error); ok && err == nil { err = e } } return reply, err } redigo-1.9.2/redis/conn_test.go000066400000000000000000000660571456663410400164540ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "bytes" "context" "crypto/tls" "crypto/x509" "fmt" "io" "math" "net" "sync" "os" "reflect" "strings" "testing" "time" "github.com/gomodule/redigo/redis" "github.com/stretchr/testify/require" ) type testConn struct { io.Reader io.Writer readDeadline time.Time writeDeadline time.Time } func (*testConn) Close() error { return nil } func (*testConn) LocalAddr() net.Addr { return nil } func (*testConn) RemoteAddr() net.Addr { return nil } func (c *testConn) SetDeadline(t time.Time) error { c.readDeadline = t c.writeDeadline = t return nil } func (c *testConn) SetReadDeadline(t time.Time) error { c.readDeadline = t; return nil } func (c *testConn) SetWriteDeadline(t time.Time) error { c.writeDeadline = t; return nil } func dialTestConn(r string, w io.Writer) redis.DialOption { return redis.DialNetDial(func(network, addr string) (net.Conn, error) { return &testConn{Reader: strings.NewReader(r), Writer: w}, nil }) } type tlsTestConn struct { net.Conn done chan struct{} } func (c *tlsTestConn) Close() error { c.Conn.Close() <-c.done return nil } func dialTestConnTLS(r string, w io.Writer) redis.DialOption { return redis.DialNetDial(func(network, addr string) (net.Conn, error) { client, server := net.Pipe() tlsServer := tls.Server(server, &serverTLSConfig) go io.Copy(tlsServer, strings.NewReader(r)) // nolint: errcheck done := make(chan struct{}) go func() { io.Copy(w, tlsServer) // nolint: errcheck close(done) }() return &tlsTestConn{Conn: client, done: done}, nil }) } type durationArg struct { time.Duration } func (t durationArg) RedisArg() interface{} { return t.Seconds() } type recursiveArg int func (v recursiveArg) RedisArg() interface{} { return v } var writeTests = []struct { args []interface{} expected string }{ { []interface{}{"SET", "key", "value"}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", }, { []interface{}{"SET", "key", "value"}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", }, { []interface{}{"SET", "key", byte(100)}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", }, { []interface{}{"SET", "key", 100}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", }, { []interface{}{"SET", "key", int64(math.MinInt64)}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$20\r\n-9223372036854775808\r\n", }, { []interface{}{"SET", "key", float64(1349673917.939762)}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$21\r\n1.349673917939762e+09\r\n", }, { []interface{}{"SET", "key", ""}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", }, { []interface{}{"SET", "key", nil}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", }, { []interface{}{"SET", "key", durationArg{time.Minute}}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$2\r\n60\r\n", }, { []interface{}{"SET", "key", recursiveArg(123)}, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n123\r\n", }, { []interface{}{"ECHO", true, false}, "*3\r\n$4\r\nECHO\r\n$1\r\n1\r\n$1\r\n0\r\n", }, } func TestWrite(t *testing.T) { for _, tt := range writeTests { var buf bytes.Buffer c, _ := redis.Dial("", "", dialTestConn("", &buf)) err := c.Send(tt.args[0].(string), tt.args[1:]...) if err != nil { t.Errorf("Send(%v) returned error %v", tt.args, err) continue } c.Flush() actual := buf.String() if actual != tt.expected { t.Errorf("Send(%v) = %q, want %q", tt.args, actual, tt.expected) } } } var errorSentinel = &struct{}{} var readTests = []struct { reply string expected interface{} }{ { "+OK\r\n", "OK", }, { "+PONG\r\n", "PONG", }, { "+OK\n\n", // no \r errorSentinel, }, { "@OK\r\n", errorSentinel, }, { "$6\r\nfoobar\r\n", []byte("foobar"), }, { "$-1\r\n", nil, }, { ":1\r\n", int64(1), }, { ":-2\r\n", int64(-2), }, { "*0\r\n", []interface{}{}, }, { "*-1\r\n", nil, }, { "*4\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$5\r\nHello\r\n$5\r\nWorld\r\n", []interface{}{[]byte("foo"), []byte("bar"), []byte("Hello"), []byte("World")}, }, { "*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n", []interface{}{[]byte("foo"), nil, []byte("bar")}, }, { // "" is not a valid length "$\r\nfoobar\r\n", errorSentinel, }, { // "x" is not a valid length "$x\r\nfoobar\r\n", errorSentinel, }, { // -2 is not a valid length "$-2\r\n", errorSentinel, }, { // "" is not a valid integer ":\r\n", errorSentinel, }, { // "x" is not a valid integer ":x\r\n", errorSentinel, }, { // missing \r\n following value "$6\r\nfoobar", errorSentinel, }, { // short value "$6\r\nxx", errorSentinel, }, { // long value "$6\r\nfoobarx\r\n", errorSentinel, }, } func TestRead(t *testing.T) { for _, tt := range readTests { c, _ := redis.Dial("", "", dialTestConn(tt.reply, nil)) actual, err := c.Receive() if tt.expected == errorSentinel { if err == nil { t.Errorf("Receive(%q) did not return expected error", tt.reply) } } else { if err != nil { t.Errorf("Receive(%q) returned error %v", tt.reply, err) continue } if !reflect.DeepEqual(actual, tt.expected) { t.Errorf("Receive(%q) = %v, want %v", tt.reply, actual, tt.expected) } } } } func TestReadString(t *testing.T) { // n is value of bufio.defaultBufSize const n = 4096 // Test read string lengths near bufio.Reader buffer boundaries. testRanges := [][2]int{{0, 64}, {n - 64, n + 64}, {2*n - 64, 2*n + 64}} p := make([]byte, 2*n+64) for i := range p { p[i] = byte('a' + i%26) } s := string(p) for _, r := range testRanges { for i := r[0]; i < r[1]; i++ { c, _ := redis.Dial("", "", dialTestConn("+"+s[:i]+"\r\n", nil)) actual, err := c.Receive() if err != nil || actual != s[:i] { t.Fatalf("Receive(string len %d) -> err=%v, equal=%v", i, err, actual != s[:i]) } } } } var testCommands = []struct { args []interface{} expected interface{} }{ { []interface{}{"PING"}, "PONG", }, { []interface{}{"SET", "foo", "bar"}, "OK", }, { []interface{}{"GET", "foo"}, []byte("bar"), }, { []interface{}{"GET", "nokey"}, nil, }, { []interface{}{"MGET", "nokey", "foo"}, []interface{}{nil, []byte("bar")}, }, { []interface{}{"INCR", "mycounter"}, int64(1), }, { []interface{}{"LPUSH", "mylist", "foo"}, int64(1), }, { []interface{}{"LPUSH", "mylist", "bar"}, int64(2), }, { []interface{}{"LRANGE", "mylist", 0, -1}, []interface{}{[]byte("bar"), []byte("foo")}, }, { []interface{}{"MULTI"}, "OK", }, { []interface{}{"LRANGE", "mylist", 0, -1}, "QUEUED", }, { []interface{}{"PING"}, "QUEUED", }, { []interface{}{"EXEC"}, []interface{}{ []interface{}{[]byte("bar"), []byte("foo")}, "PONG", }, }, } func TestDoCommands(t *testing.T) { c, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() for _, cmd := range testCommands { actual, err := c.Do(cmd.args[0].(string), cmd.args[1:]...) if err != nil { t.Errorf("Do(%v) returned error %v", cmd.args, err) continue } if !reflect.DeepEqual(actual, cmd.expected) { t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected) } } } func TestPipelineCommands(t *testing.T) { c, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() for _, cmd := range testCommands { if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { t.Fatalf("Send(%v) returned error %v", cmd.args, err) } } if err := c.Flush(); err != nil { t.Errorf("Flush() returned error %v", err) } for _, cmd := range testCommands { actual, err := c.Receive() if err != nil { t.Fatalf("Receive(%v) returned error %v", cmd.args, err) } if !reflect.DeepEqual(actual, cmd.expected) { t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) } } } func TestBlankCommand(t *testing.T) { c, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() for _, cmd := range testCommands { if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { t.Fatalf("Send(%v) returned error %v", cmd.args, err) } } reply, err := redis.Values(c.Do("")) if err != nil { t.Fatalf("Do() returned error %v", err) } if len(reply) != len(testCommands) { t.Fatalf("len(reply)=%d, want %d", len(reply), len(testCommands)) } for i, cmd := range testCommands { actual := reply[i] if !reflect.DeepEqual(actual, cmd.expected) { t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) } } } func TestRecvBeforeSend(t *testing.T) { c, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() done := make(chan struct{}) go func() { c.Receive() // nolint: errcheck close(done) }() time.Sleep(time.Millisecond) require.NoError(t, c.Send("PING")) require.NoError(t, c.Flush()) <-done _, err = c.Do("") if err != nil { t.Fatalf("error=%v", err) } } func TestError(t *testing.T) { c, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() _, err = c.Do("SET", "key", "val") require.NoError(t, err) _, err = c.Do("HSET", "key", "fld", "val") if err == nil { t.Errorf("Expected err for HSET on string key.") } if c.Err() != nil { t.Errorf("Conn has Err()=%v, expect nil", c.Err()) } _, err = c.Do("SET", "key", "val") if err != nil { t.Errorf("Do(SET, key, val) returned error %v, expected nil.", err) } } func TestReadTimeout(t *testing.T) { done := make(chan struct{}) errs := make(chan error, 2) defer func() { close(done) for err := range errs { require.NoError(t, err) } }() var wg sync.WaitGroup defer func() { wg.Wait() close(errs) }() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("net.Listen returned %v", err) } defer l.Close() wg.Add(1) go func() { defer wg.Done() for { c, err := l.Accept() if err != nil { return } wg.Add(1) go func() { defer wg.Done() to := time.NewTimer(time.Second) defer to.Stop() select { case <-to.C: case <-done: return } _, err := c.Write([]byte("+OK\r\n")) errs <- err c.Close() }() } }() // Do c1, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond)) if err != nil { t.Fatalf("redis.Dial returned %v", err) } defer c1.Close() _, err = c1.Do("PING") if err == nil { t.Fatalf("c1.Do() returned nil, expect error") } if c1.Err() == nil { t.Fatalf("c1.Err() = nil, expect error") } // Send/Flush/Receive c2, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond)) if err != nil { t.Fatalf("redis.Dial returned %v", err) } defer c2.Close() require.NoError(t, c2.Send("PING")) require.NoError(t, c2.Flush()) _, err = c2.Receive() if err == nil { t.Fatalf("c2.Receive() returned nil, expect error") } if c2.Err() == nil { t.Fatalf("c2.Err() = nil, expect error") } } func TestDialContextFunc(t *testing.T) { var isPassed bool f := func(ctx context.Context, network, addr string) (net.Conn, error) { isPassed = true return &testConn{}, nil } _, err := redis.DialContext(context.Background(), "", "", redis.DialContextFunc(f)) if err != nil { t.Fatalf("DialContext returned %v", err) } if !isPassed { t.Fatal("DialContextFunc not passed") } } func TestDialContext_CanceledContext(t *testing.T) { addr, err := redis.DefaultServerAddr() if err != nil { t.Fatalf("redis.DefaultServerAddr returned %v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err = redis.DialContext(ctx, "tcp", addr); err == nil { t.Fatalf("DialContext returned nil, expect error") } } var dialErrors = []struct { rawurl string expectedError string }{ { "localhost", "invalid redis URL scheme", }, // The error message for invalid hosts is different in different // versions of Go, so just check that there is an error message. { "redis://weird url", "", }, { "redis://foo:bar:baz", "", }, { "http://www.google.com", "invalid redis URL scheme: http", }, { "redis://localhost:6379/abc123", "invalid database: abc123", }, { "redis:foo//localhost:6379", "invalid redis URL, url is opaque: redis:foo//localhost:6379", }, } func TestDialURLErrors(t *testing.T) { for _, d := range dialErrors { _, err := redis.DialURL(d.rawurl) if err == nil || !strings.Contains(err.Error(), d.expectedError) { t.Errorf("DialURL did not return expected error (expected %v to contain %s)", err, d.expectedError) } } } func TestDialURLPort(t *testing.T) { checkPort := func(network, address string) (net.Conn, error) { if address != "localhost:6379" { t.Errorf("DialURL did not set port to 6379 by default (got %v)", address) } return nil, nil } _, err := redis.DialURL("redis://localhost", redis.DialNetDial(checkPort)) if err != nil { t.Error("dial error:", err) } } func TestDialURLHost(t *testing.T) { checkHost := func(network, address string) (net.Conn, error) { if address != "localhost:6379" { t.Errorf("DialURL did not set host to localhost by default (got %v)", address) } return nil, nil } _, err := redis.DialURL("redis://:6379", redis.DialNetDial(checkHost)) if err != nil { t.Error("dial error:", err) } } var dialURLTests = []struct { description string url string r string w string }{ {"password", "redis://:abc123@localhost", "+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"}, {"password redis-cli compat", "redis://abc123@localhost", "+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"}, {"password db1", "redis://:abc123@localhost/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"}, {"password db1 redis-cli compat", "redis://abc123@localhost/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"}, {"password no host db0", "redis://:abc123@/0", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"}, {"password no host db0 redis-cli compat", "redis://abc123@/0", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"}, {"password no host db1", "redis://:abc123@/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"}, {"password no host db1 redis-cli compat", "redis://abc123@/1", "+OK\r\n+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"}, {"username and password", "redis://user:password@localhost", "+OK\r\n", "*3\r\n$4\r\nAUTH\r\n$4\r\nuser\r\n$8\r\npassword\r\n"}, {"username", "redis://x:@localhost", "+OK\r\n", ""}, {"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"}, {"database 99", "redis://localhost/99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, {"no database", "redis://localhost/", "+OK\r\n", ""}, } func TestDialURL(t *testing.T) { for _, tt := range dialURLTests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer // UseTLS should be ignored in all of these tests. _, err := redis.DialURL(tt.url, dialTestConn(tt.r, &buf), redis.DialUseTLS(true)) if err != nil { t.Errorf("%s dial error: %v, buf: %v", tt.description, err, buf.String()) return } if w := buf.String(); w != tt.w { t.Errorf("%s commands = %q, want %q", tt.description, w, tt.w) } }) } } func checkPingPong(t *testing.T, buf *bytes.Buffer, c redis.Conn) { resp, err := c.Do("PING") if err != nil { t.Fatal("ping error:", err) } // Close connection to ensure that writes to buf are complete. c.Close() expected := "*1\r\n$4\r\nPING\r\n" actual := buf.String() if actual != expected { t.Errorf("commands = %q, want %q", actual, expected) } if resp != "PONG" { t.Errorf("resp = %v, want %v", resp, "PONG") } } const pingResponse = "+PONG\r\n" func TestDialURLTLS(t *testing.T) { var buf bytes.Buffer c, err := redis.DialURL("rediss://example.com/", redis.DialTLSConfig(&clientTLSConfig), dialTestConnTLS(pingResponse, &buf)) if err != nil { t.Fatal("dial error:", err) } checkPingPong(t, &buf, c) } func TestDialUseTLS(t *testing.T) { var buf bytes.Buffer c, err := redis.Dial("tcp", "example.com:6379", redis.DialTLSConfig(&clientTLSConfig), dialTestConnTLS(pingResponse, &buf), redis.DialUseTLS(true)) if err != nil { t.Fatal("dial error:", err) } checkPingPong(t, &buf, c) } type blockedReader struct { ch chan struct{} } func (b blockedReader) Read(p []byte) (n int, err error) { <-b.ch return 0, io.EOF } func dialTestBlockedConn(ch chan struct{}, w io.Writer) redis.DialOption { return redis.DialNetDial(func(network, addr string) (net.Conn, error) { return &testConn{Reader: blockedReader{ch: ch}, Writer: w}, nil }) } func TestDialTLSHandshakeTimeout(t *testing.T) { var buf bytes.Buffer ch := make(chan struct{}) var err error go func() { _, err = redis.Dial("tcp", "example.com:6379", redis.DialTLSConfig(&clientTLSConfig), redis.DialTLSHandshakeTimeout(time.Millisecond), dialTestBlockedConn(ch, &buf), redis.DialUseTLS(true)) close(ch) }() select { case <-time.After(time.Second): t.Fatal("dial didn't timeout") case <-ch: if err == nil { t.Fatal("dial didn't error") } else if err.Error() != "TLS handshake timeout" { t.Fatal("dial unexpected error:", err) } } } func TestDialTLSSKipVerify(t *testing.T) { var buf bytes.Buffer c, err := redis.Dial("tcp", "example.com:6379", dialTestConnTLS(pingResponse, &buf), redis.DialTLSSkipVerify(true), redis.DialUseTLS(true)) if err != nil { t.Fatal("dial error:", err) } checkPingPong(t, &buf, c) } func TestDialUseACL(t *testing.T) { var buf bytes.Buffer _, err := redis.Dial("tcp", "localhost:6379", redis.DialUsername("user"), redis.DialPassword("password"), dialTestConn(pingResponse, &buf)) if err != nil { t.Fatal("dial error:", err) } if err != nil { t.Fatal("dial error:", err) } expected := "*3\r\n$4\r\nAUTH\r\n$4\r\nuser\r\n$8\r\npassword\r\n" if w := buf.String(); w != expected { t.Errorf("got %q, want %q", w, expected) } } // Connect to an Redis instance using the Redis ACL system func ExampleDial_acl() { c, err := redis.Dial("tcp", "localhost:6379", redis.DialUsername("username"), redis.DialPassword("password"), ) if err != nil { // handle error } defer c.Close() } func TestDialClientName(t *testing.T) { var buf bytes.Buffer _, err := redis.Dial("tcp", ":6379", dialTestConn(pingResponse, &buf), redis.DialClientName("redis-connection"), ) if err != nil { t.Fatal("dial error:", err) } expected := "*3\r\n$6\r\nCLIENT\r\n$7\r\nSETNAME\r\n$16\r\nredis-connection\r\n" if w := buf.String(); w != expected { t.Errorf("got %q, want %q", w, expected) } // testing against a real server connectionName := "test-connection" c, err := redis.DialDefaultServer(redis.DialClientName(connectionName)) if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() v, err := c.Do("CLIENT", "GETNAME") if err != nil { t.Fatalf("CLIENT GETNAME returned error %v", err) } vs, err := redis.String(v, nil) if err != nil { t.Fatalf("String(v) returned error %v", err) } if vs != connectionName { t.Fatalf("wrong connection name. Got '%s', expected '%s'", vs, connectionName) } } // Connect to local instance of Redis running on the default port. func ExampleDial() { c, err := redis.Dial("tcp", ":6379") if err != nil { // handle error } defer c.Close() } // Connect to local instance of Redis running on the default port using the provided context. func ExampleDialContext() { ctx := context.Background() c, err := redis.DialContext(ctx, "tcp", ":6379") if err != nil { // handle error } defer c.Close() } // Connect to remote instance of Redis using a URL. func ExampleDialURL() { c, err := redis.DialURL(os.Getenv("REDIS_URL")) if err != nil { // handle connection error } defer c.Close() } // TextExecError tests handling of errors in a transaction. See // http://redis.io/topics/transactions for information on how Redis handles // errors in a transaction. func TestExecError(t *testing.T) { c, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() // Execute commands that fail before EXEC is called. _, err = c.Do("DEL", "k0") require.NoError(t, err) _, err = c.Do("ZADD", "k0", 0, 0) require.NoError(t, err) require.NoError(t, c.Send("MULTI")) require.NoError(t, c.Send("NOTACOMMAND", "k0", 0, 0)) require.NoError(t, c.Send("ZINCRBY", "k0", 0, 0)) v, err := c.Do("EXEC") if err == nil { t.Fatalf("EXEC returned values %v, expected error", v) } // Execute commands that fail after EXEC is called. The first command // returns an error. _, err = c.Do("DEL", "k1") require.NoError(t, err) _, err = c.Do("ZADD", "k1", 0, 0) require.NoError(t, err) require.NoError(t, c.Send("MULTI")) require.NoError(t, c.Send("HSET", "k1", 0, 0)) require.NoError(t, c.Send("ZINCRBY", "k1", 0, 0)) v, err = c.Do("EXEC") if err != nil { t.Fatalf("EXEC returned error %v", err) } vs, err := redis.Values(v, nil) if err != nil { t.Fatalf("Values(v) returned error %v", err) } if len(vs) != 2 { t.Fatalf("len(vs) == %d, want 2", len(vs)) } if _, ok := vs[0].(error); !ok { t.Fatalf("first result is type %T, expected error", vs[0]) } if _, ok := vs[1].([]byte); !ok { t.Fatalf("second result is type %T, expected []byte", vs[1]) } // Execute commands that fail after EXEC is called. The second command // returns an error. _, err = c.Do("ZADD", "k2", 0, 0) require.NoError(t, err) require.NoError(t, c.Send("MULTI")) require.NoError(t, c.Send("ZINCRBY", "k2", 0, 0)) require.NoError(t, c.Send("HSET", "k2", 0, 0)) v, err = c.Do("EXEC") if err != nil { t.Fatalf("EXEC returned error %v", err) } vs, err = redis.Values(v, nil) if err != nil { t.Fatalf("Values(v) returned error %v", err) } if len(vs) != 2 { t.Fatalf("len(vs) == %d, want 2", len(vs)) } if _, ok := vs[0].([]byte); !ok { t.Fatalf("first result is type %T, expected []byte", vs[0]) } if _, ok := vs[1].(error); !ok { t.Fatalf("second result is type %T, expected error", vs[2]) } } func BenchmarkDoEmpty(b *testing.B) { b.StopTimer() c, err := redis.DialDefaultServer() if err != nil { b.Fatal(err) } defer c.Close() b.StartTimer() for i := 0; i < b.N; i++ { if _, err := c.Do(""); err != nil { b.Fatal(err) } } } func BenchmarkDoPing(b *testing.B) { b.StopTimer() c, err := redis.DialDefaultServer() if err != nil { b.Fatal(err) } defer c.Close() b.StartTimer() for i := 0; i < b.N; i++ { if _, err := c.Do("PING"); err != nil { b.Fatal(err) } } } var clientTLSConfig, serverTLSConfig tls.Config func init() { // The certificate and key for testing TLS dial options was created // using the command // // go run GOROOT/src/crypto/tls/generate_cert.go \ // --rsa-bits 1024 \ // --host 127.0.0.1,::1,example.com --ca \ // --start-date "Jan 1 00:00:00 1970" \ // --duration=1000000h // // where GOROOT is the value of GOROOT reported by go env. localhostCert := []byte(` -----BEGIN CERTIFICATE----- MIICFDCCAX2gAwIBAgIRAJfBL4CUxkXcdlFurb3K+iowDQYJKoZIhvcNAQELBQAw EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw gYkCgYEArizw8WxMUQ3bGHLeuJ4fDrEpy+L2pqrbYRlKk1DasJ/VkB8bImzIpe6+ LGjiYIxvnDCOJ3f3QplcQuiuMyl6f2irJlJsbFT8Lo/3obnuTKAIaqUdJUqBg6y+ JaL8Auk97FvunfKFv8U1AIhgiLzAfQ/3Eaq1yi87Ra6pMjGbTtcCAwEAAaNoMGYw DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAdZ8daIVkyhVwflt5I19m0oq1TycbGO1+ ach7T6cZiBQeNR/SJtxr/wKPEpmvUgbv2BfFrKJ8QoIHYsbNSURTWSEa02pfw4k9 6RQhij3ZkG79Ituj5OYRORV6Z0HUW32r670BtcuHuAhq7YA6Nxy4FtSt7bAlVdRt rrKgNsltzMk= -----END CERTIFICATE-----`) localhostKey := []byte(` -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCuLPDxbExRDdsYct64nh8OsSnL4vamqtthGUqTUNqwn9WQHxsi bMil7r4saOJgjG+cMI4nd/dCmVxC6K4zKXp/aKsmUmxsVPwuj/ehue5MoAhqpR0l SoGDrL4lovwC6T3sW+6d8oW/xTUAiGCIvMB9D/cRqrXKLztFrqkyMZtO1wIDAQAB AoGACrc5G6FOEK6JjDeE/Fa+EmlT6PdNtXNNi+vCas3Opo8u1G8VfEi1D4BgstrB Eq+RLkrOdB8tVyuYQYWPMhabMqF+hhKJN72j0OwfuPlVvTInwb/cKjo/zbH1IA+Y HenHNK4ywv7/p/9/MvQPJ3I32cQBCgGUW5chVSH5M1sj5gECQQDabQAI1X0uDqCm KbX9gXVkAgxkFddrt6LBHt57xujFcqEKFE7nwKhDh7DweVs/VEJ+kpid4z+UnLOw KjtP9JolAkEAzCNBphQ//IsbH5rNs10wIUw3Ks/Oepicvr6kUFbIv+neRzi1iJHa m6H7EayK3PWgax6BAsR/t0Jc9XV7r2muSwJAVzN09BHnK+ADGtNEKLTqXMbEk6B0 pDhn7ZmZUOkUPN+Kky+QYM11X6Bob1jDqQDGmymDbGUxGO+GfSofC8inUQJAGfci Eo3g1a6b9JksMPRZeuLG4ZstGErxJRH6tH1Va5PDwitka8qhk8o2tTjNMO3NSdLH diKoXBcE2/Pll5pJoQJBAIMiiMIzXJhnN4mX8may44J/HvMlMf2xuVH2gNMwmZuc Bjqn3yoLHaoZVvbWOi0C2TCN4FjXjaLNZGifQPbIcaA= -----END RSA PRIVATE KEY-----`) cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { panic(fmt.Sprintf("error creating key pair: %v", err)) } serverTLSConfig.Certificates = []tls.Certificate{cert} certificate, err := x509.ParseCertificate(serverTLSConfig.Certificates[0].Certificate[0]) if err != nil { panic(fmt.Sprintf("error parsing x509 certificate: %v", err)) } clientTLSConfig.RootCAs = x509.NewCertPool() clientTLSConfig.RootCAs.AddCert(certificate) } func TestWithTimeout(t *testing.T) { for _, recv := range []bool{true, false} { for _, defaultTimout := range []time.Duration{0, time.Minute} { var buf bytes.Buffer nc := &testConn{Reader: strings.NewReader("+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n"), Writer: &buf} c, _ := redis.Dial("", "", redis.DialReadTimeout(defaultTimout), redis.DialNetDial(func(network, addr string) (net.Conn, error) { return nc, nil })) for i := 0; i < 4; i++ { var minDeadline, maxDeadline time.Time // Alternate between default and specified timeout. var err error if i%2 == 0 { if defaultTimout != 0 { minDeadline = time.Now().Add(defaultTimout) } if recv { _, err = c.Receive() } else { _, err = c.Do("PING") } require.NoError(t, err) if defaultTimout != 0 { maxDeadline = time.Now().Add(defaultTimout) } } else { timeout := 10 * time.Minute minDeadline = time.Now().Add(timeout) if recv { _, err = redis.ReceiveWithTimeout(c, timeout) } else { _, err = redis.DoWithTimeout(c, timeout, "PING") } require.NoError(t, err) maxDeadline = time.Now().Add(timeout) } // Expect set deadline in expected range. if nc.readDeadline.Before(minDeadline) || nc.readDeadline.After(maxDeadline) { t.Errorf("recv %v, %d: do deadline error: %v, %v, %v", recv, i, minDeadline, nc.readDeadline, maxDeadline) } } } } } redigo-1.9.2/redis/doc.go000066400000000000000000000147001456663410400152110ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Package redis is a client for the Redis database. // // The Redigo FAQ (https://github.com/gomodule/redigo/wiki/FAQ) contains more // documentation about this package. // // Connections // // The Conn interface is the primary interface for working with Redis. // Applications create connections by calling the Dial, DialWithTimeout or // NewConn functions. In the future, functions will be added for creating // sharded and other types of connections. // // The application must call the connection Close method when the application // is done with the connection. // // Executing Commands // // The Conn interface has a generic method for executing Redis commands: // // Do(commandName string, args ...interface{}) (reply interface{}, err error) // // The Redis command reference (http://redis.io/commands) lists the available // commands. An example of using the Redis APPEND command is: // // n, err := conn.Do("APPEND", "key", "value") // // The Do method converts command arguments to bulk strings for transmission // to the server as follows: // // Go Type Conversion // []byte Sent as is // string Sent as is // int, int64 strconv.FormatInt(v) // float64 strconv.FormatFloat(v, 'g', -1, 64) // bool true -> "1", false -> "0" // nil "" // all other types fmt.Fprint(w, v) // // Redis command reply types are represented using the following Go types: // // Redis type Go type // error redis.Error // integer int64 // simple string string // bulk string []byte or nil if value not present. // array []interface{} or nil if value not present. // // Use type assertions or the reply helper functions to convert from // interface{} to the specific Go type for the command result. // // Pipelining // // Connections support pipelining using the Send, Flush and Receive methods. // // Send(commandName string, args ...interface{}) error // Flush() error // Receive() (reply interface{}, err error) // // Send writes the command to the connection's output buffer. Flush flushes the // connection's output buffer to the server. Receive reads a single reply from // the server. The following example shows a simple pipeline. // // c.Send("SET", "foo", "bar") // c.Send("GET", "foo") // c.Flush() // c.Receive() // reply from SET // v, err = c.Receive() // reply from GET // // The Do method combines the functionality of the Send, Flush and Receive // methods. The Do method starts by writing the command and flushing the output // buffer. Next, the Do method receives all pending replies including the reply // for the command just sent by Do. If any of the received replies is an error, // then Do returns the error. If there are no errors, then Do returns the last // reply. If the command argument to the Do method is "", then the Do method // will flush the output buffer and receive pending replies without sending a // command. // // Use the Send and Do methods to implement pipelined transactions. // // c.Send("MULTI") // c.Send("INCR", "foo") // c.Send("INCR", "bar") // r, err := c.Do("EXEC") // fmt.Println(r) // prints [1, 1] // // Concurrency // // Connections support one concurrent caller to the Receive method and one // concurrent caller to the Send and Flush methods. No other concurrency is // supported including concurrent calls to the Do and Close methods. // // For full concurrent access to Redis, use the thread-safe Pool to get, use // and release a connection from within a goroutine. Connections returned from // a Pool have the concurrency restrictions described in the previous // paragraph. // // Publish and Subscribe // // Use the Send, Flush and Receive methods to implement Pub/Sub subscribers. // // c.Send("SUBSCRIBE", "example") // c.Flush() // for { // reply, err := c.Receive() // if err != nil { // return err // } // // process pushed message // } // // The PubSubConn type wraps a Conn with convenience methods for implementing // subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods // send and flush a subscription management command. The receive method // converts a pushed message to convenient types for use in a type switch. // // psc := redis.PubSubConn{Conn: c} // psc.Subscribe("example") // for { // switch v := psc.Receive().(type) { // case redis.Message: // fmt.Printf("%s: message: %s\n", v.Channel, v.Data) // case redis.Subscription: // fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count) // case error: // return v // } // } // // Reply Helpers // // The Bool, Int, Bytes, String, Strings and Values functions convert a reply // to a value of a specific type. To allow convenient wrapping of calls to the // connection Do and Receive methods, the functions take a second argument of // type error. If the error is non-nil, then the helper function returns the // error. If the error is nil, the function converts the reply to the specified // type: // // exists, err := redis.Bool(c.Do("EXISTS", "foo")) // if err != nil { // // handle error return from c.Do or type conversion error. // } // // The Scan function converts elements of a array reply to Go types: // // var value1 int // var value2 string // reply, err := redis.Values(c.Do("MGET", "key1", "key2")) // if err != nil { // // handle error // } // if _, err := redis.Scan(reply, &value1, &value2); err != nil { // // handle error // } // // Errors // // Connection methods return error replies from the server as type redis.Error. // // Call the connection Err() method to determine if the connection encountered // non-recoverable error such as a network error or protocol parsing error. If // Err() returns a non-nil value, then the connection is not usable and should // be closed. package redis redigo-1.9.2/redis/list_test.go000066400000000000000000000034361456663410400164620ustar00rootroot00000000000000// Copyright 2018 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // +build go1.9 package redis import "testing" func TestPoolList(t *testing.T) { var idle idleList var a, b, c poolConn check := func(pcs ...*poolConn) { if idle.count != len(pcs) { t.Fatal("idle.count != len(pcs)") } if len(pcs) == 0 { if idle.front != nil { t.Fatalf("front not nil") } if idle.back != nil { t.Fatalf("back not nil") } return } if idle.front != pcs[0] { t.Fatal("front != pcs[0]") } if idle.back != pcs[len(pcs)-1] { t.Fatal("back != pcs[len(pcs)-1]") } if idle.front.prev != nil { t.Fatal("front.prev != nil") } if idle.back.next != nil { t.Fatal("back.next != nil") } for i := 1; i < len(pcs)-1; i++ { if pcs[i-1].next != pcs[i] { t.Fatal("pcs[i-1].next != pcs[i]") } if pcs[i+1].prev != pcs[i] { t.Fatal("pcs[i+1].prev != pcs[i]") } } } idle.pushFront(&c) check(&c) idle.pushFront(&b) check(&b, &c) idle.pushFront(&a) check(&a, &b, &c) idle.popFront() check(&b, &c) idle.popFront() check(&c) idle.popFront() check() idle.pushFront(&c) check(&c) idle.pushFront(&b) check(&b, &c) idle.pushFront(&a) check(&a, &b, &c) idle.popBack() check(&a, &b) idle.popBack() check(&a) idle.popBack() check() } redigo-1.9.2/redis/log.go000066400000000000000000000102371456663410400152260ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "bytes" "context" "fmt" "log" "time" ) var ( _ ConnWithTimeout = (*loggingConn)(nil) ) // NewLoggingConn returns a logging wrapper around a connection. func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn { if prefix != "" { prefix = prefix + "." } return &loggingConn{conn, logger, prefix, nil} } //NewLoggingConnFilter returns a logging wrapper around a connection and a filter function. func NewLoggingConnFilter(conn Conn, logger *log.Logger, prefix string, skip func(cmdName string) bool) Conn { if prefix != "" { prefix = prefix + "." } return &loggingConn{conn, logger, prefix, skip} } type loggingConn struct { Conn logger *log.Logger prefix string skip func(cmdName string) bool } func (c *loggingConn) Close() error { err := c.Conn.Close() var buf bytes.Buffer fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err) c.logger.Output(2, buf.String()) // nolint: errcheck return err } func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) { const chop = 32 switch v := v.(type) { case []byte: if len(v) > chop { fmt.Fprintf(buf, "%q...", v[:chop]) } else { fmt.Fprintf(buf, "%q", v) } case string: if len(v) > chop { fmt.Fprintf(buf, "%q...", v[:chop]) } else { fmt.Fprintf(buf, "%q", v) } case []interface{}: if len(v) == 0 { buf.WriteString("[]") } else { sep := "[" fin := "]" if len(v) > chop { v = v[:chop] fin = "...]" } for _, vv := range v { buf.WriteString(sep) c.printValue(buf, vv) sep = ", " } buf.WriteString(fin) } default: fmt.Fprint(buf, v) } } func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) { if c.skip != nil && c.skip(commandName) { return } var buf bytes.Buffer fmt.Fprintf(&buf, "%s%s(", c.prefix, method) if method != "Receive" { buf.WriteString(commandName) for _, arg := range args { buf.WriteString(", ") c.printValue(&buf, arg) } } buf.WriteString(") -> (") if method != "Send" { c.printValue(&buf, reply) buf.WriteString(", ") } fmt.Fprintf(&buf, "%v)", err) c.logger.Output(3, buf.String()) // nolint: errcheck } func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) { reply, err := c.Conn.Do(commandName, args...) c.print("Do", commandName, args, reply, err) return reply, err } func (c *loggingConn) DoContext(ctx context.Context, commandName string, args ...interface{}) (interface{}, error) { reply, err := DoContext(c.Conn, ctx, commandName, args...) c.print("DoContext", commandName, args, reply, err) return reply, err } func (c *loggingConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (interface{}, error) { reply, err := DoWithTimeout(c.Conn, timeout, commandName, args...) c.print("DoWithTimeout", commandName, args, reply, err) return reply, err } func (c *loggingConn) Send(commandName string, args ...interface{}) error { err := c.Conn.Send(commandName, args...) c.print("Send", commandName, args, nil, err) return err } func (c *loggingConn) Receive() (interface{}, error) { reply, err := c.Conn.Receive() c.print("Receive", "", nil, reply, err) return reply, err } func (c *loggingConn) ReceiveContext(ctx context.Context) (interface{}, error) { reply, err := ReceiveContext(c.Conn, ctx) c.print("ReceiveContext", "", nil, reply, err) return reply, err } func (c *loggingConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) { reply, err := ReceiveWithTimeout(c.Conn, timeout) c.print("ReceiveWithTimeout", "", nil, reply, err) return reply, err } redigo-1.9.2/redis/pool.go000066400000000000000000000436411456663410400154230ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "bytes" "context" "crypto/rand" "crypto/sha1" "errors" "io" "strconv" "sync" "time" ) var ( _ ConnWithTimeout = (*activeConn)(nil) _ ConnWithTimeout = (*errorConn)(nil) ) var nowFunc = time.Now // for testing // ErrPoolExhausted is returned from a pool connection method (Do, Send, // Receive, Flush, Err) when the maximum number of database connections in the // pool has been reached. var ErrPoolExhausted = errors.New("redigo: connection pool exhausted") var ( errConnClosed = errors.New("redigo: connection closed") ) // Pool maintains a pool of connections. The application calls the Get method // to get a connection from the pool and the connection's Close method to // return the connection's resources to the pool. // // The following example shows how to use a pool in a web application. The // application creates a pool at application startup and makes it available to // request handlers using a package level variable. The pool configuration used // here is an example, not a recommendation. // // func newPool(addr string) *redis.Pool { // return &redis.Pool{ // MaxIdle: 3, // IdleTimeout: 240 * time.Second, // // Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial. // Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) }, // } // } // // var ( // pool *redis.Pool // redisServer = flag.String("redisServer", ":6379", "") // ) // // func main() { // flag.Parse() // pool = newPool(*redisServer) // ... // } // // A request handler gets a connection from the pool and closes the connection // when the handler is done: // // func serveHome(w http.ResponseWriter, r *http.Request) { // conn := pool.Get() // defer conn.Close() // ... // } // // Use the Dial function to authenticate connections with the AUTH command or // select a database with the SELECT command: // // pool := &redis.Pool{ // // Other pool configuration not shown in this example. // Dial: func () (redis.Conn, error) { // c, err := redis.Dial("tcp", server) // if err != nil { // return nil, err // } // if _, err := c.Do("AUTH", password); err != nil { // c.Close() // return nil, err // } // if _, err := c.Do("SELECT", db); err != nil { // c.Close() // return nil, err // } // return c, nil // }, // } // // Use the TestOnBorrow function to check the health of an idle connection // before the connection is returned to the application. This example PINGs // connections that have been idle more than a minute: // // pool := &redis.Pool{ // // Other pool configuration not shown in this example. // TestOnBorrow: func(c redis.Conn, t time.Time) error { // if time.Since(t) < time.Minute { // return nil // } // _, err := c.Do("PING") // return err // }, // } // type Pool struct { // Dial is an application supplied function for creating and configuring a // connection. // // The connection returned from Dial must not be in a special state // (subscribed to pubsub channel, transaction started, ...). Dial func() (Conn, error) // DialContext is an application supplied function for creating and configuring a // connection with the given context. // // The connection returned from DialContext must not be in a special state // (subscribed to pubsub channel, transaction started, ...). DialContext func(ctx context.Context) (Conn, error) // TestOnBorrow is an optional application supplied function for checking // the health of an idle connection before the connection is used again by // the application. Argument lastUsed is the time when the connection was returned // to the pool. If the function returns an error, then the connection is // closed. TestOnBorrow func(c Conn, lastUsed time.Time) error // TestOnBorrowContext is an optional application supplied function // for checking the health of an idle connection with the given context // before the connection is used again by the application. // Argument lastUsed is the time when the connection was returned // to the pool. If the function returns an error, then the connection is // closed. TestOnBorrowContext func(ctx context.Context, c Conn, lastUsed time.Time) error // Maximum number of idle connections in the pool. MaxIdle int // Maximum number of connections allocated by the pool at a given time. // When zero, there is no limit on the number of connections in the pool. MaxActive int // Close connections after remaining idle for this duration. If the value // is zero, then idle connections are not closed. Applications should set // the timeout to a value less than the server's timeout. IdleTimeout time.Duration // If Wait is true and the pool is at the MaxActive limit, then Get() waits // for a connection to be returned to the pool before returning. Wait bool // Close connections older than this duration. If the value is zero, then // the pool does not close connections based on age. MaxConnLifetime time.Duration mu sync.Mutex // mu protects the following fields closed bool // set to true when the pool is closed. active int // the number of open connections in the pool initOnce sync.Once // the init ch once func ch chan struct{} // limits open connections when p.Wait is true idle idleList // idle connections waitCount int64 // total number of connections waited for. waitDuration time.Duration // total time waited for new connections. } // NewPool creates a new pool. // // Deprecated: Initialize the Pool directly as shown in the example. func NewPool(newFn func() (Conn, error), maxIdle int) *Pool { return &Pool{Dial: newFn, MaxIdle: maxIdle} } // Get gets a connection. The application must close the returned connection. // This method always returns a valid connection so that applications can defer // error handling to the first use of the connection. If there is an error // getting an underlying connection, then the connection Err, Do, Send, Flush // and Receive methods return that error. func (p *Pool) Get() Conn { // GetContext returns errorConn in the first argument when an error occurs. c, _ := p.GetContext(context.Background()) return c } // GetContext gets a connection using the provided context. // // The provided Context must be non-nil. If the context expires before the // connection is complete, an error is returned. Any expiration on the context // will not affect the returned connection. // // If the function completes without error, then the application must close the // returned connection. func (p *Pool) GetContext(ctx context.Context) (Conn, error) { // Wait until there is a vacant connection in the pool. waited, err := p.waitVacantConn(ctx) if err != nil { return errorConn{err}, err } p.mu.Lock() if waited > 0 { p.waitCount++ p.waitDuration += waited } // Prune stale connections at the back of the idle list. if p.IdleTimeout > 0 { n := p.idle.count for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ { pc := p.idle.back p.idle.popBack() p.mu.Unlock() pc.c.Close() p.mu.Lock() p.active-- } } // Get idle connection from the front of idle list. for p.idle.front != nil { pc := p.idle.front p.idle.popFront() p.mu.Unlock() if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) && (p.TestOnBorrowContext == nil || p.TestOnBorrowContext(ctx, pc.c, pc.t) == nil) && (p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) { return &activeConn{p: p, pc: pc}, nil } pc.c.Close() p.mu.Lock() p.active-- } // Check for pool closed before dialing a new connection. if p.closed { p.mu.Unlock() err := errors.New("redigo: get on closed pool") return errorConn{err}, err } // Handle limit for p.Wait == false. if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive { p.mu.Unlock() return errorConn{ErrPoolExhausted}, ErrPoolExhausted } p.active++ p.mu.Unlock() c, err := p.dial(ctx) if err != nil { p.mu.Lock() p.active-- if p.ch != nil && !p.closed { p.ch <- struct{}{} } p.mu.Unlock() return errorConn{err}, err } return &activeConn{p: p, pc: &poolConn{c: c, created: nowFunc()}}, nil } // PoolStats contains pool statistics. type PoolStats struct { // ActiveCount is the number of connections in the pool. The count includes // idle connections and connections in use. ActiveCount int // IdleCount is the number of idle connections in the pool. IdleCount int // WaitCount is the total number of connections waited for. // This value is currently not guaranteed to be 100% accurate. WaitCount int64 // WaitDuration is the total time blocked waiting for a new connection. // This value is currently not guaranteed to be 100% accurate. WaitDuration time.Duration } // Stats returns pool's statistics. func (p *Pool) Stats() PoolStats { p.mu.Lock() stats := PoolStats{ ActiveCount: p.active, IdleCount: p.idle.count, WaitCount: p.waitCount, WaitDuration: p.waitDuration, } p.mu.Unlock() return stats } // ActiveCount returns the number of connections in the pool. The count // includes idle connections and connections in use. func (p *Pool) ActiveCount() int { p.mu.Lock() active := p.active p.mu.Unlock() return active } // IdleCount returns the number of idle connections in the pool. func (p *Pool) IdleCount() int { p.mu.Lock() idle := p.idle.count p.mu.Unlock() return idle } // Close releases the resources used by the pool. func (p *Pool) Close() error { p.mu.Lock() if p.closed { p.mu.Unlock() return nil } p.closed = true p.active -= p.idle.count pc := p.idle.front p.idle.count = 0 p.idle.front, p.idle.back = nil, nil if p.ch != nil { close(p.ch) } p.mu.Unlock() for ; pc != nil; pc = pc.next { pc.c.Close() } return nil } func (p *Pool) lazyInit() { p.initOnce.Do(func() { p.ch = make(chan struct{}, p.MaxActive) if p.closed { close(p.ch) } else { for i := 0; i < p.MaxActive; i++ { p.ch <- struct{}{} } } }) } // waitVacantConn waits for a vacant connection in pool if waiting // is enabled and pool size is limited, otherwise returns instantly. // If ctx expires before that, an error is returned. // // If there were no vacant connection in the pool right away it returns the time spent waiting // for that connection to appear in the pool. func (p *Pool) waitVacantConn(ctx context.Context) (waited time.Duration, err error) { if !p.Wait || p.MaxActive <= 0 { // No wait or no connection limit. return 0, nil } p.lazyInit() // wait indicates if we believe it will block so its not 100% accurate // however for stats it should be good enough. wait := len(p.ch) == 0 var start time.Time if wait { start = time.Now() } select { case <-p.ch: // Additionally check that context hasn't expired while we were waiting, // because `select` picks a random `case` if several of them are "ready". select { case <-ctx.Done(): p.ch <- struct{}{} return 0, ctx.Err() default: } case <-ctx.Done(): return 0, ctx.Err() } if wait { return time.Since(start), nil } return 0, nil } func (p *Pool) dial(ctx context.Context) (Conn, error) { if p.DialContext != nil { return p.DialContext(ctx) } if p.Dial != nil { return p.Dial() } return nil, errors.New("redigo: must pass Dial or DialContext to pool") } func (p *Pool) put(pc *poolConn, forceClose bool) error { p.mu.Lock() if !p.closed && !forceClose { pc.t = nowFunc() p.idle.pushFront(pc) if p.idle.count > p.MaxIdle { pc = p.idle.back p.idle.popBack() } else { pc = nil } } if pc != nil { p.mu.Unlock() pc.c.Close() p.mu.Lock() p.active-- } if p.ch != nil && !p.closed { p.ch <- struct{}{} } p.mu.Unlock() return nil } type activeConn struct { p *Pool pc *poolConn state int } var ( sentinel []byte sentinelOnce sync.Once ) func initSentinel() { p := make([]byte, 64) if _, err := rand.Read(p); err == nil { sentinel = p } else { h := sha1.New() io.WriteString(h, "Oops, rand failed. Use time instead.") // nolint: errcheck io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10)) // nolint: errcheck sentinel = h.Sum(nil) } } func (ac *activeConn) firstError(errs ...error) error { for _, err := range errs[:len(errs)-1] { if err != nil { return err } } return errs[len(errs)-1] } func (ac *activeConn) Close() (err error) { pc := ac.pc if pc == nil { return nil } ac.pc = nil if ac.state&connectionMultiState != 0 { err = pc.c.Send("DISCARD") ac.state &^= (connectionMultiState | connectionWatchState) } else if ac.state&connectionWatchState != 0 { err = pc.c.Send("UNWATCH") ac.state &^= connectionWatchState } if ac.state&connectionSubscribeState != 0 { err = ac.firstError(err, pc.c.Send("UNSUBSCRIBE"), pc.c.Send("PUNSUBSCRIBE"), ) // To detect the end of the message stream, ask the server to echo // a sentinel value and read until we see that value. sentinelOnce.Do(initSentinel) err = ac.firstError(err, pc.c.Send("ECHO", sentinel), pc.c.Flush(), ) for { p, err2 := pc.c.Receive() if err2 != nil { err = ac.firstError(err, err2) break } if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) { ac.state &^= connectionSubscribeState break } } } _, err2 := pc.c.Do("") return ac.firstError( err, err2, ac.p.put(pc, ac.state != 0 || pc.c.Err() != nil), ) } func (ac *activeConn) Err() error { pc := ac.pc if pc == nil { return errConnClosed } return pc.c.Err() } func (ac *activeConn) DoContext(ctx context.Context, commandName string, args ...interface{}) (reply interface{}, err error) { pc := ac.pc if pc == nil { return nil, errConnClosed } cwt, ok := pc.c.(ConnWithContext) if !ok { return nil, errContextNotSupported } ci := lookupCommandInfo(commandName) ac.state = (ac.state | ci.Set) &^ ci.Clear return cwt.DoContext(ctx, commandName, args...) } func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { pc := ac.pc if pc == nil { return nil, errConnClosed } ci := lookupCommandInfo(commandName) ac.state = (ac.state | ci.Set) &^ ci.Clear return pc.c.Do(commandName, args...) } func (ac *activeConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) { pc := ac.pc if pc == nil { return nil, errConnClosed } cwt, ok := pc.c.(ConnWithTimeout) if !ok { return nil, errTimeoutNotSupported } ci := lookupCommandInfo(commandName) ac.state = (ac.state | ci.Set) &^ ci.Clear return cwt.DoWithTimeout(timeout, commandName, args...) } func (ac *activeConn) Send(commandName string, args ...interface{}) error { pc := ac.pc if pc == nil { return errConnClosed } ci := lookupCommandInfo(commandName) ac.state = (ac.state | ci.Set) &^ ci.Clear return pc.c.Send(commandName, args...) } func (ac *activeConn) Flush() error { pc := ac.pc if pc == nil { return errConnClosed } return pc.c.Flush() } func (ac *activeConn) Receive() (reply interface{}, err error) { pc := ac.pc if pc == nil { return nil, errConnClosed } return pc.c.Receive() } func (ac *activeConn) ReceiveContext(ctx context.Context) (reply interface{}, err error) { pc := ac.pc if pc == nil { return nil, errConnClosed } cwt, ok := pc.c.(ConnWithContext) if !ok { return nil, errContextNotSupported } return cwt.ReceiveContext(ctx) } func (ac *activeConn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) { pc := ac.pc if pc == nil { return nil, errConnClosed } cwt, ok := pc.c.(ConnWithTimeout) if !ok { return nil, errTimeoutNotSupported } return cwt.ReceiveWithTimeout(timeout) } type errorConn struct{ err error } func (ec errorConn) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err } func (ec errorConn) DoContext(context.Context, string, ...interface{}) (interface{}, error) { return nil, ec.err } func (ec errorConn) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) { return nil, ec.err } func (ec errorConn) Send(string, ...interface{}) error { return ec.err } func (ec errorConn) Err() error { return ec.err } func (ec errorConn) Close() error { return nil } func (ec errorConn) Flush() error { return ec.err } func (ec errorConn) Receive() (interface{}, error) { return nil, ec.err } func (ec errorConn) ReceiveContext(context.Context) (interface{}, error) { return nil, ec.err } func (ec errorConn) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err } type idleList struct { count int front, back *poolConn } type poolConn struct { c Conn t time.Time created time.Time next, prev *poolConn } func (l *idleList) pushFront(pc *poolConn) { pc.next = l.front pc.prev = nil if l.count == 0 { l.back = pc } else { l.front.prev = pc } l.front = pc l.count++ } func (l *idleList) popFront() { pc := l.front l.count-- if l.count == 0 { l.front, l.back = nil, nil } else { pc.next.prev = nil l.front = pc.next } pc.next, pc.prev = nil, nil } func (l *idleList) popBack() { pc := l.back l.count-- if l.count == 0 { l.front, l.back = nil, nil } else { pc.prev.next = nil l.back = pc.prev } pc.next, pc.prev = nil, nil } redigo-1.9.2/redis/pool_test.go000066400000000000000000000617551456663410400164700ustar00rootroot00000000000000// Copyright 2011 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "context" "errors" "io" "net" "reflect" "sync" "testing" "time" "github.com/gomodule/redigo/redis" "github.com/stretchr/testify/require" ) const ( testGoRoutines = 10 ) type poolTestConn struct { d *poolDialer err error redis.Conn } func (c *poolTestConn) Close() error { c.d.mu.Lock() c.d.open -= 1 c.d.mu.Unlock() return c.Conn.Close() } func (c *poolTestConn) Err() error { return c.err } func (c *poolTestConn) Do(commandName string, args ...interface{}) (interface{}, error) { return c.do(c.Conn.Do, commandName, args...) } func (c *poolTestConn) DoContext(ctx context.Context, commandName string, args ...interface{}) (interface{}, error) { cwc, ok := c.Conn.(redis.ConnWithContext) if !ok { return nil, errors.New("redis: connection does not support ConnWithContext") } return c.do( func(c string, a ...interface{}) (interface{}, error) { return cwc.DoContext(ctx, c, a...) }, commandName, args) } func (c *poolTestConn) do( fn func(commandName string, args ...interface{}) (interface{}, error), commandName string, args ...interface{}, ) (interface{}, error) { if commandName == "ERR" { c.err = args[0].(error) commandName = "PING" } if commandName != "" { c.d.commands = append(c.d.commands, commandName) } return fn(commandName, args...) } func (c *poolTestConn) Send(commandName string, args ...interface{}) error { c.d.commands = append(c.d.commands, commandName) return c.Conn.Send(commandName, args...) } func (c *poolTestConn) ReceiveContext(ctx context.Context) (reply interface{}, err error) { cwc, ok := c.Conn.(redis.ConnWithContext) if !ok { return nil, errors.New("redis: connection does not support ConnWithContext") } return cwc.ReceiveContext(ctx) } type poolDialer struct { mu sync.Mutex t *testing.T dialed int open int commands []string dialErr error } func (d *poolDialer) dial() (redis.Conn, error) { return d.dialContext(context.Background()) } func (d *poolDialer) dialContext(ctx context.Context) (redis.Conn, error) { d.mu.Lock() d.dialed += 1 dialErr := d.dialErr d.mu.Unlock() if dialErr != nil { return nil, d.dialErr } c, err := redis.DialDefaultServerContext(ctx) if err != nil { return nil, err } d.mu.Lock() d.open += 1 d.mu.Unlock() return &poolTestConn{d: d, Conn: c}, nil } func (d *poolDialer) check(message string, p *redis.Pool, dialed, open, inuse int) { d.t.Helper() d.checkAll(message, p, dialed, open, inuse, 0, 0) } func (d *poolDialer) checkAll(message string, p *redis.Pool, dialed, open, inuse int, waitCountMax int64, waitDurationMax time.Duration) { d.t.Helper() d.mu.Lock() defer d.mu.Unlock() if d.dialed != dialed { d.t.Errorf("%s: dialed=%d, want %d", message, d.dialed, dialed) } if d.open != open { d.t.Errorf("%s: open=%d, want %d", message, d.open, open) } stats := p.Stats() if stats.ActiveCount != open { d.t.Errorf("%s: active=%d, want %d", message, stats.ActiveCount, open) } if stats.IdleCount != open-inuse { d.t.Errorf("%s: idle=%d, want %d", message, stats.IdleCount, open-inuse) } if stats.WaitCount > waitCountMax { d.t.Errorf("%s: unexpected wait=%d want at most %d", message, stats.WaitCount, waitCountMax) } if waitCountMax == 0 { if stats.WaitDuration != 0 { d.t.Errorf("%s: unexpected waitDuration=%v want %v", message, stats.WaitDuration, 0) } return } if stats.WaitDuration > waitDurationMax { d.t.Errorf("%s: unexpected waitDuration=%v want < %v", message, stats.WaitDuration, waitDurationMax) } } func TestPoolReuse(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, Dial: d.dial, } for i := 0; i < 10; i++ { c1 := p.Get() _, err := c1.Do("PING") require.NoError(t, err) c2 := p.Get() _, err = c2.Do("PING") require.NoError(t, err) require.NoError(t, c1.Close()) require.NoError(t, c2.Close()) } d.check("before close", p, 2, 2, 0) p.Close() d.check("after close", p, 2, 0, 0) } func TestPoolMaxIdle(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, Dial: d.dial, } defer p.Close() for i := 0; i < 10; i++ { c1 := p.Get() _, err = c1.Do("PING") require.NoError(t, err) c2 := p.Get() _, err = c2.Do("PING") require.NoError(t, err) c3 := p.Get() _, err = c3.Do("PING") require.NoError(t, err) require.NoError(t, c1.Close()) require.NoError(t, c2.Close()) require.NoError(t, c3.Close()) } d.check("before close", p, 12, 2, 0) p.Close() d.check("after close", p, 12, 0, 0) } func TestPoolError(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, Dial: d.dial, } defer p.Close() c := p.Get() _, err := c.Do("ERR", io.EOF) require.NoError(t, err) if c.Err() == nil { t.Errorf("expected c.Err() != nil") } c.Close() c = p.Get() _, err = c.Do("ERR", io.EOF) require.NoError(t, err) c.Close() d.check(".", p, 2, 0, 0) } func TestPoolClose(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, Dial: d.dial, } defer p.Close() c1 := p.Get() _, err := c1.Do("PING") require.NoError(t, err) c2 := p.Get() _, err = c2.Do("PING") require.NoError(t, err) c3 := p.Get() _, err = c3.Do("PING") require.NoError(t, err) c1.Close() if _, err := c1.Do("PING"); err == nil { t.Errorf("expected error after connection closed") } c2.Close() c2.Close() p.Close() d.check("after pool close", p, 3, 1, 1) if _, err := c1.Do("PING"); err == nil { t.Errorf("expected error after connection and pool closed") } c3.Close() d.check("after conn close", p, 3, 0, 0) c1 = p.Get() if _, err := c1.Do("PING"); err == nil { t.Errorf("expected error after pool closed") } } func TestPoolClosedConn(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, IdleTimeout: 300 * time.Second, Dial: d.dial, } defer p.Close() c := p.Get() if c.Err() != nil { t.Fatal("get failed") } c.Close() if err := c.Err(); err == nil { t.Fatal("Err on closed connection did not return error") } if _, err := c.Do("PING"); err == nil { t.Fatal("Do on closed connection did not return error") } if err := c.Send("PING"); err == nil { t.Fatal("Send on closed connection did not return error") } if err := c.Flush(); err == nil { t.Fatal("Flush on closed connection did not return error") } if _, err := c.Receive(); err == nil { t.Fatal("Receive on closed connection did not return error") } } func TestPoolIdleTimeout(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, IdleTimeout: 300 * time.Second, Dial: d.dial, } defer p.Close() now := time.Now() redis.SetNowFunc(func() time.Time { return now }) defer redis.SetNowFunc(time.Now) c := p.Get() _, err := c.Do("PING") require.NoError(t, err) c.Close() d.check("1", p, 1, 1, 0) now = now.Add(p.IdleTimeout + 1) c = p.Get() _, err = c.Do("PING") require.NoError(t, err) c.Close() d.check("2", p, 2, 1, 0) } func TestPoolMaxLifetime(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, MaxConnLifetime: 300 * time.Second, Dial: d.dial, } defer p.Close() now := time.Now() redis.SetNowFunc(func() time.Time { return now }) defer redis.SetNowFunc(time.Now) c := p.Get() _, err := c.Do("PING") require.NoError(t, err) c.Close() d.check("1", p, 1, 1, 0) now = now.Add(p.MaxConnLifetime + 1) c = p.Get() _, err = c.Do("PING") require.NoError(t, err) c.Close() d.check("2", p, 2, 1, 0) } func TestPoolConcurrenSendReceive(t *testing.T) { p := &redis.Pool{ Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() }, } defer p.Close() c := p.Get() done := make(chan error, 1) go func() { _, err := c.Receive() done <- err }() require.NoError(t, c.Send("PING")) c.Flush() err := <-done if err != nil { t.Fatalf("Receive() returned error %v", err) } _, err = c.Do("") if err != nil { t.Fatalf("Do() returned error %v", err) } c.Close() } func TestPoolBorrowCheck(t *testing.T) { pingN := func(ctx context.Context, p *redis.Pool, n int) { for i := 0; i < n; i++ { func() { c, err := p.GetContext(ctx) require.NoError(t, err) defer func() { require.NoError(t, c.Close()) }() _, err = redis.DoContext(c, ctx, "PING") require.NoError(t, err) }() } } checkLastUsedTimes := func(lastUsedTimes []time.Time, startTime, endTime time.Time, wantLen int) { require.Len(t, lastUsedTimes, wantLen) for i, lastUsed := range lastUsedTimes { if i == 0 { require.True(t, lastUsed.After(startTime)) } else { require.True(t, lastUsed.After(lastUsedTimes[i-1])) } require.True(t, lastUsed.Before(endTime)) } } t.Run("TestOnBorrow-error", func(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, DialContext: d.dialContext, TestOnBorrow: func(redis.Conn, time.Time) error { return redis.Error("BLAH") }, } defer p.Close() pingN(context.Background(), p, 10) d.check("1", p, 10, 1, 0) }) t.Run("TestOnBorrow-nil-error", func(t *testing.T) { d := poolDialer{t: t} var borrowErrs []error var lastUsedTimes []time.Time p := &redis.Pool{ MaxIdle: 2, DialContext: d.dialContext, TestOnBorrow: func(c redis.Conn, lastUsed time.Time) error { lastUsedTimes = append(lastUsedTimes, lastUsed) _, err := c.Do("PING") if err != nil { borrowErrs = append(borrowErrs, err) } return err }, } defer p.Close() startTime := time.Now() pingN(context.Background(), p, 10) endTime := time.Now() require.Empty(t, borrowErrs) checkLastUsedTimes(lastUsedTimes, startTime, endTime, 9) d.check("1", p, 1, 1, 0) }) t.Run("TestOnBorrowContext-error", func(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, DialContext: d.dialContext, TestOnBorrowContext: func(context.Context, redis.Conn, time.Time) error { return redis.Error("BLAH") }, } defer p.Close() pingN(context.Background(), p, 10) d.check("1", p, 10, 1, 0) }) t.Run("TestOnBorrowContext-nil-error", func(t *testing.T) { d := poolDialer{t: t} var borrowErrs []error var lastUsedTimes []time.Time p := &redis.Pool{ MaxIdle: 2, DialContext: d.dialContext, TestOnBorrowContext: func(ctx context.Context, c redis.Conn, lastUsed time.Time) error { lastUsedTimes = append(lastUsedTimes, lastUsed) _, err := redis.DoContext(c, ctx, "PING") if err != nil { borrowErrs = append(borrowErrs, err) } return err }, } defer p.Close() startTime := time.Now() pingN(context.Background(), p, 10) endTime := time.Now() require.Empty(t, borrowErrs) checkLastUsedTimes(lastUsedTimes, startTime, endTime, 9) d.check("1", p, 1, 1, 0) }) t.Run("TestOnBorrowContext-context.Canceled", func(t *testing.T) { d := poolDialer{t: t} var borrowErrs []error p := &redis.Pool{ MaxIdle: 2, DialContext: d.dialContext, TestOnBorrowContext: func(ctx context.Context, c redis.Conn, _ time.Time) error { _, err := redis.DoContext(c, ctx, "PING") if err != nil { borrowErrs = append(borrowErrs, err) } return err }, } defer p.Close() ctx, ctxCancel := context.WithCancel(context.Background()) defer ctxCancel() pingN(ctx, p, 2) d.check("1", p, 1, 1, 0) require.Empty(t, borrowErrs) ctxCancel() _, err := p.GetContext(ctx) require.ErrorIs(t, err, context.Canceled) d.check("1", p, 2, 0, 0) require.Len(t, borrowErrs, 1) require.ErrorIs(t, borrowErrs[0], context.Canceled) }) } func TestPoolMaxActive(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, MaxActive: 2, Dial: d.dial, } defer p.Close() c1 := p.Get() _, err := c1.Do("PING") require.NoError(t, err) c2 := p.Get() _, err = c2.Do("PING") require.NoError(t, err) d.check("1", p, 2, 2, 2) c3 := p.Get() if _, err := c3.Do("PING"); err != redis.ErrPoolExhausted { t.Errorf("expected pool exhausted") } c3.Close() d.check("2", p, 2, 2, 2) c2.Close() d.check("3", p, 2, 2, 1) c3 = p.Get() if _, err := c3.Do("PING"); err != nil { t.Errorf("expected good channel, err=%v", err) } c3.Close() d.check("4", p, 2, 2, 1) } func TestPoolWaitStats(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ Wait: true, MaxIdle: 2, MaxActive: 2, Dial: d.dial, } defer p.Close() c1 := p.Get() _, err := c1.Do("PING") require.NoError(t, err) c2 := p.Get() _, err = c2.Do("PING") require.NoError(t, err) d.checkAll("1", p, 2, 2, 2, 0, 0) start := time.Now() go func() { time.Sleep(time.Millisecond * 100) c1.Close() }() c3 := p.Get() d.checkAll("2", p, 2, 2, 2, 1, time.Since(start)) if _, err := c3.Do("PING"); err != nil { t.Errorf("expected good channel, err=%v", err) } } func TestPoolMonitorCleanup(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, MaxActive: 2, Dial: d.dial, } defer p.Close() c := p.Get() require.NoError(t, c.Send("MONITOR")) c.Close() d.check("", p, 1, 0, 0) } func TestPoolPubSubCleanup(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, MaxActive: 2, Dial: d.dial, } defer p.Close() c := p.Get() require.NoError(t, c.Send("SUBSCRIBE", "x")) c.Close() want := []string{"SUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} if !reflect.DeepEqual(d.commands, want) { t.Errorf("got commands %v, want %v", d.commands, want) } d.commands = nil c = p.Get() require.NoError(t, c.Send("PSUBSCRIBE", "x*")) c.Close() want = []string{"PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} if !reflect.DeepEqual(d.commands, want) { t.Errorf("got commands %v, want %v", d.commands, want) } d.commands = nil } func TestPoolTransactionCleanup(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 2, MaxActive: 2, Dial: d.dial, } defer p.Close() c := p.Get() _, err := c.Do("WATCH", "key") require.NoError(t, err) _, err = c.Do("PING") require.NoError(t, err) c.Close() want := []string{"WATCH", "PING", "UNWATCH"} if !reflect.DeepEqual(d.commands, want) { t.Errorf("got commands %v, want %v", d.commands, want) } d.commands = nil c = p.Get() _, err = c.Do("WATCH", "key") require.NoError(t, err) _, err = c.Do("UNWATCH") require.NoError(t, err) _, err = c.Do("PING") require.NoError(t, err) c.Close() want = []string{"WATCH", "UNWATCH", "PING"} if !reflect.DeepEqual(d.commands, want) { t.Errorf("got commands %v, want %v", d.commands, want) } d.commands = nil c = p.Get() _, err = c.Do("WATCH", "key") require.NoError(t, err) _, err = c.Do("MULTI") require.NoError(t, err) _, err = c.Do("PING") require.NoError(t, err) c.Close() want = []string{"WATCH", "MULTI", "PING", "DISCARD"} if !reflect.DeepEqual(d.commands, want) { t.Errorf("got commands %v, want %v", d.commands, want) } d.commands = nil c = p.Get() _, err = c.Do("WATCH", "key") require.NoError(t, err) _, err = c.Do("MULTI") require.NoError(t, err) _, err = c.Do("DISCARD") require.NoError(t, err) _, err = c.Do("PING") require.NoError(t, err) c.Close() want = []string{"WATCH", "MULTI", "DISCARD", "PING"} if !reflect.DeepEqual(d.commands, want) { t.Errorf("got commands %v, want %v", d.commands, want) } d.commands = nil c = p.Get() _, err = c.Do("WATCH", "key") require.NoError(t, err) _, err = c.Do("MULTI") require.NoError(t, err) _, err = c.Do("EXEC") require.NoError(t, err) _, err = c.Do("PING") require.NoError(t, err) c.Close() want = []string{"WATCH", "MULTI", "EXEC", "PING"} if !reflect.DeepEqual(d.commands, want) { t.Errorf("got commands %v, want %v", d.commands, want) } d.commands = nil } func startGoroutines(p *redis.Pool, cmd string, args ...interface{}) chan error { errs := make(chan error, testGoRoutines) for i := 0; i < cap(errs); i++ { go func() { c := p.Get() _, err := c.Do(cmd, args...) c.Close() errs <- err }() } return errs } func TestWaitPool(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() c := p.Get() start := time.Now() errs := startGoroutines(p, "PING") d.check("before close", p, 1, 1, 1) c.Close() timeout := time.After(2 * time.Second) for i := 0; i < cap(errs); i++ { select { case err := <-errs: if err != nil { t.Fatal(err) } case <-timeout: t.Fatalf("timeout waiting for blocked goroutine %d", i) } } d.checkAll("done", p, 1, 1, 0, testGoRoutines, time.Since(start)*testGoRoutines) } func TestWaitPoolClose(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() c := p.Get() if _, err := c.Do("PING"); err != nil { t.Fatal(err) } start := time.Now() errs := startGoroutines(p, "PING") d.check("before close", p, 1, 1, 1) p.Close() timeout := time.After(2 * time.Second) for i := 0; i < cap(errs); i++ { select { case err := <-errs: switch err { case nil: t.Fatal("blocked goroutine did not get error") case redis.ErrPoolExhausted: t.Fatal("blocked goroutine got pool exhausted error") } case <-timeout: t.Fatal("timeout waiting for blocked goroutine") } } c.Close() d.checkAll("done", p, 1, 0, 0, testGoRoutines, time.Since(start)*testGoRoutines) } func TestWaitPoolCommandError(t *testing.T) { testErr := errors.New("test") d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() c := p.Get() start := time.Now() errs := startGoroutines(p, "ERR", testErr) d.check("before close", p, 1, 1, 1) c.Close() timeout := time.After(2 * time.Second) for i := 0; i < cap(errs); i++ { select { case err := <-errs: if err != nil { t.Fatal(err) } case <-timeout: t.Fatalf("timeout waiting for blocked goroutine %d", i) } } d.checkAll("done", p, cap(errs), 0, 0, testGoRoutines, time.Since(start)*testGoRoutines) } func TestWaitPoolDialError(t *testing.T) { testErr := errors.New("test") d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() c := p.Get() start := time.Now() errs := startGoroutines(p, "ERR", testErr) d.check("before close", p, 1, 1, 1) d.dialErr = errors.New("dial") c.Close() nilCount := 0 errCount := 0 timeout := time.After(2 * time.Second) for i := 0; i < cap(errs); i++ { select { case err := <-errs: switch err { case nil: nilCount++ case d.dialErr: errCount++ default: t.Fatalf("expected dial error or nil, got %v", err) } case <-timeout: t.Fatalf("timeout waiting for blocked goroutine %d", i) } } if nilCount != 1 { t.Errorf("expected one nil error, got %d", nilCount) } if errCount != cap(errs)-1 { t.Errorf("expected %d dial errors, got %d", cap(errs)-1, errCount) } d.checkAll("done", p, cap(errs), 0, 0, testGoRoutines, time.Since(start)*testGoRoutines) } // Borrowing requires us to iterate over the idle connections, unlock the pool, // and perform a blocking operation to check the connection still works. If // TestOnBorrow fails, we must reacquire the lock and continue iteration. This // test ensures that iteration will work correctly if multiple threads are // iterating simultaneously. func TestLocking_TestOnBorrowFails_PoolDoesntCrash(t *testing.T) { const count = 100 // First we'll Create a pool where the pilfering of idle connections fails. d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: count, MaxActive: count, Dial: d.dial, TestOnBorrow: func(redis.Conn, time.Time) error { return errors.New("No way back into the real world.") }, } defer p.Close() // Fill the pool with idle connections. conns := make([]redis.Conn, count) for i := range conns { conns[i] = p.Get() } for i := range conns { conns[i].Close() } // Spawn a bunch of goroutines to thrash the pool. var wg sync.WaitGroup wg.Add(count) for i := 0; i < count; i++ { go func() { c := p.Get() if c.Err() != nil { t.Errorf("pool get failed: %v", c.Err()) } c.Close() wg.Done() }() } wg.Wait() if d.dialed != count*2 { t.Errorf("Expected %d dials, got %d", count*2, d.dialed) } } func BenchmarkPoolGet(b *testing.B) { b.StopTimer() p := redis.Pool{Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() }, MaxIdle: 2} c := p.Get() if err := c.Err(); err != nil { b.Fatal(err) } c.Close() defer p.Close() b.StartTimer() for i := 0; i < b.N; i++ { c = p.Get() c.Close() } } func BenchmarkPoolGetErr(b *testing.B) { b.StopTimer() p := redis.Pool{Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() }, MaxIdle: 2} c := p.Get() if err := c.Err(); err != nil { b.Fatal(err) } c.Close() defer p.Close() b.StartTimer() for i := 0; i < b.N; i++ { c = p.Get() if err := c.Err(); err != nil { b.Fatal(err) } c.Close() } } func BenchmarkPoolGetPing(b *testing.B) { b.StopTimer() p := redis.Pool{Dial: func() (redis.Conn, error) { return redis.DialDefaultServer() }, MaxIdle: 2} c := p.Get() if err := c.Err(); err != nil { b.Fatal(err) } c.Close() defer p.Close() b.StartTimer() for i := 0; i < b.N; i++ { c = p.Get() if _, err := c.Do("PING"); err != nil { b.Fatal(err) } c.Close() } } func TestWaitPoolGetContext(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() c, err := p.GetContext(context.Background()) if err != nil { t.Fatalf("GetContext returned %v", err) } defer c.Close() } func TestWaitPoolGetContextIssue520(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() ctx1, cancel1 := context.WithTimeout(context.Background(), 1*time.Nanosecond) defer cancel1() c, err := p.GetContext(ctx1) if err != context.DeadlineExceeded { t.Fatalf("GetContext returned %v", err) } defer c.Close() ctx2, cancel2 := context.WithCancel(context.Background()) defer cancel2() c2, err := p.GetContext(ctx2) if err != nil { t.Fatalf("Get context returned %v", err) } defer c2.Close() } func TestWaitPoolGetContextWithDialContext(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, DialContext: d.dialContext, Wait: true, } defer p.Close() c, err := p.GetContext(context.Background()) if err != nil { t.Fatalf("GetContext returned %v", err) } defer c.Close() } func TestPoolGetContext_DialContext(t *testing.T) { var isPassed bool f := func(ctx context.Context, network, addr string) (net.Conn, error) { isPassed = true return &testConn{}, nil } p := &redis.Pool{ DialContext: func(ctx context.Context) (redis.Conn, error) { return redis.DialContext(ctx, "", "", redis.DialContextFunc(f)) }, } defer p.Close() if _, err := p.GetContext(context.Background()); err != nil { t.Fatalf("GetContext returned %v", err) } if !isPassed { t.Fatal("DialContextFunc not passed") } } func TestPoolGetContext_DialContext_CanceledContext(t *testing.T) { addr, err := redis.DefaultServerAddr() if err != nil { t.Fatalf("redis.DefaultServerAddr returned %v", err) } p := &redis.Pool{ DialContext: func(ctx context.Context) (redis.Conn, error) { return redis.DialContext(ctx, "tcp", addr) }, } defer p.Close() ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := p.GetContext(ctx); err == nil { t.Fatalf("GetContext returned nil, expect error") } } func TestWaitPoolGetAfterClose(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } p.Close() _, err := p.GetContext(context.Background()) if err == nil { t.Fatal("expected error") } } func TestWaitPoolGetCanceledContext(t *testing.T) { t.Run("without vacant connection in the pool", func(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() ctx, cancel := context.WithCancel(context.Background()) cancel() c := p.Get() defer c.Close() _, err := p.GetContext(ctx) if err != context.Canceled { t.Fatalf("got error %v, want %v", err, context.Canceled) } }) t.Run("with vacant connection in the pool", func(t *testing.T) { d := poolDialer{t: t} p := &redis.Pool{ MaxIdle: 1, MaxActive: 1, Dial: d.dial, Wait: true, } defer p.Close() ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := p.GetContext(ctx) if err != context.Canceled { t.Fatalf("got error %v, want %v", err, context.Canceled) } }) } redigo-1.9.2/redis/pubsub.go000066400000000000000000000106361456663410400157500ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "context" "errors" "time" ) // Subscription represents a subscribe or unsubscribe notification. type Subscription struct { // Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe" Kind string // The channel that was changed. Channel string // The current number of subscriptions for connection. Count int } // Message represents a message notification. type Message struct { // The originating channel. Channel string // The matched pattern, if any Pattern string // The message data. Data []byte } // Pong represents a pubsub pong notification. type Pong struct { Data string } // PubSubConn wraps a Conn with convenience methods for subscribers. type PubSubConn struct { Conn Conn } // Close closes the connection. func (c PubSubConn) Close() error { return c.Conn.Close() } // Subscribe subscribes the connection to the specified channels. func (c PubSubConn) Subscribe(channel ...interface{}) error { if err := c.Conn.Send("SUBSCRIBE", channel...); err != nil { return err } return c.Conn.Flush() } // PSubscribe subscribes the connection to the given patterns. func (c PubSubConn) PSubscribe(channel ...interface{}) error { if err := c.Conn.Send("PSUBSCRIBE", channel...); err != nil { return err } return c.Conn.Flush() } // Unsubscribe unsubscribes the connection from the given channels, or from all // of them if none is given. func (c PubSubConn) Unsubscribe(channel ...interface{}) error { if err := c.Conn.Send("UNSUBSCRIBE", channel...); err != nil { return err } return c.Conn.Flush() } // PUnsubscribe unsubscribes the connection from the given patterns, or from all // of them if none is given. func (c PubSubConn) PUnsubscribe(channel ...interface{}) error { if err := c.Conn.Send("PUNSUBSCRIBE", channel...); err != nil { return err } return c.Conn.Flush() } // Ping sends a PING to the server with the specified data. // // The connection must be subscribed to at least one channel or pattern when // calling this method. func (c PubSubConn) Ping(data string) error { if err := c.Conn.Send("PING", data); err != nil { return err } return c.Conn.Flush() } // Receive returns a pushed message as a Subscription, Message, Pong or error. // The return value is intended to be used directly in a type switch as // illustrated in the PubSubConn example. func (c PubSubConn) Receive() interface{} { return c.receiveInternal(c.Conn.Receive()) } // ReceiveWithTimeout is like Receive, but it allows the application to // override the connection's default timeout. func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} { return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout)) } // ReceiveContext is like Receive, but it allows termination of the receive // via a Context. If the call returns due to closure of the context's Done // channel the underlying Conn will have been closed. func (c PubSubConn) ReceiveContext(ctx context.Context) interface{} { return c.receiveInternal(ReceiveContext(c.Conn, ctx)) } func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} { reply, err := Values(replyArg, errArg) if err != nil { return err } var kind string reply, err = Scan(reply, &kind) if err != nil { return err } switch kind { case "message": var m Message if _, err := Scan(reply, &m.Channel, &m.Data); err != nil { return err } return m case "pmessage": var m Message if _, err := Scan(reply, &m.Pattern, &m.Channel, &m.Data); err != nil { return err } return m case "subscribe", "psubscribe", "unsubscribe", "punsubscribe": s := Subscription{Kind: kind} if _, err := Scan(reply, &s.Channel, &s.Count); err != nil { return err } return s case "pong": var p Pong if _, err := Scan(reply, &p.Data); err != nil { return err } return p } return errors.New("redigo: unknown pubsub notification") } redigo-1.9.2/redis/pubsub_example_test.go000066400000000000000000000103411456663410400205130ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // +build go1.7 package redis_test import ( "context" "fmt" "time" "github.com/gomodule/redigo/redis" ) // listenPubSubChannels listens for messages on Redis pubsub channels. The // onStart function is called after the channels are subscribed. The onMessage // function is called for each message. func listenPubSubChannels(ctx context.Context, redisServerAddr string, onStart func() error, onMessage func(channel string, data []byte) error, channels ...string) error { // A ping is set to the server with this period to test for the health of // the connection and server. const healthCheckPeriod = time.Minute c, err := redis.Dial("tcp", redisServerAddr, // Read timeout on server should be greater than ping period. redis.DialReadTimeout(healthCheckPeriod+10*time.Second), redis.DialWriteTimeout(10*time.Second)) if err != nil { return err } defer c.Close() psc := redis.PubSubConn{Conn: c} if err := psc.Subscribe(redis.Args{}.AddFlat(channels)...); err != nil { return err } done := make(chan error, 1) // Start a goroutine to receive notifications from the server. go func() { for { switch n := psc.Receive().(type) { case error: done <- n return case redis.Message: if err := onMessage(n.Channel, n.Data); err != nil { done <- err return } case redis.Subscription: switch n.Count { case len(channels): // Notify application when all channels are subscribed. if err := onStart(); err != nil { done <- err return } case 0: // Return from the goroutine when all channels are unsubscribed. done <- nil return } } } }() ticker := time.NewTicker(healthCheckPeriod) defer ticker.Stop() loop: for { select { case <-ticker.C: // Send ping to test health of connection and server. If // corresponding pong is not received, then receive on the // connection will timeout and the receive goroutine will exit. if err = psc.Ping(""); err != nil { break loop } case <-ctx.Done(): break loop case err := <-done: // Return error from the receive goroutine. return err } } // Signal the receiving goroutine to exit by unsubscribing from all channels. if err := psc.Unsubscribe(); err != nil { return err } // Wait for goroutine to complete. return <-done } func publish() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() if _, err = c.Do("PUBLISH", "c1", "hello"); err != nil { fmt.Println(err) return } if _, err = c.Do("PUBLISH", "c2", "world"); err != nil { fmt.Println(err) return } if _, err = c.Do("PUBLISH", "c1", "goodbye"); err != nil { fmt.Println(err) return } } // This example shows how receive pubsub notifications with cancelation and // health checks. func ExamplePubSubConn() { redisServerAddr, err := serverAddr() if err != nil { fmt.Println(err) return } ctx, cancel := context.WithCancel(context.Background()) err = listenPubSubChannels(ctx, redisServerAddr, func() error { // The start callback is a good place to backfill missed // notifications. For the purpose of this example, a goroutine is // started to send notifications. go publish() return nil }, func(channel string, message []byte) error { fmt.Printf("channel: %s, message: %s\n", channel, message) // For the purpose of this example, cancel the listener's context // after receiving last message sent by publish(). if string(message) == "goodbye" { cancel() } return nil }, "c1", "c2") if err != nil { fmt.Println(err) return } // Output: // channel: c1, message: hello // channel: c2, message: world // channel: c1, message: goodbye } redigo-1.9.2/redis/pubsub_test.go000066400000000000000000000062301456663410400170020ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "context" "errors" "testing" "time" "github.com/gomodule/redigo/redis" "github.com/stretchr/testify/require" ) func TestPushed(t *testing.T) { pc, err := redis.DialDefaultServer() require.NoError(t, err) defer pc.Close() sc, err := redis.DialDefaultServer() require.NoError(t, err) defer sc.Close() c := redis.PubSubConn{Conn: sc} require.NoError(t, c.Subscribe("c1")) require.Equal(t, redis.Subscription{Kind: "subscribe", Channel: "c1", Count: 1}, c.Receive()) require.NoError(t, c.Subscribe("c2")) require.Equal(t, redis.Subscription{Kind: "subscribe", Channel: "c2", Count: 2}, c.Receive()) require.NoError(t, c.PSubscribe("p1")) require.Equal(t, redis.Subscription{Kind: "psubscribe", Channel: "p1", Count: 3}, c.Receive()) require.NoError(t, c.PSubscribe("p2")) require.Equal(t, redis.Subscription{Kind: "psubscribe", Channel: "p2", Count: 4}, c.Receive()) require.NoError(t, c.PUnsubscribe()) // Response can return in any order. v := c.Receive() require.IsType(t, redis.Subscription{}, v) u := v.(redis.Subscription) expected1 := redis.Subscription{Kind: "punsubscribe", Channel: "p1", Count: 3} expected2 := redis.Subscription{Kind: "punsubscribe", Channel: "p2", Count: 2} if u.Channel == "p2" { // Order reversed. expected1.Channel = "p2" expected2.Channel = "p1" } require.Equal(t, expected1, u) require.Equal(t, expected2, c.Receive()) _, err = pc.Do("PUBLISH", "c1", "hello") require.NoError(t, err) require.Equal(t, redis.Message{Channel: "c1", Data: []byte("hello")}, c.Receive()) require.NoError(t, c.Ping("hello")) require.Equal(t, redis.Pong{Data: "hello"}, c.Receive()) require.NoError(t, c.Conn.Send("PING")) c.Conn.Flush() require.Equal(t, redis.Pong{}, c.Receive()) require.NoError(t, c.Ping("timeout")) got := c.ReceiveWithTimeout(time.Minute) if want := (redis.Pong{Data: "timeout"}); want != got { t.Errorf("recv /w timeout got %v, want %v", got, want) } } func TestPubSubReceiveContext(t *testing.T) { sc, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer sc.Close() c := redis.PubSubConn{Conn: sc} require.NoError(t, c.Subscribe("c1")) require.Equal(t, redis.Subscription{Kind: "subscribe", Channel: "c1", Count: 1}, c.Receive()) ctx, cancel := context.WithCancel(context.Background()) cancel() got := c.ReceiveContext(ctx) if err, ok := got.(error); !ok { t.Errorf("recv w/canceled expected Canceled got non-error type %T", got) } else if !errors.Is(err, context.Canceled) { t.Errorf("recv w/canceled expected Canceled got %v", err) } } redigo-1.9.2/redis/redis.go000066400000000000000000000177421456663410400155630ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "context" "errors" "time" ) // Error represents an error returned in a command reply. type Error string func (err Error) Error() string { return string(err) } // Conn represents a connection to a Redis server. type Conn interface { // Close closes the connection. Close() error // Err returns a non-nil value when the connection is not usable. Err() error // Do sends a command to the server and returns the received reply. // This function will use the timeout which was set when the connection is created Do(commandName string, args ...interface{}) (reply interface{}, err error) // Send writes the command to the client's output buffer. Send(commandName string, args ...interface{}) error // Flush flushes the output buffer to the Redis server. Flush() error // Receive receives a single reply from the Redis server Receive() (reply interface{}, err error) } // Argument is the interface implemented by an object which wants to control how // the object is converted to Redis bulk strings. type Argument interface { // RedisArg returns a value to be encoded as a bulk string per the // conversions listed in the section 'Executing Commands'. // Implementations should typically return a []byte or string. RedisArg() interface{} } // Scanner is implemented by an object which wants to control its value is // interpreted when read from Redis. type Scanner interface { // RedisScan assigns a value from a Redis value. The argument src is one of // the reply types listed in the section `Executing Commands`. // // An error should be returned if the value cannot be stored without // loss of information. RedisScan(src interface{}) error } // ConnWithTimeout is an optional interface that allows the caller to override // a connection's default read timeout. This interface is useful for executing // the BLPOP, BRPOP, BRPOPLPUSH, XREAD and other commands that block at the // server. // // A connection's default read timeout is set with the DialReadTimeout dial // option. Applications should rely on the default timeout for commands that do // not block at the server. // // All of the Conn implementations in this package satisfy the ConnWithTimeout // interface. // // Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify // use of this interface. type ConnWithTimeout interface { Conn // DoWithTimeout sends a command to the server and returns the received reply. // The timeout overrides the readtimeout set when dialing the connection. DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) // ReceiveWithTimeout receives a single reply from the Redis server. // The timeout overrides the readtimeout set when dialing the connection. ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) } // ConnWithContext is an optional interface that allows the caller to control the command's life with context. type ConnWithContext interface { Conn // DoContext sends a command to server and returns the received reply. // min(ctx,DialReadTimeout()) will be used as the deadline. // The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running. // DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded). // ctx timeout return err context.DeadlineExceeded. // ctx canceled return err context.Canceled. DoContext(ctx context.Context, commandName string, args ...interface{}) (reply interface{}, err error) // ReceiveContext receives a single reply from the Redis server. // min(ctx,DialReadTimeout()) will be used as the deadline. // The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running. // DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded). // ctx timeout return err context.DeadlineExceeded. // ctx canceled return err context.Canceled. ReceiveContext(ctx context.Context) (reply interface{}, err error) } var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout") var errContextNotSupported = errors.New("redis: connection does not support ConnWithContext") // DoContext sends a command to server and returns the received reply. // min(ctx,DialReadTimeout()) will be used as the deadline. // The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running. // DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded). // ctx timeout return err context.DeadlineExceeded. // ctx canceled return err context.Canceled. func DoContext(c Conn, ctx context.Context, cmd string, args ...interface{}) (interface{}, error) { cwt, ok := c.(ConnWithContext) if !ok { return nil, errContextNotSupported } return cwt.DoContext(ctx, cmd, args...) } // DoWithTimeout executes a Redis command with the specified read timeout. If // the connection does not satisfy the ConnWithTimeout interface, then an error // is returned. func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { cwt, ok := c.(ConnWithTimeout) if !ok { return nil, errTimeoutNotSupported } return cwt.DoWithTimeout(timeout, cmd, args...) } // ReceiveContext receives a single reply from the Redis server. // min(ctx,DialReadTimeout()) will be used as the deadline. // The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running. // DialReadTimeout() timeout return err can be checked by strings.Contains(e.Error(), "io/timeout"). // ctx timeout return err context.DeadlineExceeded. // ctx canceled return err context.Canceled. func ReceiveContext(c Conn, ctx context.Context) (interface{}, error) { cwt, ok := c.(ConnWithContext) if !ok { return nil, errContextNotSupported } return cwt.ReceiveContext(ctx) } // ReceiveWithTimeout receives a reply with the specified read timeout. If the // connection does not satisfy the ConnWithTimeout interface, then an error is // returned. func ReceiveWithTimeout(c Conn, timeout time.Duration) (interface{}, error) { cwt, ok := c.(ConnWithTimeout) if !ok { return nil, errTimeoutNotSupported } return cwt.ReceiveWithTimeout(timeout) } // SlowLog represents a redis SlowLog type SlowLog struct { // ID is a unique progressive identifier for every slow log entry. ID int64 // Time is the unix timestamp at which the logged command was processed. Time time.Time // ExecutationTime is the amount of time needed for the command execution. ExecutionTime time.Duration // Args is the command name and arguments Args []string // ClientAddr is the client IP address (4.0 only). ClientAddr string // ClientName is the name set via the CLIENT SETNAME command (4.0 only). ClientName string } // Latency represents a redis LATENCY LATEST. type Latency struct { // Name of the latest latency spike event. Name string // Time of the latest latency spike for the event. Time time.Time // Latest is the latest recorded latency for the named event. Latest time.Duration // Max is the maximum latency for the named event. Max time.Duration } // LatencyHistory represents a redis LATENCY HISTORY. type LatencyHistory struct { // Time is the unix timestamp at which the event was processed. Time time.Time // ExecutationTime is the amount of time needed for the command execution. ExecutionTime time.Duration } redigo-1.9.2/redis/redis_test.go000066400000000000000000000076511456663410400166200ustar00rootroot00000000000000// Copyright 2017 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "context" "testing" "time" "github.com/gomodule/redigo/redis" ) type timeoutTestConn int func (tc timeoutTestConn) Do(string, ...interface{}) (interface{}, error) { return time.Duration(-1), nil } func (tc timeoutTestConn) DoWithTimeout(timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { return timeout, nil } func (tc timeoutTestConn) Receive() (interface{}, error) { return time.Duration(-1), nil } func (tc timeoutTestConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) { return timeout, nil } func (tc timeoutTestConn) Send(string, ...interface{}) error { return nil } func (tc timeoutTestConn) Err() error { return nil } func (tc timeoutTestConn) Close() error { return nil } func (tc timeoutTestConn) Flush() error { return nil } func testTimeout(t *testing.T, c redis.Conn) { r, err := c.Do("PING") if r != time.Duration(-1) || err != nil { t.Errorf("Do() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil) } r, err = redis.DoWithTimeout(c, time.Minute, "PING") if r != time.Minute || err != nil { t.Errorf("DoWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil) } r, err = c.Receive() if r != time.Duration(-1) || err != nil { t.Errorf("Receive() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil) } r, err = redis.ReceiveWithTimeout(c, time.Minute) if r != time.Minute || err != nil { t.Errorf("ReceiveWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil) } } func TestConnTimeout(t *testing.T) { testTimeout(t, timeoutTestConn(0)) } func TestPoolConnTimeout(t *testing.T) { p := &redis.Pool{Dial: func() (redis.Conn, error) { return timeoutTestConn(0), nil }} testTimeout(t, p.Get()) } type contextDeadTestConn int func (cc contextDeadTestConn) Do(string, ...interface{}) (interface{}, error) { return -1, nil } func (cc contextDeadTestConn) DoContext(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) { return 1, nil } func (cc contextDeadTestConn) Receive() (interface{}, error) { return -1, nil } func (cc contextDeadTestConn) ReceiveContext(ctx context.Context) (interface{}, error) { return 1, nil } func (cc contextDeadTestConn) Send(string, ...interface{}) error { return nil } func (cc contextDeadTestConn) Err() error { return nil } func (cc contextDeadTestConn) Close() error { return nil } func (cc contextDeadTestConn) Flush() error { return nil } func testcontext(t *testing.T, c redis.Conn) { r, e := c.Do("PING") if r != -1 || e != nil { t.Errorf("Do() = %v, %v, want %v, %v", r, e, -1, nil) } ctx, f := context.WithTimeout(context.Background(), time.Minute) defer f() r, e = redis.DoContext(c, ctx, "PING") if r != 1 || e != nil { t.Errorf("DoContext() = %v, %v, want %v, %v", r, e, 1, nil) } r, e = c.Receive() if r != -1 || e != nil { t.Errorf("Receive() = %v, %v, want %v, %v", r, e, -1, nil) } r, e = redis.ReceiveContext(c, ctx) if r != 1 || e != nil { t.Errorf("ReceiveContext() = %v, %v, want %v, %v", r, e, 1, nil) } } func TestConnContext(t *testing.T) { testcontext(t, contextDeadTestConn(0)) } func TestPoolConnContext(t *testing.T) { p := redis.Pool{Dial: func() (redis.Conn, error) { return contextDeadTestConn(0), nil }} testcontext(t, p.Get()) } redigo-1.9.2/redis/reflect.go000066400000000000000000000020321456663410400160630ustar00rootroot00000000000000package redis import ( "reflect" "runtime" ) // methodName returns the name of the calling method, // assumed to be two stack frames above. func methodName() string { pc, _, _, _ := runtime.Caller(2) f := runtime.FuncForPC(pc) if f == nil { return "unknown method" } return f.Name() } // mustBe panics if f's kind is not expected. func mustBe(v reflect.Value, expected reflect.Kind) { if v.Kind() != expected { panic(&reflect.ValueError{Method: methodName(), Kind: v.Kind()}) } } // fieldByIndexCreate returns the nested field corresponding // to index creating elements that are nil when stepping through. // It panics if v is not a struct. func fieldByIndexCreate(v reflect.Value, index []int) reflect.Value { if len(index) == 1 { return v.Field(index[0]) } mustBe(v, reflect.Struct) for i, x := range index { if i > 0 { if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } v = v.Elem() } } v = v.Field(x) } return v } redigo-1.9.2/redis/reflect_go117.go000066400000000000000000000014441456663410400170070ustar00rootroot00000000000000//go:build !go1.18 // +build !go1.18 package redis import ( "errors" "reflect" ) // fieldByIndexErr returns the nested field corresponding to index. // It returns an error if evaluation requires stepping through a nil // pointer, but panics if it must step through a field that // is not a struct. func fieldByIndexErr(v reflect.Value, index []int) (reflect.Value, error) { if len(index) == 1 { return v.Field(index[0]), nil } mustBe(v, reflect.Struct) for i, x := range index { if i > 0 { if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { if v.IsNil() { return reflect.Value{}, errors.New("reflect: indirection through nil pointer to embedded struct field " + v.Type().Elem().Name()) } v = v.Elem() } } v = v.Field(x) } return v, nil } redigo-1.9.2/redis/reflect_go118.go000066400000000000000000000006221456663410400170050ustar00rootroot00000000000000//go:build go1.18 // +build go1.18 package redis import ( "reflect" ) // fieldByIndexErr returns the nested field corresponding to index. // It returns an error if evaluation requires stepping through a nil // pointer, but panics if it must step through a field that // is not a struct. func fieldByIndexErr(v reflect.Value, index []int) (reflect.Value, error) { return v.FieldByIndexErr(index) } redigo-1.9.2/redis/reply.go000066400000000000000000000502241456663410400156000ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "errors" "fmt" "strconv" "time" ) // ErrNil indicates that a reply value is nil. var ErrNil = errors.New("redigo: nil returned") // Int is a helper that converts a command reply to an integer. If err is not // equal to nil, then Int returns 0, err. Otherwise, Int converts the // reply to an int as follows: // // Reply type Result // integer int(reply), nil // bulk string parsed reply, nil // nil 0, ErrNil // other 0, error func Int(reply interface{}, err error) (int, error) { if err != nil { return 0, err } switch reply := reply.(type) { case int64: x := int(reply) if int64(x) != reply { return 0, strconv.ErrRange } return x, nil case []byte: n, err := strconv.ParseInt(string(reply), 10, 0) return int(n), err case nil: return 0, ErrNil case Error: return 0, reply } return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply) } // Int64 is a helper that converts a command reply to 64 bit integer. If err is // not equal to nil, then Int64 returns 0, err. Otherwise, Int64 converts the // reply to an int64 as follows: // // Reply type Result // integer reply, nil // bulk string parsed reply, nil // nil 0, ErrNil // other 0, error func Int64(reply interface{}, err error) (int64, error) { if err != nil { return 0, err } switch reply := reply.(type) { case int64: return reply, nil case []byte: n, err := strconv.ParseInt(string(reply), 10, 64) return n, err case nil: return 0, ErrNil case Error: return 0, reply } return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply) } func errNegativeInt(v int64) error { return fmt.Errorf("redigo: unexpected negative value %v for Uint64", v) } // Uint64 is a helper that converts a command reply to 64 bit unsigned integer. // If err is not equal to nil, then Uint64 returns 0, err. Otherwise, Uint64 converts the // reply to an uint64 as follows: // // Reply type Result // +integer reply, nil // bulk string parsed reply, nil // nil 0, ErrNil // other 0, error func Uint64(reply interface{}, err error) (uint64, error) { if err != nil { return 0, err } switch reply := reply.(type) { case int64: if reply < 0 { return 0, errNegativeInt(reply) } return uint64(reply), nil case []byte: n, err := strconv.ParseUint(string(reply), 10, 64) return n, err case nil: return 0, ErrNil case Error: return 0, reply } return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply) } // Float64 is a helper that converts a command reply to 64 bit float. If err is // not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts // the reply to a float64 as follows: // // Reply type Result // bulk string parsed reply, nil // nil 0, ErrNil // other 0, error func Float64(reply interface{}, err error) (float64, error) { if err != nil { return 0, err } switch reply := reply.(type) { case []byte: n, err := strconv.ParseFloat(string(reply), 64) return n, err case nil: return 0, ErrNil case Error: return 0, reply } return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply) } // String is a helper that converts a command reply to a string. If err is not // equal to nil, then String returns "", err. Otherwise String converts the // reply to a string as follows: // // Reply type Result // bulk string string(reply), nil // simple string reply, nil // nil "", ErrNil // other "", error func String(reply interface{}, err error) (string, error) { if err != nil { return "", err } switch reply := reply.(type) { case []byte: return string(reply), nil case string: return reply, nil case nil: return "", ErrNil case Error: return "", reply } return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply) } // Bytes is a helper that converts a command reply to a slice of bytes. If err // is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts // the reply to a slice of bytes as follows: // // Reply type Result // bulk string reply, nil // simple string []byte(reply), nil // nil nil, ErrNil // other nil, error func Bytes(reply interface{}, err error) ([]byte, error) { if err != nil { return nil, err } switch reply := reply.(type) { case []byte: return reply, nil case string: return []byte(reply), nil case nil: return nil, ErrNil case Error: return nil, reply } return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply) } // Bool is a helper that converts a command reply to a boolean. If err is not // equal to nil, then Bool returns false, err. Otherwise Bool converts the // reply to boolean as follows: // // Reply type Result // integer value != 0, nil // bulk string strconv.ParseBool(reply) // nil false, ErrNil // other false, error func Bool(reply interface{}, err error) (bool, error) { if err != nil { return false, err } switch reply := reply.(type) { case int64: return reply != 0, nil case []byte: return strconv.ParseBool(string(reply)) case nil: return false, ErrNil case Error: return false, reply } return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply) } // MultiBulk is a helper that converts an array command reply to a []interface{}. // // Deprecated: Use Values instead. func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) } // Values is a helper that converts an array command reply to a []interface{}. // If err is not equal to nil, then Values returns nil, err. Otherwise, Values // converts the reply as follows: // // Reply type Result // array reply, nil // nil nil, ErrNil // other nil, error func Values(reply interface{}, err error) ([]interface{}, error) { if err != nil { return nil, err } switch reply := reply.(type) { case []interface{}: return reply, nil case nil: return nil, ErrNil case Error: return nil, reply } return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply) } func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error { if err != nil { return err } switch reply := reply.(type) { case []interface{}: makeSlice(len(reply)) for i := range reply { if reply[i] == nil { continue } if err := assign(i, reply[i]); err != nil { return err } } return nil case nil: return ErrNil case Error: return reply } return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply) } // Float64s is a helper that converts an array command reply to a []float64. If // err is not equal to nil, then Float64s returns nil, err. Nil array items are // converted to 0 in the output slice. Floats64 returns an error if an array // item is not a bulk string or nil. func Float64s(reply interface{}, err error) ([]float64, error) { var result []float64 err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error { switch v := v.(type) { case []byte: f, err := strconv.ParseFloat(string(v), 64) result[i] = f return err case Error: return v default: return fmt.Errorf("redigo: unexpected element type for Float64s, got type %T", v) } }) return result, err } // Strings is a helper that converts an array command reply to a []string. If // err is not equal to nil, then Strings returns nil, err. Nil array items are // converted to "" in the output slice. Strings returns an error if an array // item is not a bulk string or nil. func Strings(reply interface{}, err error) ([]string, error) { var result []string err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error { switch v := v.(type) { case string: result[i] = v return nil case []byte: result[i] = string(v) return nil case Error: return v default: return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v) } }) return result, err } // ByteSlices is a helper that converts an array command reply to a [][]byte. // If err is not equal to nil, then ByteSlices returns nil, err. Nil array // items are stay nil. ByteSlices returns an error if an array item is not a // bulk string or nil. func ByteSlices(reply interface{}, err error) ([][]byte, error) { var result [][]byte err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error { switch v := v.(type) { case []byte: result[i] = v return nil case Error: return v default: return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v) } }) return result, err } // Int64s is a helper that converts an array command reply to a []int64. // If err is not equal to nil, then Int64s returns nil, err. Nil array // items are stay nil. Int64s returns an error if an array item is not a // bulk string or nil. func Int64s(reply interface{}, err error) ([]int64, error) { var result []int64 err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error { switch v := v.(type) { case int64: result[i] = v return nil case []byte: n, err := strconv.ParseInt(string(v), 10, 64) result[i] = n return err case Error: return v default: return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v) } }) return result, err } // Ints is a helper that converts an array command reply to a []int. // If err is not equal to nil, then Ints returns nil, err. Nil array // items are stay nil. Ints returns an error if an array item is not a // bulk string or nil. func Ints(reply interface{}, err error) ([]int, error) { var result []int err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error { switch v := v.(type) { case int64: n := int(v) if int64(n) != v { return strconv.ErrRange } result[i] = n return nil case []byte: n, err := strconv.Atoi(string(v)) result[i] = n return err case Error: return v default: return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v) } }) return result, err } // mapHelper builds a map from the data in reply. func mapHelper(reply interface{}, err error, name string, makeMap func(int), assign func(key string, value interface{}) error) error { values, err := Values(reply, err) if err != nil { return err } if len(values)%2 != 0 { return fmt.Errorf("redigo: %s expects even number of values result, got %d", name, len(values)) } makeMap(len(values) / 2) for i := 0; i < len(values); i += 2 { key, ok := values[i].([]byte) if !ok { return fmt.Errorf("redigo: %s key[%d] not a bulk string value, got %T", name, i, values[i]) } if err := assign(string(key), values[i+1]); err != nil { return err } } return nil } // StringMap is a helper that converts an array of strings (alternating key, value) // into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format. // Requires an even number of values in result. func StringMap(reply interface{}, err error) (map[string]string, error) { var result map[string]string err = mapHelper(reply, err, "StringMap", func(n int) { result = make(map[string]string, n) }, func(key string, v interface{}) error { value, ok := v.([]byte) if !ok { return fmt.Errorf("redigo: StringMap for %q not a bulk string value, got %T", key, v) } result[key] = string(value) return nil }, ) return result, err } // IntMap is a helper that converts an array of strings (alternating key, value) // into a map[string]int. The HGETALL commands return replies in this format. // Requires an even number of values in result. func IntMap(result interface{}, err error) (map[string]int, error) { var m map[string]int err = mapHelper(result, err, "IntMap", func(n int) { m = make(map[string]int, n) }, func(key string, v interface{}) error { value, err := Int(v, nil) if err != nil { return err } m[key] = value return nil }, ) return m, err } // Int64Map is a helper that converts an array of strings (alternating key, value) // into a map[string]int64. The HGETALL commands return replies in this format. // Requires an even number of values in result. func Int64Map(result interface{}, err error) (map[string]int64, error) { var m map[string]int64 err = mapHelper(result, err, "Int64Map", func(n int) { m = make(map[string]int64, n) }, func(key string, v interface{}) error { value, err := Int64(v, nil) if err != nil { return err } m[key] = value return nil }, ) return m, err } // Float64Map is a helper that converts an array of strings (alternating key, value) // into a map[string]float64. The HGETALL commands return replies in this format. // Requires an even number of values in result. func Float64Map(result interface{}, err error) (map[string]float64, error) { var m map[string]float64 err = mapHelper(result, err, "Float64Map", func(n int) { m = make(map[string]float64, n) }, func(key string, v interface{}) error { value, err := Float64(v, nil) if err != nil { return err } m[key] = value return nil }, ) return m, err } // Positions is a helper that converts an array of positions (lat, long) // into a [][2]float64. The GEOPOS command returns replies in this format. func Positions(result interface{}, err error) ([]*[2]float64, error) { values, err := Values(result, err) if err != nil { return nil, err } positions := make([]*[2]float64, len(values)) for i := range values { if values[i] == nil { continue } p, ok := values[i].([]interface{}) if !ok { return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i]) } if len(p) != 2 { return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p)) } lat, err := Float64(p[0], nil) if err != nil { return nil, err } long, err := Float64(p[1], nil) if err != nil { return nil, err } positions[i] = &[2]float64{lat, long} } return positions, nil } // Uint64s is a helper that converts an array command reply to a []uint64. // If err is not equal to nil, then Uint64s returns nil, err. Nil array // items are stay nil. Uint64s returns an error if an array item is not a // bulk string or nil. func Uint64s(reply interface{}, err error) ([]uint64, error) { var result []uint64 err = sliceHelper(reply, err, "Uint64s", func(n int) { result = make([]uint64, n) }, func(i int, v interface{}) error { switch v := v.(type) { case uint64: result[i] = v return nil case []byte: n, err := strconv.ParseUint(string(v), 10, 64) result[i] = n return err case Error: return v default: return fmt.Errorf("redigo: unexpected element type for Uint64s, got type %T", v) } }) return result, err } // Uint64Map is a helper that converts an array of strings (alternating key, value) // into a map[string]uint64. The HGETALL commands return replies in this format. // Requires an even number of values in result. func Uint64Map(result interface{}, err error) (map[string]uint64, error) { var m map[string]uint64 err = mapHelper(result, err, "Uint64Map", func(n int) { m = make(map[string]uint64, n) }, func(key string, v interface{}) error { value, err := Uint64(v, nil) if err != nil { return err } m[key] = value return nil }, ) return m, err } // SlowLogs is a helper that parse the SLOWLOG GET command output and // return the array of SlowLog func SlowLogs(result interface{}, err error) ([]SlowLog, error) { rawLogs, err := Values(result, err) if err != nil { return nil, err } logs := make([]SlowLog, len(rawLogs)) for i, e := range rawLogs { rawLog, ok := e.([]interface{}) if !ok { return nil, fmt.Errorf("redigo: slowlog element is not an array, got %T", e) } var log SlowLog if len(rawLog) < 4 { return nil, fmt.Errorf("redigo: slowlog element has %d elements, expected at least 4", len(rawLog)) } log.ID, ok = rawLog[0].(int64) if !ok { return nil, fmt.Errorf("redigo: slowlog element[0] not an int64, got %T", rawLog[0]) } timestamp, ok := rawLog[1].(int64) if !ok { return nil, fmt.Errorf("redigo: slowlog element[1] not an int64, got %T", rawLog[1]) } log.Time = time.Unix(timestamp, 0) duration, ok := rawLog[2].(int64) if !ok { return nil, fmt.Errorf("redigo: slowlog element[2] not an int64, got %T", rawLog[2]) } log.ExecutionTime = time.Duration(duration) * time.Microsecond log.Args, err = Strings(rawLog[3], nil) if err != nil { return nil, fmt.Errorf("redigo: slowlog element[3] is not array of strings: %w", err) } if len(rawLog) >= 6 { log.ClientAddr, err = String(rawLog[4], nil) if err != nil { return nil, fmt.Errorf("redigo: slowlog element[4] is not a string: %w", err) } log.ClientName, err = String(rawLog[5], nil) if err != nil { return nil, fmt.Errorf("redigo: slowlog element[5] is not a string: %w", err) } } logs[i] = log } return logs, nil } // Latencies is a helper that parses the LATENCY LATEST command output and // return the slice of Latency values. func Latencies(result interface{}, err error) ([]Latency, error) { rawLatencies, err := Values(result, err) if err != nil { return nil, err } latencies := make([]Latency, len(rawLatencies)) for i, e := range rawLatencies { rawLatency, ok := e.([]interface{}) if !ok { return nil, fmt.Errorf("redigo: latencies element is not slice, got %T", e) } var event Latency if len(rawLatency) != 4 { return nil, fmt.Errorf("redigo: latencies element has %d elements, expected 4", len(rawLatency)) } event.Name, err = String(rawLatency[0], nil) if err != nil { return nil, fmt.Errorf("redigo: latencies element[0] is not a string: %w", err) } timestamp, ok := rawLatency[1].(int64) if !ok { return nil, fmt.Errorf("redigo: latencies element[1] not an int64, got %T", rawLatency[1]) } event.Time = time.Unix(timestamp, 0) latestDuration, ok := rawLatency[2].(int64) if !ok { return nil, fmt.Errorf("redigo: latencies element[2] not an int64, got %T", rawLatency[2]) } event.Latest = time.Duration(latestDuration) * time.Millisecond maxDuration, ok := rawLatency[3].(int64) if !ok { return nil, fmt.Errorf("redigo: latencies element[3] not an int64, got %T", rawLatency[3]) } event.Max = time.Duration(maxDuration) * time.Millisecond latencies[i] = event } return latencies, nil } // LatencyHistories is a helper that parse the LATENCY HISTORY command output and // returns a LatencyHistory slice. func LatencyHistories(result interface{}, err error) ([]LatencyHistory, error) { rawLogs, err := Values(result, err) if err != nil { return nil, err } latencyHistories := make([]LatencyHistory, len(rawLogs)) for i, e := range rawLogs { rawLog, ok := e.([]interface{}) if !ok { return nil, fmt.Errorf("redigo: latency history element is not an slice, got %T", e) } var event LatencyHistory timestamp, ok := rawLog[0].(int64) if !ok { return nil, fmt.Errorf("redigo: latency history element[0] not an int64, got %T", rawLog[0]) } event.Time = time.Unix(timestamp, 0) duration, ok := rawLog[1].(int64) if !ok { return nil, fmt.Errorf("redigo: latency history element[1] not an int64, got %T", rawLog[1]) } event.ExecutionTime = time.Duration(duration) * time.Millisecond latencyHistories[i] = event } return latencyHistories, nil } redigo-1.9.2/redis/reply_test.go000066400000000000000000000254541456663410400166460ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "fmt" "math" "reflect" "strconv" "testing" "time" "github.com/gomodule/redigo/redis" "github.com/stretchr/testify/require" ) var ( maxUint64Str = strconv.FormatUint(math.MaxUint64, 10) ) type valueError struct { v interface{} err error } func ve(v interface{}, err error) valueError { return valueError{v, err} } var replyTests = []struct { name interface{} actual valueError expected valueError }{ { "ints([[]byte, []byte])", ve(redis.Ints([]interface{}{[]byte("4"), []byte("5")}, nil)), ve([]int{4, 5}, nil), }, { "ints([nt64, int64])", ve(redis.Ints([]interface{}{int64(4), int64(5)}, nil)), ve([]int{4, 5}, nil), }, { "ints([[]byte, nil, []byte])", ve(redis.Ints([]interface{}{[]byte("4"), nil, []byte("5")}, nil)), ve([]int{4, 0, 5}, nil), }, { "ints(nil)", ve(redis.Ints(nil, nil)), ve([]int(nil), redis.ErrNil), }, { "int64s([[]byte, []byte])", ve(redis.Int64s([]interface{}{[]byte("4"), []byte("5")}, nil)), ve([]int64{4, 5}, nil), }, { "int64s([int64, int64])", ve(redis.Int64s([]interface{}{int64(4), int64(5)}, nil)), ve([]int64{4, 5}, nil), }, { "uint64s([[]byte, []byte])", ve(redis.Uint64s([]interface{}{[]byte(maxUint64Str), []byte("5")}, nil)), ve([]uint64{math.MaxUint64, 5}, nil), }, { "Uint64Map([[]byte, []byte])", ve(redis.Uint64Map([]interface{}{[]byte("key1"), []byte(maxUint64Str), []byte("key2"), []byte("5")}, nil)), ve(map[string]uint64{"key1": math.MaxUint64, "key2": 5}, nil), }, { "strings([[]byte, []byte])", ve(redis.Strings([]interface{}{[]byte("v1"), []byte("v2")}, nil)), ve([]string{"v1", "v2"}, nil), }, { "strings([string, string])", ve(redis.Strings([]interface{}{"v1", "v2"}, nil)), ve([]string{"v1", "v2"}, nil), }, { "byteslices([v1, v2])", ve(redis.ByteSlices([]interface{}{[]byte("v1"), []byte("v2")}, nil)), ve([][]byte{[]byte("v1"), []byte("v2")}, nil), }, { "float64s([v1, v2])", ve(redis.Float64s([]interface{}{[]byte("1.234"), []byte("5.678")}, nil)), ve([]float64{1.234, 5.678}, nil), }, { "values([v1, v2])", ve(redis.Values([]interface{}{[]byte("v1"), []byte("v2")}, nil)), ve([]interface{}{[]byte("v1"), []byte("v2")}, nil), }, { "values(nil)", ve(redis.Values(nil, nil)), ve([]interface{}(nil), redis.ErrNil), }, { "float64(1.0)", ve(redis.Float64([]byte("1.0"), nil)), ve(float64(1.0), nil), }, { "float64(nil)", ve(redis.Float64(nil, nil)), ve(float64(0.0), redis.ErrNil), }, { "float64Map([[]byte, []byte])", ve(redis.Float64Map([]interface{}{[]byte("key1"), []byte("1.234"), []byte("key2"), []byte("5.678")}, nil)), ve(map[string]float64{"key1": 1.234, "key2": 5.678}, nil), }, { "uint64(1)", ve(redis.Uint64(int64(1), nil)), ve(uint64(1), nil), }, { "uint64(-1)", ve(redis.Uint64(int64(-1), nil)), ve(uint64(0), redis.ErrNegativeInt(-1)), }, { "positions([[1, 2], nil, [3, 4]])", ve(redis.Positions([]interface{}{[]interface{}{[]byte("1"), []byte("2")}, nil, []interface{}{[]byte("3"), []byte("4")}}, nil)), ve([]*[2]float64{{1.0, 2.0}, nil, {3.0, 4.0}}, nil), }, { "SlowLogs(1, 1579625870, 3, {set, x, y}, localhost:1234, testClient", ve(getSlowLog()), ve(redis.SlowLog{ID: 1, Time: time.Unix(1579625870, 0), ExecutionTime: time.Duration(3) * time.Microsecond, Args: []string{"set", "x", "y"}, ClientAddr: "localhost:1234", ClientName: "testClient"}, nil), }, } func getSlowLog() (redis.SlowLog, error) { slowLogs, _ := redis.SlowLogs([]interface{}{[]interface{}{int64(1), int64(1579625870), int64(3), []interface{}{"set", "x", "y"}, "localhost:1234", "testClient"}}, nil) if err != nil { return redis.SlowLog{}, err } return slowLogs[0], nil } func TestReply(t *testing.T) { for _, rt := range replyTests { if rt.actual.err != rt.expected.err && rt.actual.err.Error() != rt.expected.err.Error() { t.Errorf("%s returned err %v, want %v", rt.name, rt.actual.err, rt.expected.err) continue } if !reflect.DeepEqual(rt.actual.v, rt.expected.v) { t.Errorf("%s=%+v, want %+v", rt.name, rt.actual.v, rt.expected.v) } } } func TestSlowLog(t *testing.T) { c, err := dial() if err != nil { t.Errorf("TestSlowLog failed during dial with error " + err.Error()) return } defer c.Close() resultStr, err := redis.Strings(c.Do("CONFIG", "GET", "slowlog-log-slower-than")) if err != nil { t.Errorf("TestSlowLog failed during CONFIG GET slowlog-log-slower-than with error " + err.Error()) return } // in case of older verion < 2.2.12 where SLOWLOG command is not supported // don't run the test if len(resultStr) == 0 { return } slowLogSlowerThanOldCfg, err := strconv.Atoi(resultStr[1]) if err != nil { t.Errorf("TestSlowLog failed during strconv.Atoi with error " + err.Error()) return } result, err := c.Do("CONFIG", "SET", "slowlog-log-slower-than", "0") if err != nil && result != "OK" { t.Errorf("TestSlowLog failed during CONFIG SET with error " + err.Error()) return } result, err = c.Do("SLOWLOG", "GET") if err != nil { t.Errorf("TestSlowLog failed during SLOWLOG GET with error " + err.Error()) return } slowLogs, err := redis.SlowLogs(result, err) if err != nil { t.Errorf("TestSlowLog failed during redis.SlowLogs with error " + err.Error()) return } slowLog := slowLogs[0] if slowLog.Args[0] != "CONFIG" || slowLog.Args[1] != "SET" || slowLog.Args[2] != "slowlog-log-slower-than" || slowLog.Args[3] != "0" { t.Errorf("%s=%+v, want %+v", "TestSlowLog test failed : ", slowLog.Args[0]+" "+slowLog.Args[1]+" "+slowLog.Args[2]+" "+ slowLog.Args[3], "CONFIG SET slowlog-log-slower-than 0") } // reset the old configuration after test result, err = c.Do("CONFIG", "SET", "slowlog-log-slower-than", slowLogSlowerThanOldCfg) if err != nil && result != "OK" { t.Errorf("TestSlowLog failed during CONFIG SET with error " + err.Error()) return } } func TestLatency(t *testing.T) { c, err := dial() require.NoError(t, err) defer c.Close() resultStr, err := redis.Strings(c.Do("CONFIG", "GET", "latency-monitor-threshold")) require.NoError(t, err) // LATENCY commands were added in 2.8.13 so might not be supported. if len(resultStr) == 0 { t.Skip("Latency commands not supported") } latencyMonitorThresholdOldCfg, err := strconv.Atoi(resultStr[1]) require.NoError(t, err) // Enable latency monitoring for events that take 1ms or longer. result, err := c.Do("CONFIG", "SET", "latency-monitor-threshold", "1") // reset the old configuration after test. defer func() { res, err := c.Do("CONFIG", "SET", "latency-monitor-threshold", latencyMonitorThresholdOldCfg) require.NoError(t, err) require.Equal(t, "OK", res) }() require.NoError(t, err) require.Equal(t, "OK", result) // Sleep for 1ms to register a slow event. _, err = c.Do("DEBUG", "SLEEP", 0.001) require.NoError(t, err) result, err = c.Do("LATENCY", "LATEST") require.NoError(t, err) latestLatencies, err := redis.Latencies(result, err) require.NoError(t, err) require.Equal(t, 1, len(latestLatencies)) latencyEvent := latestLatencies[0] expected := redis.Latency{ Name: "command", Latest: time.Millisecond, Max: time.Millisecond, Time: latencyEvent.Time, } require.Equal(t, latencyEvent, expected) } func TestLatencyHistories(t *testing.T) { c, err := dial() require.NoError(t, err) defer c.Close() res, err := redis.Strings(c.Do("CONFIG", "GET", "latency-monitor-threshold")) require.NoError(t, err) // LATENCY commands were added in 2.8.13 so might not be supported. if len(res) == 0 { t.Skip("Latency commands not supported") } latencyMonitorThresholdOldCfg, err := strconv.Atoi(res[1]) require.NoError(t, err) // Reset so we're compatible with -count=X _, err = c.Do("LATENCY", "RESET", "command") require.NoError(t, err) // Enable latency monitoring for events that take 1ms or longer result, err := c.Do("CONFIG", "SET", "latency-monitor-threshold", "1") // reset the old configuration after test. defer func() { res, err := c.Do("CONFIG", "SET", "latency-monitor-threshold", latencyMonitorThresholdOldCfg) require.NoError(t, err) require.Equal(t, "OK", res) }() require.NoError(t, err) require.Equal(t, "OK", result) // Sleep for 1ms to register a slow event _, err = c.Do("DEBUG", "SLEEP", 0.001) require.NoError(t, err) result, err = c.Do("LATENCY", "HISTORY", "command") require.NoError(t, err) latencyHistory, err := redis.LatencyHistories(result, err) require.NoError(t, err) require.Len(t, latencyHistory, 1) latencyEvent := latencyHistory[0] require.Equal(t, time.Millisecond, latencyEvent.ExecutionTime) } // dial wraps DialDefaultServer() with a more suitable function name for examples. func dial() (redis.Conn, error) { return redis.DialDefaultServer() } // serverAddr wraps DefaultServerAddr() with a more suitable function name for examples. func serverAddr() (string, error) { return redis.DefaultServerAddr() } func ExampleBool() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() if _, err = c.Do("SET", "foo", 1); err != nil { fmt.Println(err) return } exists, err := redis.Bool(c.Do("EXISTS", "foo")) if err != nil { fmt.Println(err) return } fmt.Printf("%#v\n", exists) // Output: // true } func ExampleInt() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() _, err = c.Do("SET", "k1", 1) if err != nil { fmt.Println(err) return } n, err := redis.Int(c.Do("GET", "k1")) if err != nil { fmt.Println(err) return } fmt.Printf("%#v\n", n) n, err = redis.Int(c.Do("INCR", "k1")) if err != nil { fmt.Println(err) return } fmt.Printf("%#v\n", n) // Output: // 1 // 2 } func ExampleInts() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() _, err = c.Do("SADD", "set_with_integers", 4, 5, 6) if err != nil { fmt.Println(err) return } ints, err := redis.Ints(c.Do("SMEMBERS", "set_with_integers")) if err != nil { fmt.Println(err) return } fmt.Printf("%#v\n", ints) // Output: // []int{4, 5, 6} } func ExampleString() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() _, err = c.Do("SET", "hello", "world") if err != nil { fmt.Println(err) return } s, err := redis.String(c.Do("GET", "hello")) if err != nil { fmt.Println(err) return } fmt.Printf("%#v\n", s) // Output: // "world" } redigo-1.9.2/redis/scan.go000066400000000000000000000416041456663410400153730ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "errors" "fmt" "reflect" "strconv" "strings" "sync" ) var ( scannerType = reflect.TypeOf((*Scanner)(nil)).Elem() ) func ensureLen(d reflect.Value, n int) { if n > d.Cap() { d.Set(reflect.MakeSlice(d.Type(), n, n)) } else { d.SetLen(n) } } func cannotConvert(d reflect.Value, s interface{}) error { var sname string switch s.(type) { case string: sname = "Redis simple string" case Error: sname = "Redis error" case int64: sname = "Redis integer" case []byte: sname = "Redis bulk string" case []interface{}: sname = "Redis array" case nil: sname = "Redis nil" default: sname = reflect.TypeOf(s).String() } return fmt.Errorf("cannot convert from %s to %s", sname, d.Type()) } func convertAssignNil(d reflect.Value) (err error) { switch d.Type().Kind() { case reflect.Slice, reflect.Interface: d.Set(reflect.Zero(d.Type())) default: err = cannotConvert(d, nil) } return err } func convertAssignError(d reflect.Value, s Error) (err error) { if d.Kind() == reflect.String { d.SetString(string(s)) } else if d.Kind() == reflect.Slice && d.Type().Elem().Kind() == reflect.Uint8 { d.SetBytes([]byte(s)) } else { err = cannotConvert(d, s) } return } func convertAssignString(d reflect.Value, s string) (err error) { switch d.Type().Kind() { case reflect.Float32, reflect.Float64: var x float64 x, err = strconv.ParseFloat(s, d.Type().Bits()) d.SetFloat(x) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: var x int64 x, err = strconv.ParseInt(s, 10, d.Type().Bits()) d.SetInt(x) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: var x uint64 x, err = strconv.ParseUint(s, 10, d.Type().Bits()) d.SetUint(x) case reflect.Bool: var x bool x, err = strconv.ParseBool(s) d.SetBool(x) case reflect.String: d.SetString(s) case reflect.Slice: if d.Type().Elem().Kind() == reflect.Uint8 { d.SetBytes([]byte(s)) } else { err = cannotConvert(d, s) } case reflect.Ptr: err = convertAssignString(d.Elem(), s) default: err = cannotConvert(d, s) } return } func convertAssignBulkString(d reflect.Value, s []byte) (err error) { switch d.Type().Kind() { case reflect.Slice: // Handle []byte destination here to avoid unnecessary // []byte -> string -> []byte converion. if d.Type().Elem().Kind() == reflect.Uint8 { d.SetBytes(s) } else { err = cannotConvert(d, s) } case reflect.Ptr: if d.CanInterface() && d.CanSet() { if s == nil { if d.IsNil() { return nil } d.Set(reflect.Zero(d.Type())) return nil } if d.IsNil() { d.Set(reflect.New(d.Type().Elem())) } if sc, ok := d.Interface().(Scanner); ok { return sc.RedisScan(s) } } err = convertAssignString(d, string(s)) default: err = convertAssignString(d, string(s)) } return err } func convertAssignInt(d reflect.Value, s int64) (err error) { switch d.Type().Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: d.SetInt(s) if d.Int() != s { err = strconv.ErrRange d.SetInt(0) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if s < 0 { err = strconv.ErrRange } else { x := uint64(s) d.SetUint(x) if d.Uint() != x { err = strconv.ErrRange d.SetUint(0) } } case reflect.Bool: d.SetBool(s != 0) default: err = cannotConvert(d, s) } return } func convertAssignValue(d reflect.Value, s interface{}) (err error) { if d.Kind() != reflect.Ptr { if d.CanAddr() { d2 := d.Addr() if d2.CanInterface() { if scanner, ok := d2.Interface().(Scanner); ok { return scanner.RedisScan(s) } } } } else if d.CanInterface() { // Already a reflect.Ptr if d.IsNil() { d.Set(reflect.New(d.Type().Elem())) } if scanner, ok := d.Interface().(Scanner); ok { return scanner.RedisScan(s) } } switch s := s.(type) { case nil: err = convertAssignNil(d) case []byte: err = convertAssignBulkString(d, s) case int64: err = convertAssignInt(d, s) case string: err = convertAssignString(d, s) case Error: err = convertAssignError(d, s) default: err = cannotConvert(d, s) } return err } func convertAssignArray(d reflect.Value, s []interface{}) error { if d.Type().Kind() != reflect.Slice { return cannotConvert(d, s) } ensureLen(d, len(s)) for i := 0; i < len(s); i++ { if err := convertAssignValue(d.Index(i), s[i]); err != nil { return err } } return nil } func convertAssign(d interface{}, s interface{}) (err error) { if scanner, ok := d.(Scanner); ok { return scanner.RedisScan(s) } // Handle the most common destination types using type switches and // fall back to reflection for all other types. switch s := s.(type) { case nil: // ignore case []byte: switch d := d.(type) { case *string: *d = string(s) case *int: *d, err = strconv.Atoi(string(s)) case *bool: *d, err = strconv.ParseBool(string(s)) case *[]byte: *d = s case *interface{}: *d = s case nil: // skip value default: if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { err = cannotConvert(d, s) } else { err = convertAssignBulkString(d.Elem(), s) } } case int64: switch d := d.(type) { case *int: x := int(s) if int64(x) != s { err = strconv.ErrRange x = 0 } *d = x case *bool: *d = s != 0 case *interface{}: *d = s case nil: // skip value default: if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { err = cannotConvert(d, s) } else { err = convertAssignInt(d.Elem(), s) } } case string: switch d := d.(type) { case *string: *d = s case *interface{}: *d = s case nil: // skip value default: err = cannotConvert(reflect.ValueOf(d), s) } case []interface{}: switch d := d.(type) { case *[]interface{}: *d = s case *interface{}: *d = s case nil: // skip value default: if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { err = cannotConvert(d, s) } else { err = convertAssignArray(d.Elem(), s) } } case Error: err = s default: err = cannotConvert(reflect.ValueOf(d), s) } return } // Scan copies from src to the values pointed at by dest. // // Scan uses RedisScan if available otherwise: // // The values pointed at by dest must be an integer, float, boolean, string, // []byte, interface{} or slices of these types. Scan uses the standard strconv // package to convert bulk strings to numeric and boolean types. // // If a dest value is nil, then the corresponding src value is skipped. // // If a src element is nil, then the corresponding dest value is not modified. // // To enable easy use of Scan in a loop, Scan returns the slice of src // following the copied values. func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) { if len(src) < len(dest) { return nil, errors.New("redigo.Scan: array short") } var err error for i, d := range dest { err = convertAssign(d, src[i]) if err != nil { err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err) break } } return src[len(dest):], err } type fieldSpec struct { name string index []int omitEmpty bool } type structSpec struct { m map[string]*fieldSpec l []*fieldSpec } func (ss *structSpec) fieldSpec(name []byte) *fieldSpec { return ss.m[string(name)] } func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec, seen map[reflect.Type]struct{}) error { if _, ok := seen[t]; ok { // Protect against infinite recursion. return fmt.Errorf("recursive struct definition for %v", t) } seen[t] = struct{}{} LOOP: for i := 0; i < t.NumField(); i++ { f := t.Field(i) switch { case f.PkgPath != "" && !f.Anonymous: // Ignore unexported fields. case f.Anonymous: switch f.Type.Kind() { case reflect.Struct: if err := compileStructSpec(f.Type, depth, append(index, i), ss, seen); err != nil { return err } case reflect.Ptr: if f.Type.Elem().Kind() == reflect.Struct { if err := compileStructSpec(f.Type.Elem(), depth, append(index, i), ss, seen); err != nil { return err } } } default: fs := &fieldSpec{name: f.Name} tag := f.Tag.Get("redis") var p string first := true for len(tag) > 0 { i := strings.IndexByte(tag, ',') if i < 0 { p, tag = tag, "" } else { p, tag = tag[:i], tag[i+1:] } if p == "-" { continue LOOP } if first && len(p) > 0 { fs.name = p first = false } else { switch p { case "omitempty": fs.omitEmpty = true default: panic(fmt.Errorf("redigo: unknown field tag %s for type %s", p, t.Name())) } } } d, found := depth[fs.name] if !found { d = 1 << 30 } switch { case len(index) == d: // At same depth, remove from result. delete(ss.m, fs.name) j := 0 for i := 0; i < len(ss.l); i++ { if fs.name != ss.l[i].name { ss.l[j] = ss.l[i] j += 1 } } ss.l = ss.l[:j] case len(index) < d: fs.index = make([]int, len(index)+1) copy(fs.index, index) fs.index[len(index)] = i depth[fs.name] = len(index) ss.m[fs.name] = fs ss.l = append(ss.l, fs) } } } return nil } var ( structSpecMutex sync.RWMutex structSpecCache = make(map[reflect.Type]*structSpec) ) func structSpecForType(t reflect.Type) (*structSpec, error) { structSpecMutex.RLock() ss, found := structSpecCache[t] structSpecMutex.RUnlock() if found { return ss, nil } structSpecMutex.Lock() defer structSpecMutex.Unlock() ss, found = structSpecCache[t] if found { return ss, nil } ss = &structSpec{m: make(map[string]*fieldSpec)} if err := compileStructSpec(t, make(map[string]int), nil, ss, make(map[reflect.Type]struct{})); err != nil { return nil, fmt.Errorf("compile struct: %s: %w", t, err) } structSpecCache[t] = ss return ss, nil } var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct") // ScanStruct scans alternating names and values from src to a struct. The // HGETALL and CONFIG GET commands return replies in this format. // // ScanStruct uses exported field names to match values in the response. Use // 'redis' field tag to override the name: // // Field int `redis:"myName"` // // Fields with the tag redis:"-" are ignored. // // Each field uses RedisScan if available otherwise: // Integer, float, boolean, string and []byte fields are supported. Scan uses the // standard strconv package to convert bulk string values to numeric and // boolean types. // // If a src element is nil, then the corresponding field is not modified. func ScanStruct(src []interface{}, dest interface{}) error { d := reflect.ValueOf(dest) if d.Kind() != reflect.Ptr || d.IsNil() { return errScanStructValue } d = d.Elem() if d.Kind() != reflect.Struct { return errScanStructValue } if len(src)%2 != 0 { return errors.New("redigo.ScanStruct: number of values not a multiple of 2") } ss, err := structSpecForType(d.Type()) if err != nil { return fmt.Errorf("redigo.ScanStruct: %w", err) } for i := 0; i < len(src); i += 2 { s := src[i+1] if s == nil { continue } name, ok := src[i].([]byte) if !ok { return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i) } fs := ss.fieldSpec(name) if fs == nil { continue } if err := convertAssignValue(fieldByIndexCreate(d, fs.index), s); err != nil { return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err) } } return nil } var ( errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct") ) // ScanSlice scans src to the slice pointed to by dest. // // If the target is a slice of types which implement Scanner then the custom // RedisScan method is used otherwise the following rules apply: // // The elements in the dest slice must be integer, float, boolean, string, struct // or pointer to struct values. // // Struct fields must be integer, float, boolean or string values. All struct // fields are used unless a subset is specified using fieldNames. func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error { d := reflect.ValueOf(dest) if d.Kind() != reflect.Ptr || d.IsNil() { return errScanSliceValue } d = d.Elem() if d.Kind() != reflect.Slice { return errScanSliceValue } isPtr := false t := d.Type().Elem() st := t if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { isPtr = true t = t.Elem() } if t.Kind() != reflect.Struct || st.Implements(scannerType) { ensureLen(d, len(src)) for i, s := range src { if s == nil { continue } if err := convertAssignValue(d.Index(i), s); err != nil { return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err) } } return nil } ss, err := structSpecForType(t) if err != nil { return fmt.Errorf("redigo.ScanSlice: %w", err) } fss := ss.l if len(fieldNames) > 0 { fss = make([]*fieldSpec, len(fieldNames)) for i, name := range fieldNames { fss[i] = ss.m[name] if fss[i] == nil { return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name) } } } if len(fss) == 0 { return errors.New("redigo.ScanSlice: no struct fields") } n := len(src) / len(fss) if n*len(fss) != len(src) { return errors.New("redigo.ScanSlice: length not a multiple of struct field count") } ensureLen(d, n) for i := 0; i < n; i++ { d := d.Index(i) if isPtr { if d.IsNil() { d.Set(reflect.New(t)) } d = d.Elem() } for j, fs := range fss { s := src[i*len(fss)+j] if s == nil { continue } if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err) } } } return nil } // Args is a helper for constructing command arguments from structured values. type Args []interface{} // Add returns the result of appending value to args. func (args Args) Add(value ...interface{}) Args { return append(args, value...) } // AddFlat returns the result of appending the flattened value of v to args. // // Maps are flattened by appending the alternating keys and map values to args. // // Slices are flattened by appending the slice elements to args. // // Structs are flattened by appending the alternating names and values of // exported fields to args. If v is a nil struct pointer, then nothing is // appended. The 'redis' field tag overrides struct field names. See ScanStruct // for more information on the use of the 'redis' field tag. // // Other types are appended to args as is. // panics if v includes a recursive anonymous struct. func (args Args) AddFlat(v interface{}) Args { rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Struct: args = flattenStruct(args, rv) case reflect.Slice: for i := 0; i < rv.Len(); i++ { args = append(args, rv.Index(i).Interface()) } case reflect.Map: for _, k := range rv.MapKeys() { args = append(args, k.Interface(), rv.MapIndex(k).Interface()) } case reflect.Ptr: if rv.Type().Elem().Kind() == reflect.Struct { if !rv.IsNil() { args = flattenStruct(args, rv.Elem()) } } else { args = append(args, v) } default: args = append(args, v) } return args } func flattenStruct(args Args, v reflect.Value) Args { ss, err := structSpecForType(v.Type()) if err != nil { panic(fmt.Errorf("redigo.AddFlat: %w", err)) } for _, fs := range ss.l { fv, err := fieldByIndexErr(v, fs.index) if err != nil { // Nil item ignore. continue } if fs.omitEmpty { var empty = false switch fv.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: empty = fv.Len() == 0 case reflect.Bool: empty = !fv.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: empty = fv.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: empty = fv.Uint() == 0 case reflect.Float32, reflect.Float64: empty = fv.Float() == 0 case reflect.Interface, reflect.Ptr: empty = fv.IsNil() } if empty { continue } } if arg, ok := fv.Interface().(Argument); ok { args = append(args, fs.name, arg.RedisArg()) } else if fv.Kind() == reflect.Ptr { if !fv.IsNil() { args = append(args, fs.name, fv.Elem().Interface()) } } else { args = append(args, fs.name, fv.Interface()) } } return args } redigo-1.9.2/redis/scan_test.go000066400000000000000000000422051456663410400164300ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "fmt" "math" "reflect" "strconv" "testing" "time" "github.com/gomodule/redigo/redis" "github.com/stretchr/testify/require" ) type durationScan struct { time.Duration `redis:"sd"` } func (t *durationScan) RedisScan(src interface{}) (err error) { if t == nil { return fmt.Errorf("nil pointer") } switch src := src.(type) { case string: t.Duration, err = time.ParseDuration(src) case []byte: t.Duration, err = time.ParseDuration(string(src)) case int64: t.Duration = time.Duration(src) default: err = fmt.Errorf("cannot convert from %T to %T", src, t) } return err } var scanConversionTests = []struct { src interface{} dest interface{} }{ {[]byte("-inf"), math.Inf(-1)}, {[]byte("+inf"), math.Inf(1)}, {[]byte("0"), float64(0)}, {[]byte("3.14159"), float64(3.14159)}, {[]byte("3.14"), float32(3.14)}, {[]byte("-100"), int(-100)}, {[]byte("101"), int(101)}, {int64(102), int(102)}, {[]byte("103"), uint(103)}, {int64(104), uint(104)}, {[]byte("105"), int8(105)}, {int64(106), int8(106)}, {[]byte("107"), uint8(107)}, {int64(108), uint8(108)}, {[]byte("0"), false}, {int64(0), false}, {[]byte("f"), false}, {[]byte("1"), true}, {int64(1), true}, {[]byte("t"), true}, {"hello", "hello"}, {[]byte("hello"), "hello"}, {[]byte("world"), []byte("world")}, {nil, ""}, {nil, []byte(nil)}, {[]interface{}{[]byte("b1")}, []interface{}{[]byte("b1")}}, {[]interface{}{[]byte("b2")}, []string{"b2"}}, {[]interface{}{[]byte("b3"), []byte("b4")}, []string{"b3", "b4"}}, {[]interface{}{[]byte("b5")}, [][]byte{[]byte("b5")}}, {[]interface{}{[]byte("1")}, []int{1}}, {[]interface{}{[]byte("1"), []byte("2")}, []int{1, 2}}, {[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}}, {[]interface{}{[]byte("1")}, []byte{1}}, {[]interface{}{[]byte("1")}, []bool{true}}, {[]interface{}{"s1"}, []interface{}{"s1"}}, {[]interface{}{"s2"}, [][]byte{[]byte("s2")}}, {[]interface{}{"s3", "s4"}, []string{"s3", "s4"}}, {[]interface{}{"s5"}, [][]byte{[]byte("s5")}}, {[]interface{}{"1"}, []int{1}}, {[]interface{}{"1", "2"}, []int{1, 2}}, {[]interface{}{"1", "2"}, []float64{1, 2}}, {[]interface{}{"1"}, []byte{1}}, {[]interface{}{"1"}, []bool{true}}, {[]interface{}{nil, "2"}, []interface{}{nil, "2"}}, {[]interface{}{nil, []byte("2")}, [][]byte{nil, []byte("2")}}, {[]interface{}{redis.Error("e1")}, []interface{}{redis.Error("e1")}}, {[]interface{}{redis.Error("e2")}, [][]byte{[]byte("e2")}}, {[]interface{}{redis.Error("e3")}, []string{"e3"}}, {"1m", durationScan{Duration: time.Minute}}, {[]byte("1m"), durationScan{Duration: time.Minute}}, {time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}}, {[]interface{}{[]byte("1m")}, []durationScan{{Duration: time.Minute}}}, {[]interface{}{[]byte("1m")}, []*durationScan{{Duration: time.Minute}}}, } func TestScanConversion(t *testing.T) { for _, tt := range scanConversionTests { values := []interface{}{tt.src} dest := reflect.New(reflect.TypeOf(tt.dest)) _, err := redis.Scan(values, dest.Interface()) if err != nil { t.Errorf("Scan(%v) returned error %v", tt, err) continue } if !reflect.DeepEqual(tt.dest, dest.Elem().Interface()) { t.Errorf("Scan(%v) returned %v, want %v", tt, dest.Elem().Interface(), tt.dest) } } } var scanConversionErrorTests = []struct { src interface{} dest interface{} }{ {[]byte("1234"), byte(0)}, {int64(1234), byte(0)}, {[]byte("-1"), byte(0)}, {int64(-1), byte(0)}, {[]byte("junk"), false}, {redis.Error("blah"), false}, {redis.Error("blah"), durationScan{Duration: time.Minute}}, {"invalid", durationScan{Duration: time.Minute}}, } func TestScanConversionError(t *testing.T) { for _, tt := range scanConversionErrorTests { values := []interface{}{tt.src} dest := reflect.New(reflect.TypeOf(tt.dest)) _, err := redis.Scan(values, dest.Interface()) if err == nil { t.Errorf("Scan(%v) did not return error", tt) } } } func ExampleScan() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() if err = c.Send("HMSET", "album:1", "title", "Red", "rating", 5); err != nil { fmt.Println(err) return } if err = c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1); err != nil { fmt.Println(err) return } if err = c.Send("HMSET", "album:3", "title", "Beat"); err != nil { fmt.Println(err) return } if err = c.Send("LPUSH", "albums", "1"); err != nil { fmt.Println(err) return } if err = c.Send("LPUSH", "albums", "2"); err != nil { fmt.Println(err) return } if err = c.Send("LPUSH", "albums", "3"); err != nil { fmt.Println(err) return } values, err := redis.Values(c.Do("SORT", "albums", "BY", "album:*->rating", "GET", "album:*->title", "GET", "album:*->rating")) if err != nil { fmt.Println(err) return } for len(values) > 0 { var title string rating := -1 // initialize to illegal value to detect nil. values, err = redis.Scan(values, &title, &rating) if err != nil { fmt.Println(err) return } if rating == -1 { fmt.Println(title, "not-rated") } else { fmt.Println(title, rating) } } // Output: // Beat not-rated // Earthbound 1 // Red 5 } type s0 struct { X int Y int `redis:"y"` Bt bool } type s1 struct { X int `redis:"-"` I int `redis:"i"` U uint `redis:"u"` S string `redis:"s"` P []byte `redis:"p"` B bool `redis:"b"` Bt bool Bf bool PtrB *bool s0 Sd durationScan `redis:"sd"` Sdp *durationScan `redis:"sdp"` } var ( boolTrue = true int5 = 5 ) var scanStructTests = []struct { name string reply []string expected interface{} }{ {"basic", []string{ "i", "-1234", "u", "5678", "s", "hello", "p", "world", "b", "t", "Bt", "1", "Bf", "0", "PtrB", "1", "X", "123", "y", "456", "sd", "1m", "sdp", "1m", }, &s1{ I: -1234, U: 5678, S: "hello", P: []byte("world"), B: true, Bt: true, Bf: false, PtrB: &boolTrue, s0: s0{X: 123, Y: 456}, Sd: durationScan{Duration: time.Minute}, Sdp: &durationScan{Duration: time.Minute}, }, }, {"absent values", []string{}, &s1{}, }, {"struct-anonymous-nil", []string{"edi", "2"}, &struct { Ed *Edp }{ Ed: Ed{EdI: 2}, }, }, {"struct-anonymous-multi-nil-early", []string{"edi", "2"}, &struct { Ed *Ed2 }{ Ed: Ed{EdI: 2}, }, }, {"struct-anonymous-multi-nil-late", []string{"edi", "2", "ed2i", "3", "edp2i", "4"}, &struct { Ed *Ed2 }{ Ed: Ed{EdI: 2}, Ed2: &Ed2{ Ed2I: 3, Edp2: &Edp2{ Edp2I: 4, }, }, }, }, } func TestScanStruct(t *testing.T) { for _, tt := range scanStructTests { t.Run(tt.name, func(t *testing.T) { reply := make([]interface{}, len(tt.reply)) for i, v := range tt.reply { reply[i] = []byte(v) } value := reflect.New(reflect.ValueOf(tt.expected).Type().Elem()).Interface() err := redis.ScanStruct(reply, value) require.NoError(t, err) require.Equal(t, tt.expected, value) }) } } func TestBadScanStructArgs(t *testing.T) { x := []interface{}{"A", "b"} test := func(v interface{}) { if err := redis.ScanStruct(x, v); err == nil { t.Errorf("Expect error for ScanStruct(%T, %T)", x, v) } } test(nil) var v0 *struct{} test(v0) var v1 int test(&v1) x = x[:1] v2 := struct{ A string }{} test(&v2) } type sliceScanner struct { Field string } func (ss *sliceScanner) RedisScan(s interface{}) error { v, ok := s.([]interface{}) if !ok { return fmt.Errorf("invalid type %T", s) } return redis.ScanStruct(v, ss) } var scanSliceTests = []struct { name string src []interface{} fieldNames []string ok bool dest interface{} }{ { "scanner", []interface{}{[]interface{}{[]byte("Field"), []byte("1")}}, nil, true, []*sliceScanner{{"1"}}, }, { "int", []interface{}{[]byte("1"), nil, []byte("-1")}, nil, true, []int{1, 0, -1}, }, { "uint", []interface{}{[]byte("1"), nil, []byte("2")}, nil, true, []uint{1, 0, 2}, }, { "uint-error", []interface{}{[]byte("-1")}, nil, false, []uint{1}, }, { "[]byte", []interface{}{[]byte("hello"), nil, []byte("world")}, nil, true, [][]byte{[]byte("hello"), nil, []byte("world")}, }, { "string", []interface{}{[]byte("hello"), nil, []byte("world")}, nil, true, []string{"hello", "", "world"}, }, { "struct", []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, nil, true, []struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, }, { "struct-error", []interface{}{[]byte("a1"), []byte("b1")}, nil, false, []struct{ A, B, C string }{{"a1", "b1", ""}}, }, { "struct-field-names", []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, []string{"A", "B"}, true, []struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}}, }, { "struct-no-fields", []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, nil, false, []struct{}{}, }, } func TestScanSlice(t *testing.T) { for _, tt := range scanSliceTests { t.Run(tt.name, func(t *testing.T) { typ := reflect.ValueOf(tt.dest).Type() dest := reflect.New(typ) err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...) if tt.ok != (err == nil) { t.Fatalf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err) } if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) { t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest) } }) } } func ExampleScanSlice() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() if err = c.Send("HMSET", "album:1", "title", "Red", "rating", 5); err != nil { fmt.Println(err) return } if err = c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1); err != nil { fmt.Println(err) return } if err = c.Send("HMSET", "album:3", "title", "Beat", "rating", 4); err != nil { fmt.Println(err) return } if err = c.Send("LPUSH", "albums", "1"); err != nil { fmt.Println(err) return } if err = c.Send("LPUSH", "albums", "2"); err != nil { fmt.Println(err) return } if err = c.Send("LPUSH", "albums", "3"); err != nil { fmt.Println(err) return } values, err := redis.Values(c.Do("SORT", "albums", "BY", "album:*->rating", "GET", "album:*->title", "GET", "album:*->rating")) if err != nil { fmt.Println(err) return } var albums []struct { Title string Rating int } if err := redis.ScanSlice(values, &albums); err != nil { fmt.Println(err) return } fmt.Printf("%v\n", albums) // Output: // [{Earthbound 1} {Beat 4} {Red 5}] } type Ed struct { EdI int `redis:"edi"` } type Edp struct { EdpI int `redis:"edpi"` } type Ed2 struct { Ed2I int `redis:"ed2i"` *Edp2 } type Edp2 struct { Edp2I int `redis:"edp2i"` *Edp } type Edpr1 struct { Edpr1I int `redis:"edpr1i"` *Edpr2 } type Edpr2 struct { Edpr2I int `redis:"edpr2i"` *Edpr1 } var argsTests = []struct { title string fn func() redis.Args expected redis.Args panics bool }{ {"struct-ptr", func() redis.Args { return redis.Args{}.AddFlat(&struct { I int `redis:"i"` U uint `redis:"u"` S string `redis:"s"` P []byte `redis:"p"` M map[string]string `redis:"m"` Bt bool Bf bool PtrB *bool PtrI *int PtrI2 *int }{ -1234, 5678, "hello", []byte("world"), map[string]string{"hello": "world"}, true, false, &boolTrue, nil, &int5, }) }, redis.Args{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "m", map[string]string{"hello": "world"}, "Bt", true, "Bf", false, "PtrB", true, "PtrI2", 5}, false, }, {"struct", func() redis.Args { return redis.Args{}.AddFlat(struct{ I int }{123}) }, redis.Args{"I", 123}, false, }, {"struct-with-RedisArg-direct", func() redis.Args { return redis.Args{}.AddFlat(struct{ T CustomTime }{CustomTime{Time: time.Unix(1573231058, 0)}}) }, redis.Args{"T", int64(1573231058)}, false, }, {"struct-with-RedisArg-direct-ptr", func() redis.Args { return redis.Args{}.AddFlat(struct{ T *CustomTime }{&CustomTime{Time: time.Unix(1573231058, 0)}}) }, redis.Args{"T", int64(1573231058)}, false, }, {"struct-with-RedisArg-ptr", func() redis.Args { return redis.Args{}.AddFlat(struct{ T *CustomTimePtr }{&CustomTimePtr{Time: time.Unix(1573231058, 0)}}) }, redis.Args{"T", int64(1573231058)}, false, }, {"slice", func() redis.Args { return redis.Args{}.Add(1).AddFlat([]string{"a", "b", "c"}).Add(2) }, redis.Args{1, "a", "b", "c", 2}, false, }, {"struct-omitempty", func() redis.Args { return redis.Args{}.AddFlat(&struct { Sdp *durationArg `redis:"Sdp,omitempty"` }{ nil, }) }, redis.Args{}, false, }, {"struct-anonymous", func() redis.Args { return redis.Args{}.AddFlat(struct { Ed *Edp }{ Ed{EdI: 2}, &Edp{EdpI: 3}, }) }, redis.Args{"edi", 2, "edpi", 3}, false, }, {"struct-anonymous-nil", func() redis.Args { return redis.Args{}.AddFlat(struct { Ed *Edp }{ Ed: Ed{EdI: 2}, }) }, redis.Args{"edi", 2}, false, }, {"struct-anonymous-multi-nil-early", func() redis.Args { return redis.Args{}.AddFlat(struct { Ed *Ed2 }{ Ed: Ed{EdI: 2}, }) }, redis.Args{"edi", 2}, false, }, {"struct-anonymous-multi-nil-late", func() redis.Args { return redis.Args{}.AddFlat(struct { Ed *Ed2 }{ Ed: Ed{EdI: 2}, Ed2: &Ed2{ Ed2I: 3, Edp2: &Edp2{ Edp2I: 4, }, }, }) }, redis.Args{"edi", 2, "ed2i", 3, "edp2i", 4}, false, }, {"struct-recursive-ptr", func() redis.Args { return redis.Args{}.AddFlat(struct { Edpr1 }{ Edpr1: Edpr1{ Edpr1I: 1, Edpr2: &Edpr2{ Edpr2I: 2, Edpr1: &Edpr1{ Edpr1I: 10, }, }, }, }) }, redis.Args{}, true, }, } func TestArgs(t *testing.T) { for _, tt := range argsTests { t.Run(tt.title, func(t *testing.T) { if tt.panics { require.Panics(t, func() { tt.fn() }) return } require.Equal(t, tt.expected, tt.fn()) }) } } type CustomTimePtr struct { time.Time } func (t *CustomTimePtr) RedisArg() interface{} { return t.Unix() } type CustomTime struct { time.Time } func (t CustomTime) RedisArg() interface{} { return t.Unix() } type InnerStruct struct { Foo int64 } func (f *InnerStruct) RedisScan(src interface{}) (err error) { switch s := src.(type) { case []byte: f.Foo, err = strconv.ParseInt(string(s), 10, 64) case string: f.Foo, err = strconv.ParseInt(s, 10, 64) default: return fmt.Errorf("invalid type %T", src) } return err } type OuterStruct struct { Inner *InnerStruct } func TestScanPtrRedisScan(t *testing.T) { tests := []struct { name string src []interface{} dest OuterStruct expected OuterStruct }{ { name: "value-to-nil", src: []interface{}{[]byte("1234"), nil}, dest: OuterStruct{&InnerStruct{}}, expected: OuterStruct{Inner: &InnerStruct{Foo: 1234}}, }, { name: "nil-to-nil", src: []interface{}{[]byte(nil), nil}, dest: OuterStruct{}, expected: OuterStruct{}, }, { name: "value-to-value", src: []interface{}{[]byte("1234"), nil}, dest: OuterStruct{Inner: &InnerStruct{Foo: 5678}}, expected: OuterStruct{Inner: &InnerStruct{Foo: 1234}}, }, { name: "nil-to-value", src: []interface{}{[]byte(nil), nil}, dest: OuterStruct{Inner: &InnerStruct{Foo: 1234}}, expected: OuterStruct{}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := redis.Scan(tc.src, &tc.dest.Inner) require.NoError(t, err) require.Equal(t, tc.expected, tc.dest) }) } } func ExampleArgs() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() var p1, p2 struct { Title string `redis:"title"` Author string `redis:"author"` Body string `redis:"body"` } p1.Title = "Example" p1.Author = "Gary" p1.Body = "Hello" if _, err := c.Do("HMSET", redis.Args{}.Add("id1").AddFlat(&p1)...); err != nil { fmt.Println(err) return } m := map[string]string{ "title": "Example2", "author": "Steve", "body": "Map", } if _, err := c.Do("HMSET", redis.Args{}.Add("id2").AddFlat(m)...); err != nil { fmt.Println(err) return } for _, id := range []string{"id1", "id2"} { v, err := redis.Values(c.Do("HGETALL", id)) if err != nil { fmt.Println(err) return } if err := redis.ScanStruct(v, &p2); err != nil { fmt.Println(err) return } fmt.Printf("%+v\n", p2) } // Output: // {Title:Example Author:Gary Body:Hello} // {Title:Example2 Author:Steve Body:Map} } redigo-1.9.2/redis/script.go000066400000000000000000000066521456663410400157570ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "context" "crypto/sha1" "encoding/hex" "io" "strings" ) // Script encapsulates the source, hash and key count for a Lua script. See // http://redis.io/commands/eval for information on scripts in Redis. type Script struct { keyCount int src string hash string } // NewScript returns a new script object. If keyCount is greater than or equal // to zero, then the count is automatically inserted in the EVAL command // argument list. If keyCount is less than zero, then the application supplies // the count as the first value in the keysAndArgs argument to the Do, Send and // SendHash methods. func NewScript(keyCount int, src string) *Script { h := sha1.New() io.WriteString(h, src) // nolint: errcheck return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))} } func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} { var args []interface{} if s.keyCount < 0 { args = make([]interface{}, 1+len(keysAndArgs)) args[0] = spec copy(args[1:], keysAndArgs) } else { args = make([]interface{}, 2+len(keysAndArgs)) args[0] = spec args[1] = s.keyCount copy(args[2:], keysAndArgs) } return args } // Hash returns the script hash. func (s *Script) Hash() string { return s.hash } func (s *Script) DoContext(ctx context.Context, c Conn, keysAndArgs ...interface{}) (interface{}, error) { cwt, ok := c.(ConnWithContext) if !ok { return nil, errContextNotSupported } v, err := cwt.DoContext(ctx, "EVALSHA", s.args(s.hash, keysAndArgs)...) if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") { v, err = cwt.DoContext(ctx, "EVAL", s.args(s.src, keysAndArgs)...) } return v, err } // Do evaluates the script. Under the covers, Do optimistically evaluates the // script using the EVALSHA command. If the command fails because the script is // not loaded, then Do evaluates the script using the EVAL command (thus // causing the script to load). func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) { v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...) if err != nil && strings.HasPrefix(err.Error(), "NOSCRIPT ") { v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...) } return v, err } // SendHash evaluates the script without waiting for the reply. The script is // evaluated with the EVALSHA command. The application must ensure that the // script is loaded by a previous call to Send, Do or Load methods. func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error { return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...) } // Send evaluates the script without waiting for the reply. func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error { return c.Send("EVAL", s.args(s.src, keysAndArgs)...) } // Load loads the script without evaluating it. func (s *Script) Load(c Conn) error { _, err := c.Do("SCRIPT", "LOAD", s.src) return err } redigo-1.9.2/redis/script_test.go000066400000000000000000000053461456663410400170150ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "fmt" "reflect" "testing" "time" "github.com/gomodule/redigo/redis" ) var ( // These variables are declared at package level to remove distracting // details from the examples. c redis.Conn reply interface{} err error ) func ExampleScript() { // Initialize a package-level variable with a script. var getScript = redis.NewScript(1, `return redis.call('get', KEYS[1])`) // In a function, use the script Do method to evaluate the script. The Do // method optimistically uses the EVALSHA command. If the script is not // loaded, then the Do method falls back to the EVAL command. reply, err = getScript.Do(c, "foo") } func TestScript(t *testing.T) { c, err := redis.DialDefaultServer() if err != nil { t.Fatalf("error connection to database, %v", err) } defer c.Close() // To test fall back in Do, we make script unique by adding comment with current time. script := fmt.Sprintf("--%d\nreturn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", time.Now().UnixNano()) s := redis.NewScript(2, script) reply := []interface{}{[]byte("key1"), []byte("key2"), []byte("arg1"), []byte("arg2")} v, err := s.Do(c, "key1", "key2", "arg1", "arg2") if err != nil { t.Errorf("s.Do(c, ...) returned %v", err) } if !reflect.DeepEqual(v, reply) { t.Errorf("s.Do(c, ..); = %v, want %v", v, reply) } err = s.Load(c) if err != nil { t.Errorf("s.Load(c) returned %v", err) } err = s.SendHash(c, "key1", "key2", "arg1", "arg2") if err != nil { t.Errorf("s.SendHash(c, ...) returned %v", err) } err = c.Flush() if err != nil { t.Errorf("c.Flush() returned %v", err) } v, err = c.Receive() if err != nil { t.Errorf("s.Recieve() returned %v", err) } if !reflect.DeepEqual(v, reply) { t.Errorf("s.SendHash(c, ..); c.Receive() = %v, want %v", v, reply) } err = s.Send(c, "key1", "key2", "arg1", "arg2") if err != nil { t.Errorf("s.Send(c, ...) returned %v", err) } err = c.Flush() if err != nil { t.Errorf("c.Flush() returned %v", err) } v, err = c.Receive() if err != nil { t.Errorf("s.Recieve() returned %v", err) } if !reflect.DeepEqual(v, reply) { t.Errorf("s.Send(c, ..); c.Receive() = %v, want %v", v, reply) } } redigo-1.9.2/redis/test_test.go000066400000000000000000000132431456663410400164630ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis import ( "bufio" "context" "errors" "flag" "fmt" "io" "io/ioutil" "os" "os/exec" "regexp" "strconv" "strings" "sync" "testing" "time" ) func SetNowFunc(f func() time.Time) { nowFunc = f } var ( ErrNegativeInt = errNegativeInt serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary") serverAddress = flag.String("redis-address", "127.0.0.1", "The address of the server") serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers") serverLogName = flag.String("redis-log", "", "Write Redis server logs to `filename`") serverLog = ioutil.Discard defaultServerMu sync.Mutex defaultServer *Server defaultServerErr error ) type Server struct { name string cmd *exec.Cmd done chan struct{} } type version struct { major int minor int patch int } func redisServerVersion() (*version, error) { out, err := exec.Command("redis-server", "--version").Output() if err != nil { return nil, fmt.Errorf("server version: %w", err) } ver := string(out) re := regexp.MustCompile(`v=(\d+)\.(\d+)\.(\d+)`) match := re.FindStringSubmatch(ver) if len(match) != 4 { return nil, fmt.Errorf("no server version found in %q", ver) } var v version if v.major, err = strconv.Atoi(match[1]); err != nil { return nil, fmt.Errorf("invalid major version %q", match[1]) } if v.minor, err = strconv.Atoi(match[2]); err != nil { return nil, fmt.Errorf("invalid minor version %q", match[2]) } if v.patch, err = strconv.Atoi(match[3]); err != nil { return nil, fmt.Errorf("invalid patch version %q", match[3]) } return &v, nil } func NewServer(name string, args ...string) (*Server, error) { version, err := redisServerVersion() if err != nil { return nil, err } if version.major >= 7 { args = append(args, "--enable-debug-command", "local") } s := &Server{ name: name, cmd: exec.Command(*serverPath, args...), done: make(chan struct{}), } r, err := s.cmd.StdoutPipe() if err != nil { return nil, err } err = s.cmd.Start() if err != nil { return nil, err } ready := make(chan error, 1) go s.watch(r, ready) select { case err = <-ready: case <-time.After(time.Second * 10): err = errors.New("timeout waiting for server to start") } if err != nil { s.Stop() return nil, err } return s, nil } func (s *Server) watch(r io.Reader, ready chan error) { fmt.Fprintf(serverLog, "%d START %s \n", s.cmd.Process.Pid, s.name) var listening bool var text string scn := bufio.NewScanner(r) for scn.Scan() { text = scn.Text() fmt.Fprintf(serverLog, "%s\n", text) if !listening { if strings.Contains(text, " * Ready to accept connections") || strings.Contains(text, " * The server is now ready to accept connections on port") { listening = true ready <- nil } } } if !listening { ready <- fmt.Errorf("server exited: %s", text) } if err := s.cmd.Wait(); err != nil { if listening { ready <- err } } fmt.Fprintf(serverLog, "%d STOP %s \n", s.cmd.Process.Pid, s.name) close(s.done) } func (s *Server) Stop() { s.cmd.Process.Signal(os.Interrupt) // nolint: errcheck <-s.done } // stopDefaultServer stops the server created by DialDefaultServer. func stopDefaultServer() { defaultServerMu.Lock() defer defaultServerMu.Unlock() if defaultServer != nil { defaultServer.Stop() defaultServer = nil } } // DefaultServerAddr starts the test server if not already started and returns // the address of that server. func DefaultServerAddr() (string, error) { defaultServerMu.Lock() defer defaultServerMu.Unlock() addr := fmt.Sprintf("%v:%d", *serverAddress, *serverBasePort) if defaultServer != nil || defaultServerErr != nil { return addr, defaultServerErr } defaultServer, defaultServerErr = NewServer( "default", "--port", strconv.Itoa(*serverBasePort), "--bind", *serverAddress, "--save", "", "--appendonly", "no") return addr, defaultServerErr } // DialDefaultServer starts the test server if not already started and dials a // connection to the server. func DialDefaultServer(options ...DialOption) (Conn, error) { return DialDefaultServerContext(context.Background(), options...) } // DialDefaultServerContext starts the test server if not already started and // dials a connection to the server with the given context. func DialDefaultServerContext(ctx context.Context, options ...DialOption) (Conn, error) { addr, err := DefaultServerAddr() if err != nil { return nil, err } c, err := DialContext(ctx, "tcp", addr, append([]DialOption{DialReadTimeout(1 * time.Second), DialWriteTimeout(1 * time.Second)}, options...)...) if err != nil { return nil, err } if _, err = DoContext(c, ctx, "FLUSHDB"); err != nil { return nil, err } return c, nil } func TestMain(m *testing.M) { os.Exit(func() int { flag.Parse() var f *os.File if *serverLogName != "" { var err error f, err = os.OpenFile(*serverLogName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) if err != nil { fmt.Fprintf(os.Stderr, "Error opening redis-log: %v\n", err) return 1 } defer f.Close() serverLog = f } defer stopDefaultServer() return m.Run() }()) } redigo-1.9.2/redis/zpop_example_test.go000066400000000000000000000047331456663410400202130ustar00rootroot00000000000000// Copyright 2013 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redis_test import ( "fmt" "github.com/gomodule/redigo/redis" ) // zpop pops a value from the ZSET key using WATCH/MULTI/EXEC commands. func zpop(c redis.Conn, key string) (result string, err error) { defer func() { // Return connection to normal state on error. if err != nil { c.Do("DISCARD") // nolint: errcheck } }() // Loop until transaction is successful. for { if _, err := c.Do("WATCH", key); err != nil { return "", err } members, err := redis.Strings(c.Do("ZRANGE", key, 0, 0)) if err != nil { return "", err } if len(members) != 1 { return "", redis.ErrNil } if err = c.Send("MULTI"); err != nil { return "", err } if err = c.Send("ZREM", key, members[0]); err != nil { return "", err } queued, err := c.Do("EXEC") if err != nil { return "", err } if queued != nil { result = members[0] break } } return result, nil } // zpopScript pops a value from a ZSET. var zpopScript = redis.NewScript(1, ` local r = redis.call('ZRANGE', KEYS[1], 0, 0) if r ~= nil then r = r[1] redis.call('ZREM', KEYS[1], r) end return r `) // This example implements ZPOP as described at // http://redis.io/topics/transactions using WATCH/MULTI/EXEC and scripting. func Example_zpop() { c, err := dial() if err != nil { fmt.Println(err) return } defer c.Close() // Add test data using a pipeline. for i, member := range []string{"red", "blue", "green"} { if err = c.Send("ZADD", "zset", i, member); err != nil { fmt.Println(err) return } } if _, err := c.Do(""); err != nil { fmt.Println(err) return } // Pop using WATCH/MULTI/EXEC v, err := zpop(c, "zset") if err != nil { fmt.Println(err) return } fmt.Println(v) // Pop using a script. v, err = redis.String(zpopScript.Do(c, "zset")) if err != nil { fmt.Println(err) return } fmt.Println(v) // Output: // red // blue } redigo-1.9.2/redisx/000077500000000000000000000000001456663410400143035ustar00rootroot00000000000000redigo-1.9.2/redisx/commandinfo.go000066400000000000000000000023601456663410400171250ustar00rootroot00000000000000// Copyright 2014 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redisx import ( "strings" ) type commandInfo struct { notMuxable bool } var commandInfos = map[string]commandInfo{ "WATCH": {notMuxable: true}, "UNWATCH": {notMuxable: true}, "MULTI": {notMuxable: true}, "EXEC": {notMuxable: true}, "DISCARD": {notMuxable: true}, "PSUBSCRIBE": {notMuxable: true}, "SUBSCRIBE": {notMuxable: true}, "MONITOR": {notMuxable: true}, } func init() { for n, ci := range commandInfos { commandInfos[strings.ToLower(n)] = ci } } func lookupCommandInfo(commandName string) commandInfo { if ci, ok := commandInfos[commandName]; ok { return ci } return commandInfos[strings.ToUpper(commandName)] } redigo-1.9.2/redisx/commandinfo_test.go000066400000000000000000000004171456663410400201650ustar00rootroot00000000000000package redisx import "testing" func TestLookupCommandInfo(t *testing.T) { for _, n := range []string{"watch", "WATCH", "wAtch"} { if lookupCommandInfo(n) == (commandInfo{}) { t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n) } } } redigo-1.9.2/redisx/connmux.go000066400000000000000000000057551456663410400163350ustar00rootroot00000000000000// Copyright 2014 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redisx import ( "errors" "sync" "github.com/gomodule/redigo/redis" ) // ConnMux multiplexes one or more connections to a single underlying // connection. The ConnMux connections do not support concurrency, commands // that associate server side state with the connection or commands that put // the connection in a special mode. type ConnMux struct { c redis.Conn sendMu sync.Mutex sendID uint recvMu sync.Mutex recvID uint recvWait map[uint]chan struct{} } func NewConnMux(c redis.Conn) *ConnMux { return &ConnMux{c: c, recvWait: make(map[uint]chan struct{})} } // Get gets a connection. The application must close the returned connection. func (p *ConnMux) Get() redis.Conn { c := &muxConn{p: p} c.ids = c.buf[:0] return c } // Close closes the underlying connection. func (p *ConnMux) Close() error { return p.c.Close() } type muxConn struct { p *ConnMux ids []uint buf [8]uint } func (c *muxConn) send(flush bool, cmd string, args ...interface{}) error { if lookupCommandInfo(cmd).notMuxable { return errors.New("command not supported by mux pool") } p := c.p p.sendMu.Lock() id := p.sendID c.ids = append(c.ids, id) p.sendID++ err := p.c.Send(cmd, args...) if flush { err = p.c.Flush() } p.sendMu.Unlock() return err } func (c *muxConn) Send(cmd string, args ...interface{}) error { return c.send(false, cmd, args...) } func (c *muxConn) Flush() error { p := c.p p.sendMu.Lock() err := p.c.Flush() p.sendMu.Unlock() return err } func (c *muxConn) Receive() (interface{}, error) { if len(c.ids) == 0 { return nil, errors.New("mux pool underflow") } id := c.ids[0] c.ids = c.ids[1:] if len(c.ids) == 0 { c.ids = c.buf[:0] } p := c.p p.recvMu.Lock() if p.recvID != id { ch := make(chan struct{}) p.recvWait[id] = ch p.recvMu.Unlock() <-ch p.recvMu.Lock() if p.recvID != id { panic("out of sync") } } v, err := p.c.Receive() id++ p.recvID = id ch, ok := p.recvWait[id] if ok { delete(p.recvWait, id) } p.recvMu.Unlock() if ok { ch <- struct{}{} } return v, err } func (c *muxConn) Close() error { var err error if len(c.ids) == 0 { return nil } c.Flush() for range c.ids { _, err = c.Receive() } return err } func (c *muxConn) Do(cmd string, args ...interface{}) (interface{}, error) { if err := c.send(true, cmd, args...); err != nil { return nil, err } return c.Receive() } func (c *muxConn) Err() error { return c.p.c.Err() } redigo-1.9.2/redisx/connmux_test.go000066400000000000000000000114011456663410400173550ustar00rootroot00000000000000// Copyright 2014 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package redisx_test import ( "net/textproto" "sync" "testing" "github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redisx" "github.com/stretchr/testify/require" ) func TestConnMux(t *testing.T) { c, err := redisx.DialTest() if err != nil { t.Fatalf("error connection to database, %v", err) } m := redisx.NewConnMux(c) defer m.Close() c1 := m.Get() c2 := m.Get() err = c1.Send("ECHO", "hello") require.NoError(t, err) err = c2.Send("ECHO", "world") require.NoError(t, err) c1.Flush() c2.Flush() s, err := redis.String(c1.Receive()) if err != nil { t.Fatal(err) } if s != "hello" { t.Fatalf("echo returned %q, want %q", s, "hello") } s, err = redis.String(c2.Receive()) if err != nil { t.Fatal(err) } if s != "world" { t.Fatalf("echo returned %q, want %q", s, "world") } c1.Close() c2.Close() } func TestConnMuxClose(t *testing.T) { c, err := redisx.DialTest() if err != nil { t.Fatalf("error connection to database, %v", err) } m := redisx.NewConnMux(c) defer m.Close() c1 := m.Get() c2 := m.Get() if err := c1.Send("ECHO", "hello"); err != nil { t.Fatal(err) } if err := c1.Close(); err != nil { t.Fatal(err) } if err := c2.Send("ECHO", "world"); err != nil { t.Fatal(err) } if err := c2.Flush(); err != nil { t.Fatal(err) } s, err := redis.String(c2.Receive()) if err != nil { t.Fatal(err) } if s != "world" { t.Fatalf("echo returned %q, want %q", s, "world") } c2.Close() } func BenchmarkConn(b *testing.B) { b.StopTimer() c, err := redisx.DialTest() if err != nil { b.Fatalf("error connection to database, %v", err) } defer c.Close() b.StartTimer() for i := 0; i < b.N; i++ { if _, err := c.Do("PING"); err != nil { b.Fatal(err) } } } func BenchmarkConnMux(b *testing.B) { b.StopTimer() c, err := redisx.DialTest() if err != nil { b.Fatalf("error connection to database, %v", err) } m := redisx.NewConnMux(c) defer m.Close() b.StartTimer() for i := 0; i < b.N; i++ { c := m.Get() if _, err := c.Do("PING"); err != nil { b.Fatal(err) } c.Close() } } func BenchmarkPool(b *testing.B) { b.StopTimer() p := redis.Pool{Dial: redisx.DialTest, MaxIdle: 1} defer p.Close() // Fill the pool. c := p.Get() if err := c.Err(); err != nil { b.Fatal(err) } c.Close() b.StartTimer() for i := 0; i < b.N; i++ { c := p.Get() if _, err := c.Do("PING"); err != nil { b.Fatal(err) } c.Close() } } const numConcurrent = 10 func BenchmarkConnMuxConcurrent(b *testing.B) { b.StopTimer() c, err := redisx.DialTest() if err != nil { b.Fatalf("error connection to database, %v", err) } defer c.Close() m := redisx.NewConnMux(c) var wg sync.WaitGroup wg.Add(numConcurrent) b.StartTimer() for i := 0; i < numConcurrent; i++ { go func() { defer wg.Done() for i := 0; i < b.N; i++ { c := m.Get() _, err := c.Do("PING") require.NoError(b, err) c.Close() } }() } wg.Wait() } func BenchmarkPoolConcurrent(b *testing.B) { b.StopTimer() p := redis.Pool{Dial: redisx.DialTest, MaxIdle: numConcurrent} defer p.Close() // Fill the pool. conns := make([]redis.Conn, numConcurrent) for i := range conns { c := p.Get() if err := c.Err(); err != nil { b.Fatal(err) } conns[i] = c } for _, c := range conns { c.Close() } var wg sync.WaitGroup wg.Add(numConcurrent) b.StartTimer() for i := 0; i < numConcurrent; i++ { go func() { defer wg.Done() for i := 0; i < b.N; i++ { c := p.Get() _, err := c.Do("PING") require.NoError(b, err) c.Close() } }() } wg.Wait() } func BenchmarkPipelineConcurrency(b *testing.B) { b.StopTimer() c, err := redisx.DialTest() if err != nil { b.Fatalf("error connection to database, %v", err) } defer c.Close() var wg sync.WaitGroup wg.Add(numConcurrent) var pipeline textproto.Pipeline b.StartTimer() for i := 0; i < numConcurrent; i++ { go func() { defer wg.Done() for i := 0; i < b.N; i++ { id := pipeline.Next() pipeline.StartRequest(id) err = c.Send("PING") require.NoError(b, err) c.Flush() pipeline.EndRequest(id) pipeline.StartResponse(id) _, err := c.Receive() require.NoError(b, err) pipeline.EndResponse(id) } }() } wg.Wait() } redigo-1.9.2/redisx/db_test.go000066400000000000000000000031771456663410400162660ustar00rootroot00000000000000// Copyright 2014 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Package redistest contains utilities for writing Redigo tests. package redisx import ( "errors" "time" "github.com/gomodule/redigo/redis" ) type testConn struct { redis.Conn } func (t testConn) Close() error { _, err := t.Conn.Do("SELECT", "9") if err != nil { return nil } _, err = t.Conn.Do("FLUSHDB") if err != nil { return err } return t.Conn.Close() } // Dial dials the local Redis server and selects database 9. To prevent // stomping on real data, DialTestDB fails if database 9 contains data. The // returned connection flushes database 9 on close. func DialTest() (redis.Conn, error) { c, err := redis.Dial("tcp", ":6379", redis.DialReadTimeout(time.Second), redis.DialWriteTimeout(time.Second), ) if err != nil { return nil, err } _, err = c.Do("SELECT", "9") if err != nil { c.Close() return nil, err } n, err := redis.Int(c.Do("DBSIZE")) if err != nil { c.Close() return nil, err } if n != 0 { c.Close() return nil, errors.New("database #9 is not empty, test can not continue") } return testConn{c}, nil } redigo-1.9.2/redisx/doc.go000066400000000000000000000013331456663410400153770ustar00rootroot00000000000000// Copyright 2012 Gary Burd // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Package redisx contains experimental features for Redigo. Features in this // package may be modified or deleted at any time. package redisx