From 7f8e3192cd941f008a3a2413ca0e9ff90c02fd88 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 27 Sep 2020 22:09:46 +0100 Subject: [PATCH] Allow common redis and leveldb connections (#12385) * Allow common redis and leveldb connections Prevents multiple reopening of redis and leveldb connections to the same place by sharing connections. Further allows for more configurable redis connection type using the redisURI and a leveldbURI scheme. Signed-off-by: Andrew Thornton * add unit-test Signed-off-by: Andrew Thornton * as per @lunny Signed-off-by: Andrew Thornton * add test Signed-off-by: Andrew Thornton * Update modules/cache/cache_redis.go * Update modules/queue/queue_disk.go * Update modules/cache/cache_redis.go * Update modules/cache/cache_redis.go * Update modules/queue/unique_queue_disk.go * Update modules/queue/queue_disk.go * Update modules/queue/unique_queue_disk.go * Update modules/session/redis.go Co-authored-by: techknowlogick Co-authored-by: Lauris BH --- custom/conf/app.example.ini | 4 +- .../doc/advanced/config-cheat-sheet.en-us.md | 16 +- go.mod | 3 +- go.sum | 7 + modules/cache/cache.go | 1 - .../redis.go => modules/cache/cache_redis.go | 68 +- modules/nosql/leveldb.go | 25 + modules/nosql/manager.go | 71 + modules/nosql/manager_leveldb.go | 151 ++ modules/nosql/manager_redis.go | 205 +++ modules/nosql/redis.go | 102 ++ modules/nosql/redis_test.go | 35 + modules/queue/queue_disk.go | 31 +- modules/queue/queue_redis.go | 29 +- modules/queue/unique_queue_disk.go | 31 +- modules/queue/unique_queue_redis.go | 2 +- .../redis => modules/session}/redis.go | 49 +- modules/session/virtual.go | 3 +- .../macaron/cache/redis/redis.goconvey | 1 - .../macaron/session/redis/redis.goconvey | 1 - vendor/github.com/go-redis/redis/.travis.yml | 19 - vendor/github.com/go-redis/redis/CHANGELOG.md | 25 - .../go-redis/redis/internal/error.go | 89 -- .../github.com/go-redis/redis/internal/log.go | 15 - .../go-redis/redis/internal/pool/conn.go | 93 -- .../redis/internal/pool/pool_single.go | 53 - .../go-redis/redis/internal/util.go | 29 - vendor/github.com/go-redis/redis/redis.go | 580 -------- .../go-redis/redis/{ => v7}/.gitignore | 0 .../go-redis/redis/v7/.golangci.yml | 15 + .../github.com/go-redis/redis/v7/.travis.yml | 22 + .../github.com/go-redis/redis/v7/CHANGELOG.md | 46 + .../go-redis/redis/{ => v7}/LICENSE | 0 .../go-redis/redis/{ => v7}/Makefile | 8 +- .../go-redis/redis/{ => v7}/README.md | 66 +- .../go-redis/redis/{ => v7}/cluster.go | 882 ++++++------ .../redis/{ => v7}/cluster_commands.go | 2 +- .../go-redis/redis/{ => v7}/command.go | 1236 +++++++++-------- .../go-redis/redis/{ => v7}/commands.go | 1208 ++++++++-------- .../github.com/go-redis/redis/{ => v7}/doc.go | 0 vendor/github.com/go-redis/redis/v7/error.go | 108 ++ vendor/github.com/go-redis/redis/v7/go.mod | 15 + vendor/github.com/go-redis/redis/v7/go.sum | 47 + .../internal/consistenthash/consistenthash.go | 0 .../{ => v7}/internal/hashtag/hashtag.go | 0 .../redis/{ => v7}/internal/internal.go | 0 .../go-redis/redis/v7/internal/log.go | 8 + .../go-redis/redis/{ => v7}/internal/once.go | 0 .../go-redis/redis/v7/internal/pool/conn.go | 118 ++ .../redis/{ => v7}/internal/pool/pool.go | 223 +-- .../redis/v7/internal/pool/pool_single.go | 208 +++ .../{ => v7}/internal/pool/pool_sticky.go | 21 +- .../redis/{ => v7}/internal/proto/reader.go | 52 +- .../redis/{ => v7}/internal/proto/scan.go | 2 +- .../redis/{ => v7}/internal/proto/writer.go | 12 +- .../go-redis/redis/v7/internal/util.go | 56 + .../redis/{ => v7}/internal/util/safe.go | 0 .../redis/{ => v7}/internal/util/strconv.go | 0 .../redis/{ => v7}/internal/util/unsafe.go | 0 .../go-redis/redis/{ => v7}/iterator.go | 10 +- .../go-redis/redis/{ => v7}/options.go | 55 +- .../go-redis/redis/{ => v7}/pipeline.go | 42 +- .../go-redis/redis/{ => v7}/pubsub.go | 296 ++-- vendor/github.com/go-redis/redis/v7/redis.go | 758 ++++++++++ .../go-redis/redis/{ => v7}/result.go | 74 +- .../go-redis/redis/{ => v7}/ring.go | 322 +++-- .../go-redis/redis/{ => v7}/script.go | 2 +- .../go-redis/redis/{ => v7}/sentinel.go | 262 +++- .../github.com/go-redis/redis/{ => v7}/tx.go | 91 +- .../go-redis/redis/{ => v7}/universal.go | 41 +- vendor/modules.txt | 19 +- 71 files changed, 4927 insertions(+), 3138 deletions(-) rename vendor/gitea.com/macaron/cache/redis/redis.go => modules/cache/cache_redis.go (63%) create mode 100644 modules/nosql/leveldb.go create mode 100644 modules/nosql/manager.go create mode 100644 modules/nosql/manager_leveldb.go create mode 100644 modules/nosql/manager_redis.go create mode 100644 modules/nosql/redis.go create mode 100644 modules/nosql/redis_test.go rename {vendor/gitea.com/macaron/session/redis => modules/session}/redis.go (82%) delete mode 100644 vendor/gitea.com/macaron/cache/redis/redis.goconvey delete mode 100644 vendor/gitea.com/macaron/session/redis/redis.goconvey delete mode 100644 vendor/github.com/go-redis/redis/.travis.yml delete mode 100644 vendor/github.com/go-redis/redis/CHANGELOG.md delete mode 100644 vendor/github.com/go-redis/redis/internal/error.go delete mode 100644 vendor/github.com/go-redis/redis/internal/log.go delete mode 100644 vendor/github.com/go-redis/redis/internal/pool/conn.go delete mode 100644 vendor/github.com/go-redis/redis/internal/pool/pool_single.go delete mode 100644 vendor/github.com/go-redis/redis/internal/util.go delete mode 100644 vendor/github.com/go-redis/redis/redis.go rename vendor/github.com/go-redis/redis/{ => v7}/.gitignore (100%) create mode 100644 vendor/github.com/go-redis/redis/v7/.golangci.yml create mode 100644 vendor/github.com/go-redis/redis/v7/.travis.yml create mode 100644 vendor/github.com/go-redis/redis/v7/CHANGELOG.md rename vendor/github.com/go-redis/redis/{ => v7}/LICENSE (100%) rename vendor/github.com/go-redis/redis/{ => v7}/Makefile (59%) rename vendor/github.com/go-redis/redis/{ => v7}/README.md (56%) rename vendor/github.com/go-redis/redis/{ => v7}/cluster.go (75%) rename vendor/github.com/go-redis/redis/{ => v7}/cluster_commands.go (95%) rename vendor/github.com/go-redis/redis/{ => v7}/command.go (66%) rename vendor/github.com/go-redis/redis/{ => v7}/commands.go (63%) rename vendor/github.com/go-redis/redis/{ => v7}/doc.go (100%) create mode 100644 vendor/github.com/go-redis/redis/v7/error.go create mode 100644 vendor/github.com/go-redis/redis/v7/go.mod create mode 100644 vendor/github.com/go-redis/redis/v7/go.sum rename vendor/github.com/go-redis/redis/{ => v7}/internal/consistenthash/consistenthash.go (100%) rename vendor/github.com/go-redis/redis/{ => v7}/internal/hashtag/hashtag.go (100%) rename vendor/github.com/go-redis/redis/{ => v7}/internal/internal.go (100%) create mode 100644 vendor/github.com/go-redis/redis/v7/internal/log.go rename vendor/github.com/go-redis/redis/{ => v7}/internal/once.go (100%) create mode 100644 vendor/github.com/go-redis/redis/v7/internal/pool/conn.go rename vendor/github.com/go-redis/redis/{ => v7}/internal/pool/pool.go (72%) create mode 100644 vendor/github.com/go-redis/redis/v7/internal/pool/pool_single.go rename vendor/github.com/go-redis/redis/{ => v7}/internal/pool/pool_sticky.go (76%) rename vendor/github.com/go-redis/redis/{ => v7}/internal/proto/reader.go (84%) rename vendor/github.com/go-redis/redis/{ => v7}/internal/proto/scan.go (98%) rename vendor/github.com/go-redis/redis/{ => v7}/internal/proto/writer.go (93%) create mode 100644 vendor/github.com/go-redis/redis/v7/internal/util.go rename vendor/github.com/go-redis/redis/{ => v7}/internal/util/safe.go (100%) rename vendor/github.com/go-redis/redis/{ => v7}/internal/util/strconv.go (100%) rename vendor/github.com/go-redis/redis/{ => v7}/internal/util/unsafe.go (100%) rename vendor/github.com/go-redis/redis/{ => v7}/iterator.go (91%) rename vendor/github.com/go-redis/redis/{ => v7}/options.go (76%) rename vendor/github.com/go-redis/redis/{ => v7}/pipeline.go (62%) rename vendor/github.com/go-redis/redis/{ => v7}/pubsub.go (61%) create mode 100644 vendor/github.com/go-redis/redis/v7/redis.go rename vendor/github.com/go-redis/redis/{ => v7}/result.go (71%) rename vendor/github.com/go-redis/redis/{ => v7}/ring.go (71%) rename vendor/github.com/go-redis/redis/{ => v7}/script.go (97%) rename vendor/github.com/go-redis/redis/{ => v7}/sentinel.go (57%) rename vendor/github.com/go-redis/redis/{ => v7}/tx.go (56%) rename vendor/github.com/go-redis/redis/{ => v7}/universal.go (78%) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index e67657d07..34a305c4a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -467,8 +467,10 @@ LENGTH = 20 BATCH_LENGTH = 20 ; Connection string for redis queues this will store the redis connection string. CONN_STR = "addrs=127.0.0.1:6379 db=0" -; Provide the suffix of the default redis queue name - specific queues can be overriden within in their [queue.name] sections. +; Provides the suffix of the default redis/disk queue name - specific queues can be overriden within in their [queue.name] sections. QUEUE_NAME = "_queue" +; Provides the suffix of the default redis/disk unique queue set name - specific queues can be overriden within in their [queue.name] sections. +SET_NAME = "_unique" ; If the queue cannot be created at startup - level queues may need a timeout at startup - wrap the queue: WRAP_IF_NECESSARY = true ; Attempt to create the wrapped queue at max diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index c63233fe1..883f09ff9 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -308,15 +308,13 @@ relation to port exhaustion. ## Queue (`queue` and `queue.*`) - `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel`, `channel`, `level`, `redis`, `dummy` -- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. `DATADIR` for inidividual queues can be set in `queue.name` sections but will default to `DATADIR/`**`name`**. +- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. `DATADIR` for individual queues can be set in `queue.name` sections but will default to `DATADIR/`**`name`**. - `LENGTH`: **20**: Maximal queue size before channel queues block - `BATCH_LENGTH`: **20**: Batch data before passing to the handler -- `CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Connection string for the redis queue type. -- `QUEUE_NAME`: **_queue**: The suffix for default redis queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overriden in the specific `queue.name` section. -- `SET_NAME`: **_unique**: The suffix that will added to the default redis -set name for unique queues. Individual queues will default to -**`name`**`QUEUE_NAME`_`SET_NAME`_ but can be overridden in the specific -`queue.name` section. +- `CONN_STR`: **redis://127.0.0.1:6379/0**: Connection string for the redis queue type. Options can be set using query params. Similarly LevelDB options can also be set using: **leveldb://relative/path?option=value** or **leveldb:///absolute/path?option=value** +- `QUEUE_NAME`: **_queue**: The suffix for default redis and disk queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overriden in the specific `queue.name` section. +- `SET_NAME`: **_unique**: The suffix that will be added to the default redis and disk queue `set` name for unique queues. Individual queues will default to + **`name`**`QUEUE_NAME`_`SET_NAME`_ but can be overridden in the specific `queue.name` section. - `WRAP_IF_NECESSARY`: **true**: Will wrap queues with a timeoutable queue if the selected queue is not ready to be created - (Only relevant for the level queue.) - `MAX_ATTEMPTS`: **10**: Maximum number of attempts to create the wrapped queue - `TIMEOUT`: **GRACEFUL_HAMMER_TIME + 30s**: Timeout the creation of the wrapped queue if it takes longer than this to create. @@ -459,7 +457,7 @@ set name for unique queues. Individual queues will default to - `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, or `memcache`. - `INTERVAL`: **60**: Garbage Collection interval (sec), for memory cache only. - `HOST`: **\**: Connection string for `redis` and `memcache`. - - Redis: `network=tcp,addr=127.0.0.1:6379,password=macaron,db=0,pool_size=100,idle_timeout=180` + - Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` - Memcache: `127.0.0.1:9090;127.0.0.1:9091` - `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to 0 disables caching. @@ -708,7 +706,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf - `QUEUE_TYPE`: **channel**: Task queue type, could be `channel` or `redis`. - `QUEUE_LENGTH`: **1000**: Task queue length, available only when `QUEUE_TYPE` is `channel`. -- `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Task queue connection string, available only when `QUEUE_TYPE` is `redis`. If redis needs a password, use `addrs=127.0.0.1:6379 password=123 db=0`. +- `QUEUE_CONN_STR`: **redis://127.0.0.1:6379/0**: Task queue connection string, available only when `QUEUE_TYPE` is `redis`. If redis needs a password, use `redis://123@127.0.0.1:6379/0`. ## Migrations (`migrations`) diff --git a/go.mod b/go.mod index 00a970c0b..ac417ac89 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/go-enry/go-enry/v2 v2.5.2 github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.1.0 - github.com/go-redis/redis v6.15.2+incompatible + github.com/go-redis/redis/v7 v7.4.0 github.com/go-sql-driver/mysql v1.5.0 github.com/go-swagger/go-swagger v0.25.0 github.com/go-testfixtures/testfixtures/v3 v3.4.0 @@ -88,6 +88,7 @@ require ( github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b // indirect github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd github.com/stretchr/testify v1.6.1 + github.com/syndtr/goleveldb v1.0.0 github.com/tecbot/gorocksdb v0.0.0-20181010114359-8752a9433481 // indirect github.com/tinylib/msgp v1.1.2 // indirect github.com/tstranex/u2f v1.0.0 diff --git a/go.sum b/go.sum index a9b6f6f01..7a6fa8aef 100644 --- a/go.sum +++ b/go.sum @@ -342,6 +342,8 @@ github.com/go-openapi/validate v0.19.10 h1:tG3SZ5DC5KF4cyt7nqLVcQXGj5A7mpaYkAcNP github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4= +github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -730,9 +732,13 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1014,6 +1020,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/modules/cache/cache.go b/modules/cache/cache.go index 859f4a4b4..60865d833 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -13,7 +13,6 @@ import ( mc "gitea.com/macaron/cache" _ "gitea.com/macaron/cache/memcache" // memcache plugin for cache - _ "gitea.com/macaron/cache/redis" ) var ( diff --git a/vendor/gitea.com/macaron/cache/redis/redis.go b/modules/cache/cache_redis.go similarity index 63% rename from vendor/gitea.com/macaron/cache/redis/redis.go rename to modules/cache/cache_redis.go index 892ee28bd..96e865a38 100644 --- a/vendor/gitea.com/macaron/cache/redis/redis.go +++ b/modules/cache/cache_redis.go @@ -1,35 +1,23 @@ -// Copyright 2013 Beego Authors -// Copyright 2014 The Macaron Authors -// -// 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. +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. package cache import ( "fmt" - "strings" "time" - "github.com/go-redis/redis" - "github.com/unknwon/com" - "gopkg.in/ini.v1" + "code.gitea.io/gitea/modules/nosql" "gitea.com/macaron/cache" + "github.com/go-redis/redis/v7" + "github.com/unknwon/com" ) // RedisCacher represents a redis cache adapter implementation. type RedisCacher struct { - c *redis.Client + c redis.UniversalClient prefix string hsetName string occupyMode bool @@ -112,7 +100,7 @@ func (c *RedisCacher) IsExist(key string) bool { // Flush deletes all cached data. func (c *RedisCacher) Flush() error { if c.occupyMode { - return c.c.FlushDb().Err() + return c.c.FlushDB().Err() } keys, err := c.c.HKeys(c.hsetName).Result() @@ -131,46 +119,20 @@ func (c *RedisCacher) StartAndGC(opts cache.Options) error { c.hsetName = "MacaronCache" c.occupyMode = opts.OccupyMode - cfg, err := ini.Load([]byte(strings.Replace(opts.AdapterConfig, ",", "\n", -1))) - if err != nil { - return err - } + uri := nosql.ToRedisURI(opts.AdapterConfig) - opt := &redis.Options{ - Network: "tcp", - } - for k, v := range cfg.Section("").KeysHash() { + c.c = nosql.GetManager().GetRedisClient(uri.String()) + + for k, v := range uri.Query() { switch k { - case "network": - opt.Network = v - case "addr": - opt.Addr = v - case "password": - opt.Password = v - case "db": - opt.DB = com.StrTo(v).MustInt() - case "pool_size": - opt.PoolSize = com.StrTo(v).MustInt() - case "idle_timeout": - opt.IdleTimeout, err = time.ParseDuration(v + "s") - if err != nil { - return fmt.Errorf("error parsing idle timeout: %v", err) - } case "hset_name": - c.hsetName = v + c.hsetName = v[0] case "prefix": - c.prefix = v - default: - return fmt.Errorf("session/redis: unsupported option '%s'", k) + c.prefix = v[0] } } - c.c = redis.NewClient(opt) - if err = c.c.Ping().Err(); err != nil { - return err - } - - return nil + return c.c.Ping().Err() } func init() { diff --git a/modules/nosql/leveldb.go b/modules/nosql/leveldb.go new file mode 100644 index 000000000..5da2291e0 --- /dev/null +++ b/modules/nosql/leveldb.go @@ -0,0 +1,25 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package nosql + +import "net/url" + +// ToLevelDBURI converts old style connections to a LevelDBURI +// +// A LevelDBURI matches the pattern: +// +// leveldb://path[?[option=value]*] +// +// We have previously just provided the path but this prevent other options +func ToLevelDBURI(connection string) *url.URL { + uri, err := url.Parse(connection) + if err == nil && uri.Scheme == "leveldb" { + return uri + } + uri, _ = url.Parse("leveldb://common") + uri.Host = "" + uri.Path = connection + return uri +} diff --git a/modules/nosql/manager.go b/modules/nosql/manager.go new file mode 100644 index 000000000..ad61d6d18 --- /dev/null +++ b/modules/nosql/manager.go @@ -0,0 +1,71 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package nosql + +import ( + "strconv" + "sync" + "time" + + "github.com/go-redis/redis/v7" + "github.com/syndtr/goleveldb/leveldb" +) + +var manager *Manager + +// Manager is the nosql connection manager +type Manager struct { + mutex sync.Mutex + + RedisConnections map[string]*redisClientHolder + LevelDBConnections map[string]*levelDBHolder +} + +type redisClientHolder struct { + redis.UniversalClient + name []string + count int64 +} + +func (r *redisClientHolder) Close() error { + return manager.CloseRedisClient(r.name[0]) +} + +type levelDBHolder struct { + name []string + count int64 + db *leveldb.DB +} + +func init() { + _ = GetManager() +} + +// GetManager returns a Manager and initializes one as singleton is there's none yet +func GetManager() *Manager { + if manager == nil { + manager = &Manager{ + RedisConnections: make(map[string]*redisClientHolder), + LevelDBConnections: make(map[string]*levelDBHolder), + } + } + return manager +} + +func valToTimeDuration(vs []string) (result time.Duration) { + var err error + for _, v := range vs { + result, err = time.ParseDuration(v) + if err != nil { + var val int + val, err = strconv.Atoi(v) + result = time.Duration(val) + } + if err == nil { + return + } + } + return +} diff --git a/modules/nosql/manager_leveldb.go b/modules/nosql/manager_leveldb.go new file mode 100644 index 000000000..769d5002d --- /dev/null +++ b/modules/nosql/manager_leveldb.go @@ -0,0 +1,151 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package nosql + +import ( + "path" + "strconv" + "strings" + + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/errors" + "github.com/syndtr/goleveldb/leveldb/opt" +) + +// CloseLevelDB closes a levelDB +func (m *Manager) CloseLevelDB(connection string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + db, ok := m.LevelDBConnections[connection] + if !ok { + connection = ToLevelDBURI(connection).String() + db, ok = m.LevelDBConnections[connection] + } + if !ok { + return nil + } + + db.count-- + if db.count > 0 { + return nil + } + + for _, name := range db.name { + delete(m.LevelDBConnections, name) + } + return db.db.Close() +} + +// GetLevelDB gets a levelDB for a particular connection +func (m *Manager) GetLevelDB(connection string) (*leveldb.DB, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + db, ok := m.LevelDBConnections[connection] + if ok { + db.count++ + + return db.db, nil + } + dataDir := connection + uri := ToLevelDBURI(connection) + db = &levelDBHolder{ + name: []string{connection, uri.String()}, + } + + dataDir = path.Join(uri.Host, uri.Path) + opts := &opt.Options{} + for k, v := range uri.Query() { + switch replacer.Replace(strings.ToLower(k)) { + case "blockcachecapacity": + opts.BlockCacheCapacity, _ = strconv.Atoi(v[0]) + case "blockcacheevictremoved": + opts.BlockCacheEvictRemoved, _ = strconv.ParseBool(v[0]) + case "blockrestartinterval": + opts.BlockRestartInterval, _ = strconv.Atoi(v[0]) + case "blocksize": + opts.BlockSize, _ = strconv.Atoi(v[0]) + case "compactionexpandlimitfactor": + opts.CompactionExpandLimitFactor, _ = strconv.Atoi(v[0]) + case "compactiongpoverlapsfactor": + opts.CompactionGPOverlapsFactor, _ = strconv.Atoi(v[0]) + case "compactionl0trigger": + opts.CompactionL0Trigger, _ = strconv.Atoi(v[0]) + case "compactionsourcelimitfactor": + opts.CompactionSourceLimitFactor, _ = strconv.Atoi(v[0]) + case "compactiontablesize": + opts.CompactionTableSize, _ = strconv.Atoi(v[0]) + case "compactiontablesizemultiplier": + opts.CompactionTableSizeMultiplier, _ = strconv.ParseFloat(v[0], 64) + case "compactiontablesizemultiplierperlevel": + for _, val := range v { + f, _ := strconv.ParseFloat(val, 64) + opts.CompactionTableSizeMultiplierPerLevel = append(opts.CompactionTableSizeMultiplierPerLevel, f) + } + case "compactiontotalsize": + opts.CompactionTotalSize, _ = strconv.Atoi(v[0]) + case "compactiontotalsizemultiplier": + opts.CompactionTotalSizeMultiplier, _ = strconv.ParseFloat(v[0], 64) + case "compactiontotalsizemultiplierperlevel": + for _, val := range v { + f, _ := strconv.ParseFloat(val, 64) + opts.CompactionTotalSizeMultiplierPerLevel = append(opts.CompactionTotalSizeMultiplierPerLevel, f) + } + case "compression": + val, _ := strconv.Atoi(v[0]) + opts.Compression = opt.Compression(val) + case "disablebufferpool": + opts.DisableBufferPool, _ = strconv.ParseBool(v[0]) + case "disableblockcache": + opts.DisableBlockCache, _ = strconv.ParseBool(v[0]) + case "disablecompactionbackoff": + opts.DisableCompactionBackoff, _ = strconv.ParseBool(v[0]) + case "disablelargebatchtransaction": + opts.DisableLargeBatchTransaction, _ = strconv.ParseBool(v[0]) + case "errorifexist": + opts.ErrorIfExist, _ = strconv.ParseBool(v[0]) + case "errorifmissing": + opts.ErrorIfMissing, _ = strconv.ParseBool(v[0]) + case "iteratorsamplingrate": + opts.IteratorSamplingRate, _ = strconv.Atoi(v[0]) + case "nosync": + opts.NoSync, _ = strconv.ParseBool(v[0]) + case "nowritemerge": + opts.NoWriteMerge, _ = strconv.ParseBool(v[0]) + case "openfilescachecapacity": + opts.OpenFilesCacheCapacity, _ = strconv.Atoi(v[0]) + case "readonly": + opts.ReadOnly, _ = strconv.ParseBool(v[0]) + case "strict": + val, _ := strconv.Atoi(v[0]) + opts.Strict = opt.Strict(val) + case "writebuffer": + opts.WriteBuffer, _ = strconv.Atoi(v[0]) + case "writel0pausetrigger": + opts.WriteL0PauseTrigger, _ = strconv.Atoi(v[0]) + case "writel0slowdowntrigger": + opts.WriteL0SlowdownTrigger, _ = strconv.Atoi(v[0]) + case "clientname": + db.name = append(db.name, v[0]) + } + } + + var err error + db.db, err = leveldb.OpenFile(dataDir, opts) + if err != nil { + if !errors.IsCorrupted(err) { + return nil, err + } + db.db, err = leveldb.RecoverFile(dataDir, opts) + if err != nil { + return nil, err + } + } + + for _, name := range db.name { + m.LevelDBConnections[name] = db + } + db.count++ + return db.db, nil +} diff --git a/modules/nosql/manager_redis.go b/modules/nosql/manager_redis.go new file mode 100644 index 000000000..7792a9011 --- /dev/null +++ b/modules/nosql/manager_redis.go @@ -0,0 +1,205 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package nosql + +import ( + "crypto/tls" + "path" + "strconv" + "strings" + + "github.com/go-redis/redis/v7" +) + +var replacer = strings.NewReplacer("_", "", "-", "") + +// CloseRedisClient closes a redis client +func (m *Manager) CloseRedisClient(connection string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + client, ok := m.RedisConnections[connection] + if !ok { + connection = ToRedisURI(connection).String() + client, ok = m.RedisConnections[connection] + } + if !ok { + return nil + } + + client.count-- + if client.count > 0 { + return nil + } + + for _, name := range client.name { + delete(m.RedisConnections, name) + } + return client.UniversalClient.Close() +} + +// GetRedisClient gets a redis client for a particular connection +func (m *Manager) GetRedisClient(connection string) redis.UniversalClient { + m.mutex.Lock() + defer m.mutex.Unlock() + client, ok := m.RedisConnections[connection] + if ok { + client.count++ + return client + } + + uri := ToRedisURI(connection) + client, ok = m.RedisConnections[uri.String()] + if ok { + client.count++ + return client + } + client = &redisClientHolder{ + name: []string{connection, uri.String()}, + } + + opts := &redis.UniversalOptions{} + tlsConfig := &tls.Config{} + + // Handle username/password + if password, ok := uri.User.Password(); ok { + opts.Password = password + // Username does not appear to be handled by redis.Options + opts.Username = uri.User.Username() + } else if uri.User.Username() != "" { + // assume this is the password + opts.Password = uri.User.Username() + } + + // Now handle the uri query sets + for k, v := range uri.Query() { + switch replacer.Replace(strings.ToLower(k)) { + case "addr": + opts.Addrs = append(opts.Addrs, v...) + case "addrs": + opts.Addrs = append(opts.Addrs, strings.Split(v[0], ",")...) + case "username": + opts.Username = v[0] + case "password": + opts.Password = v[0] + case "database": + fallthrough + case "db": + opts.DB, _ = strconv.Atoi(v[0]) + case "maxretries": + opts.MaxRetries, _ = strconv.Atoi(v[0]) + case "minretrybackoff": + opts.MinRetryBackoff = valToTimeDuration(v) + case "maxretrybackoff": + opts.MaxRetryBackoff = valToTimeDuration(v) + case "timeout": + timeout := valToTimeDuration(v) + if timeout != 0 { + if opts.DialTimeout == 0 { + opts.DialTimeout = timeout + } + if opts.ReadTimeout == 0 { + opts.ReadTimeout = timeout + } + } + case "dialtimeout": + opts.DialTimeout = valToTimeDuration(v) + case "readtimeout": + opts.ReadTimeout = valToTimeDuration(v) + case "writetimeout": + opts.WriteTimeout = valToTimeDuration(v) + case "poolsize": + opts.PoolSize, _ = strconv.Atoi(v[0]) + case "minidleconns": + opts.MinIdleConns, _ = strconv.Atoi(v[0]) + case "pooltimeout": + opts.PoolTimeout = valToTimeDuration(v) + case "idletimeout": + opts.IdleTimeout = valToTimeDuration(v) + case "idlecheckfrequency": + opts.IdleCheckFrequency = valToTimeDuration(v) + case "maxredirects": + opts.MaxRedirects, _ = strconv.Atoi(v[0]) + case "readonly": + opts.ReadOnly, _ = strconv.ParseBool(v[0]) + case "routebylatency": + opts.RouteByLatency, _ = strconv.ParseBool(v[0]) + case "routerandomly": + opts.RouteRandomly, _ = strconv.ParseBool(v[0]) + case "sentinelmasterid": + fallthrough + case "mastername": + opts.MasterName = v[0] + case "skipverify": + fallthrough + case "insecureskipverify": + insecureSkipVerify, _ := strconv.ParseBool(v[0]) + tlsConfig.InsecureSkipVerify = insecureSkipVerify + case "clientname": + client.name = append(client.name, v[0]) + } + } + + switch uri.Scheme { + case "redis+sentinels": + fallthrough + case "rediss+sentinel": + opts.TLSConfig = tlsConfig + fallthrough + case "redis+sentinel": + if uri.Host != "" { + opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) + } + if uri.Path != "" { + if db, err := strconv.Atoi(uri.Path); err == nil { + opts.DB = db + } + } + + client.UniversalClient = redis.NewFailoverClient(opts.Failover()) + case "redis+clusters": + fallthrough + case "rediss+cluster": + opts.TLSConfig = tlsConfig + fallthrough + case "redis+cluster": + if uri.Host != "" { + opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) + } + if uri.Path != "" { + if db, err := strconv.Atoi(uri.Path); err == nil { + opts.DB = db + } + } + client.UniversalClient = redis.NewClusterClient(opts.Cluster()) + case "redis+socket": + simpleOpts := opts.Simple() + simpleOpts.Network = "unix" + simpleOpts.Addr = path.Join(uri.Host, uri.Path) + client.UniversalClient = redis.NewClient(simpleOpts) + case "rediss": + opts.TLSConfig = tlsConfig + fallthrough + case "redis": + if uri.Host != "" { + opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...) + } + if uri.Path != "" { + if db, err := strconv.Atoi(uri.Path); err == nil { + opts.DB = db + } + } + client.UniversalClient = redis.NewClient(opts.Simple()) + default: + return nil + } + + for _, name := range client.name { + m.RedisConnections[name] = client + } + + client.count++ + + return client +} diff --git a/modules/nosql/redis.go b/modules/nosql/redis.go new file mode 100644 index 000000000..528f5fc80 --- /dev/null +++ b/modules/nosql/redis.go @@ -0,0 +1,102 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package nosql + +import ( + "net/url" + "strconv" + "strings" +) + +// The file contains common redis connection functions + +// ToRedisURI converts old style connections to a RedisURI +// +// A RedisURI matches the pattern: +// +// redis://[username:password@]host[:port][/database][?[option=value]*] +// rediss://[username:password@]host[:port][/database][?[option=value]*] +// redis+socket://[username:password@]path[/database][?[option=value]*] +// redis+sentinel://[password@]host1 [: port1][, host2 [:port2]][, hostN [:portN]][/ database][?[option=value]*] +// redis+cluster://[password@]host1 [: port1][, host2 [:port2]][, hostN [:portN]][/ database][?[option=value]*] +// +// We have previously used a URI like: +// addrs=127.0.0.1:6379 db=0 +// network=tcp,addr=127.0.0.1:6379,password=macaron,db=0,pool_size=100,idle_timeout=180 +// +// We need to convert this old style to the new style +func ToRedisURI(connection string) *url.URL { + uri, err := url.Parse(connection) + if err == nil && strings.HasPrefix(uri.Scheme, "redis") { + // OK we're going to assume that this is a reasonable redis URI + return uri + } + + // Let's set a nice default + uri, _ = url.Parse("redis://127.0.0.1:6379/0") + network := "tcp" + query := uri.Query() + + // OK so there are two types: Space delimited and Comma delimited + // Let's assume that we have a space delimited string - as this is the most common + fields := strings.Fields(connection) + if len(fields) == 1 { + // It's a comma delimited string, then... + fields = strings.Split(connection, ",") + + } + for _, f := range fields { + items := strings.SplitN(f, "=", 2) + if len(items) < 2 { + continue + } + switch strings.ToLower(items[0]) { + case "network": + if items[1] == "unix" { + uri.Scheme = "redis+socket" + } + network = items[1] + case "addrs": + uri.Host = items[1] + // now we need to handle the clustering + if strings.Contains(items[1], ",") && network == "tcp" { + uri.Scheme = "redis+cluster" + } + case "addr": + uri.Host = items[1] + case "password": + uri.User = url.UserPassword(uri.User.Username(), items[1]) + case "username": + password, set := uri.User.Password() + if !set { + uri.User = url.User(items[1]) + } else { + uri.User = url.UserPassword(items[1], password) + } + case "db": + uri.Path = "/" + items[1] + case "idle_timeout": + _, err := strconv.Atoi(items[1]) + if err == nil { + query.Add("idle_timeout", items[1]+"s") + } else { + query.Add("idle_timeout", items[1]) + } + default: + // Other options become query params + query.Add(items[0], items[1]) + } + } + + // Finally we need to fix up the Host if we have a unix port + if uri.Scheme == "redis+socket" { + query.Set("db", uri.Path) + uri.Path = uri.Host + uri.Host = "" + } + uri.RawQuery = query.Encode() + + return uri +} diff --git a/modules/nosql/redis_test.go b/modules/nosql/redis_test.go new file mode 100644 index 000000000..c70d236bd --- /dev/null +++ b/modules/nosql/redis_test.go @@ -0,0 +1,35 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package nosql + +import ( + "testing" +) + +func TestToRedisURI(t *testing.T) { + tests := []struct { + name string + connection string + want string + }{ + { + name: "old_default", + connection: "addrs=127.0.0.1:6379 db=0", + want: "redis://127.0.0.1:6379/0", + }, + { + name: "old_macaron_session_default", + connection: "network=tcp,addr=127.0.0.1:6379,password=macaron,db=0,pool_size=100,idle_timeout=180", + want: "redis://:macaron@127.0.0.1:6379/0?idle_timeout=180s&pool_size=100", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ToRedisURI(tt.connection); got == nil || got.String() != tt.want { + t.Errorf(`ToRedisURI(%q) = %s, want %s`, tt.connection, got.String(), tt.want) + } + }) + } +} diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index ff0876488..88b8c414c 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -5,6 +5,8 @@ package queue import ( + "code.gitea.io/gitea/modules/nosql" + "gitea.com/lunny/levelqueue" ) @@ -14,7 +16,9 @@ const LevelQueueType Type = "level" // LevelQueueConfiguration is the configuration for a LevelQueue type LevelQueueConfiguration struct { ByteFIFOQueueConfiguration - DataDir string + DataDir string + ConnectionString string + QueueName string } // LevelQueue implements a disk library queue @@ -30,7 +34,11 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) } config := configInterface.(LevelQueueConfiguration) - byteFIFO, err := NewLevelQueueByteFIFO(config.DataDir) + if len(config.ConnectionString) == 0 { + config.ConnectionString = config.DataDir + } + + byteFIFO, err := NewLevelQueueByteFIFO(config.ConnectionString, config.QueueName) if err != nil { return nil, err } @@ -51,18 +59,25 @@ var _ (ByteFIFO) = &LevelQueueByteFIFO{} // LevelQueueByteFIFO represents a ByteFIFO formed from a LevelQueue type LevelQueueByteFIFO struct { - internal *levelqueue.Queue + internal *levelqueue.Queue + connection string } // NewLevelQueueByteFIFO creates a ByteFIFO formed from a LevelQueue -func NewLevelQueueByteFIFO(dataDir string) (*LevelQueueByteFIFO, error) { - internal, err := levelqueue.Open(dataDir) +func NewLevelQueueByteFIFO(connection, prefix string) (*LevelQueueByteFIFO, error) { + db, err := nosql.GetManager().GetLevelDB(connection) + if err != nil { + return nil, err + } + + internal, err := levelqueue.NewQueue(db, []byte(prefix), false) if err != nil { return nil, err } return &LevelQueueByteFIFO{ - internal: internal, + connection: connection, + internal: internal, }, nil } @@ -87,7 +102,9 @@ func (fifo *LevelQueueByteFIFO) Pop() ([]byte, error) { // Close this fifo func (fifo *LevelQueueByteFIFO) Close() error { - return fifo.internal.Close() + err := fifo.internal.Close() + _ = nosql.GetManager().CloseLevelDB(fifo.connection) + return err } // Len returns the length of the fifo diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 4e05ddd17..04e7b5d25 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -5,12 +5,10 @@ package queue import ( - "errors" - "strings" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/nosql" - "github.com/go-redis/redis" + "github.com/go-redis/redis/v7" ) // RedisQueueType is the type for redis queue @@ -75,11 +73,8 @@ type RedisByteFIFO struct { // RedisByteFIFOConfiguration is the configuration for the RedisByteFIFO type RedisByteFIFOConfiguration struct { - Network string - Addresses string - Password string - DBIndex int - QueueName string + ConnectionString string + QueueName string } // NewRedisByteFIFO creates a ByteFIFO formed from a redisClient @@ -87,21 +82,7 @@ func NewRedisByteFIFO(config RedisByteFIFOConfiguration) (*RedisByteFIFO, error) fifo := &RedisByteFIFO{ queueName: config.QueueName, } - dbs := strings.Split(config.Addresses, ",") - if len(dbs) == 0 { - return nil, errors.New("no redis host specified") - } else if len(dbs) == 1 { - fifo.client = redis.NewClient(&redis.Options{ - Network: config.Network, - Addr: strings.TrimSpace(dbs[0]), // use default Addr - Password: config.Password, // no password set - DB: config.DBIndex, // use default DB - }) - } else { - fifo.client = redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: dbs, - }) - } + fifo.client = nosql.GetManager().GetRedisClient(config.ConnectionString) if err := fifo.client.Ping().Err(); err != nil { return nil, err } diff --git a/modules/queue/unique_queue_disk.go b/modules/queue/unique_queue_disk.go index bfe7aeed8..dd6ac1a53 100644 --- a/modules/queue/unique_queue_disk.go +++ b/modules/queue/unique_queue_disk.go @@ -5,6 +5,8 @@ package queue import ( + "code.gitea.io/gitea/modules/nosql" + "gitea.com/lunny/levelqueue" ) @@ -14,7 +16,9 @@ const LevelUniqueQueueType Type = "unique-level" // LevelUniqueQueueConfiguration is the configuration for a LevelUniqueQueue type LevelUniqueQueueConfiguration struct { ByteFIFOQueueConfiguration - DataDir string + DataDir string + ConnectionString string + QueueName string } // LevelUniqueQueue implements a disk library queue @@ -34,7 +38,11 @@ func NewLevelUniqueQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, } config := configInterface.(LevelUniqueQueueConfiguration) - byteFIFO, err := NewLevelUniqueQueueByteFIFO(config.DataDir) + if len(config.ConnectionString) == 0 { + config.ConnectionString = config.DataDir + } + + byteFIFO, err := NewLevelUniqueQueueByteFIFO(config.ConnectionString, config.QueueName) if err != nil { return nil, err } @@ -55,18 +63,25 @@ var _ (UniqueByteFIFO) = &LevelUniqueQueueByteFIFO{} // LevelUniqueQueueByteFIFO represents a ByteFIFO formed from a LevelUniqueQueue type LevelUniqueQueueByteFIFO struct { - internal *levelqueue.UniqueQueue + internal *levelqueue.UniqueQueue + connection string } // NewLevelUniqueQueueByteFIFO creates a new ByteFIFO formed from a LevelUniqueQueue -func NewLevelUniqueQueueByteFIFO(dataDir string) (*LevelUniqueQueueByteFIFO, error) { - internal, err := levelqueue.OpenUnique(dataDir) +func NewLevelUniqueQueueByteFIFO(connection, prefix string) (*LevelUniqueQueueByteFIFO, error) { + db, err := nosql.GetManager().GetLevelDB(connection) + if err != nil { + return nil, err + } + + internal, err := levelqueue.NewUniqueQueue(db, []byte(prefix), []byte(prefix+"-unique"), false) if err != nil { return nil, err } return &LevelUniqueQueueByteFIFO{ - internal: internal, + connection: connection, + internal: internal, }, nil } @@ -96,7 +111,9 @@ func (fifo *LevelUniqueQueueByteFIFO) Has(data []byte) (bool, error) { // Close this fifo func (fifo *LevelUniqueQueueByteFIFO) Close() error { - return fifo.internal.Close() + err := fifo.internal.Close() + _ = nosql.GetManager().CloseLevelDB(fifo.connection) + return err } func init() { diff --git a/modules/queue/unique_queue_redis.go b/modules/queue/unique_queue_redis.go index 940436907..67efc66bc 100644 --- a/modules/queue/unique_queue_redis.go +++ b/modules/queue/unique_queue_redis.go @@ -4,7 +4,7 @@ package queue -import "github.com/go-redis/redis" +import "github.com/go-redis/redis/v7" // RedisUniqueQueueType is the type for redis queue const RedisUniqueQueueType Type = "unique-redis" diff --git a/vendor/gitea.com/macaron/session/redis/redis.go b/modules/session/redis.go similarity index 82% rename from vendor/gitea.com/macaron/session/redis/redis.go rename to modules/session/redis.go index 5f242d6b3..c88ebd576 100644 --- a/vendor/gitea.com/macaron/session/redis/redis.go +++ b/modules/session/redis.go @@ -1,5 +1,6 @@ // Copyright 2013 Beego Authors // Copyright 2014 The Macaron Authors +// Copyright 2020 The Gitea Authors. All rights reserved. // // 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 @@ -17,19 +18,18 @@ package session import ( "fmt" - "strings" "sync" "time" + "code.gitea.io/gitea/modules/nosql" + "gitea.com/macaron/session" - "github.com/go-redis/redis" - "github.com/unknwon/com" - "gopkg.in/ini.v1" + "github.com/go-redis/redis/v7" ) // RedisStore represents a redis session store implementation. type RedisStore struct { - c *redis.Client + c redis.UniversalClient prefix, sid string duration time.Duration lock sync.RWMutex @@ -37,7 +37,7 @@ type RedisStore struct { } // NewRedisStore creates and returns a redis session store. -func NewRedisStore(c *redis.Client, prefix, sid string, dur time.Duration, kv map[interface{}]interface{}) *RedisStore { +func NewRedisStore(c redis.UniversalClient, prefix, sid string, dur time.Duration, kv map[interface{}]interface{}) *RedisStore { return &RedisStore{ c: c, prefix: prefix, @@ -104,7 +104,7 @@ func (s *RedisStore) Flush() error { // RedisProvider represents a redis session provider implementation. type RedisProvider struct { - c *redis.Client + c redis.UniversalClient duration time.Duration prefix string } @@ -117,39 +117,16 @@ func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) { return err } - cfg, err := ini.Load([]byte(strings.Replace(configs, ",", "\n", -1))) - if err != nil { - return err - } + uri := nosql.ToRedisURI(configs) - opt := &redis.Options{ - Network: "tcp", - } - for k, v := range cfg.Section("").KeysHash() { + for k, v := range uri.Query() { switch k { - case "network": - opt.Network = v - case "addr": - opt.Addr = v - case "password": - opt.Password = v - case "db": - opt.DB = com.StrTo(v).MustInt() - case "pool_size": - opt.PoolSize = com.StrTo(v).MustInt() - case "idle_timeout": - opt.IdleTimeout, err = time.ParseDuration(v + "s") - if err != nil { - return fmt.Errorf("error parsing idle timeout: %v", err) - } case "prefix": - p.prefix = v - default: - return fmt.Errorf("session/redis: unsupported option '%s'", k) + p.prefix = v[0] } } - p.c = redis.NewClient(opt) + p.c = nosql.GetManager().GetRedisClient(uri.String()) return p.c.Ping().Err() } @@ -228,11 +205,11 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err // Count counts and returns number of sessions. func (p *RedisProvider) Count() int { - return int(p.c.DbSize().Val()) + return int(p.c.DBSize().Val()) } // GC calls GC to clean expired sessions. -func (_ *RedisProvider) GC() {} +func (*RedisProvider) GC() {} func init() { session.Register("redis", &RedisProvider{}) diff --git a/modules/session/virtual.go b/modules/session/virtual.go index c8e1e210c..1139cfe89 100644 --- a/modules/session/virtual.go +++ b/modules/session/virtual.go @@ -15,7 +15,6 @@ import ( mysql "gitea.com/macaron/session/mysql" nodb "gitea.com/macaron/session/nodb" postgres "gitea.com/macaron/session/postgres" - redis "gitea.com/macaron/session/redis" ) // VirtualSessionProvider represents a shadowed session provider implementation. @@ -40,7 +39,7 @@ func (o *VirtualSessionProvider) Init(gclifetime int64, config string) error { case "file": o.provider = &session.FileProvider{} case "redis": - o.provider = &redis.RedisProvider{} + o.provider = &RedisProvider{} case "mysql": o.provider = &mysql.MysqlProvider{} case "postgres": diff --git a/vendor/gitea.com/macaron/cache/redis/redis.goconvey b/vendor/gitea.com/macaron/cache/redis/redis.goconvey deleted file mode 100644 index 8485e986e..000000000 --- a/vendor/gitea.com/macaron/cache/redis/redis.goconvey +++ /dev/null @@ -1 +0,0 @@ -ignore \ No newline at end of file diff --git a/vendor/gitea.com/macaron/session/redis/redis.goconvey b/vendor/gitea.com/macaron/session/redis/redis.goconvey deleted file mode 100644 index 8485e986e..000000000 --- a/vendor/gitea.com/macaron/session/redis/redis.goconvey +++ /dev/null @@ -1 +0,0 @@ -ignore \ No newline at end of file diff --git a/vendor/github.com/go-redis/redis/.travis.yml b/vendor/github.com/go-redis/redis/.travis.yml deleted file mode 100644 index 6b110b4cb..000000000 --- a/vendor/github.com/go-redis/redis/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -sudo: false -language: go - -services: - - redis-server - -go: - - 1.9.x - - 1.10.x - - 1.11.x - - tip - -matrix: - allow_failures: - - go: tip - -install: - - go get github.com/onsi/ginkgo - - go get github.com/onsi/gomega diff --git a/vendor/github.com/go-redis/redis/CHANGELOG.md b/vendor/github.com/go-redis/redis/CHANGELOG.md deleted file mode 100644 index 19645661a..000000000 --- a/vendor/github.com/go-redis/redis/CHANGELOG.md +++ /dev/null @@ -1,25 +0,0 @@ -# Changelog - -## Unreleased - -- Cluster and Ring pipelines process commands for each node in its own goroutine. - -## 6.14 - -- Added Options.MinIdleConns. -- Added Options.MaxConnAge. -- PoolStats.FreeConns is renamed to PoolStats.IdleConns. -- Add Client.Do to simplify creating custom commands. -- Add Cmd.String, Cmd.Int, Cmd.Int64, Cmd.Uint64, Cmd.Float64, and Cmd.Bool helpers. -- Lower memory usage. - -## v6.13 - -- Ring got new options called `HashReplicas` and `Hash`. It is recommended to set `HashReplicas = 1000` for better keys distribution between shards. -- Cluster client was optimized to use much less memory when reloading cluster state. -- PubSub.ReceiveMessage is re-worked to not use ReceiveTimeout so it does not lose data when timeout occurres. In most cases it is recommended to use PubSub.Channel instead. -- Dialer.KeepAlive is set to 5 minutes by default. - -## v6.12 - -- ClusterClient got new option called `ClusterSlots` which allows to build cluster of normal Redis Servers that don't have cluster mode enabled. See https://godoc.org/github.com/go-redis/redis#example-NewClusterClient--ManualSetup diff --git a/vendor/github.com/go-redis/redis/internal/error.go b/vendor/github.com/go-redis/redis/internal/error.go deleted file mode 100644 index 34f6bd4dc..000000000 --- a/vendor/github.com/go-redis/redis/internal/error.go +++ /dev/null @@ -1,89 +0,0 @@ -package internal - -import ( - "io" - "net" - "strings" - - "github.com/go-redis/redis/internal/proto" -) - -func IsRetryableError(err error, retryTimeout bool) bool { - if err == nil { - return false - } - if err == io.EOF { - return true - } - if netErr, ok := err.(net.Error); ok { - if netErr.Timeout() { - return retryTimeout - } - return true - } - s := err.Error() - if s == "ERR max number of clients reached" { - return true - } - if strings.HasPrefix(s, "LOADING ") { - return true - } - if strings.HasPrefix(s, "READONLY ") { - return true - } - if strings.HasPrefix(s, "CLUSTERDOWN ") { - return true - } - return false -} - -func IsRedisError(err error) bool { - _, ok := err.(proto.RedisError) - return ok -} - -func IsBadConn(err error, allowTimeout bool) bool { - if err == nil { - return false - } - if IsRedisError(err) { - // #790 - return IsReadOnlyError(err) - } - if allowTimeout { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - return false - } - } - return true -} - -func IsMovedError(err error) (moved bool, ask bool, addr string) { - if !IsRedisError(err) { - return - } - - s := err.Error() - if strings.HasPrefix(s, "MOVED ") { - moved = true - } else if strings.HasPrefix(s, "ASK ") { - ask = true - } else { - return - } - - ind := strings.LastIndex(s, " ") - if ind == -1 { - return false, false, "" - } - addr = s[ind+1:] - return -} - -func IsLoadingError(err error) bool { - return strings.HasPrefix(err.Error(), "LOADING ") -} - -func IsReadOnlyError(err error) bool { - return strings.HasPrefix(err.Error(), "READONLY ") -} diff --git a/vendor/github.com/go-redis/redis/internal/log.go b/vendor/github.com/go-redis/redis/internal/log.go deleted file mode 100644 index fd14222ee..000000000 --- a/vendor/github.com/go-redis/redis/internal/log.go +++ /dev/null @@ -1,15 +0,0 @@ -package internal - -import ( - "fmt" - "log" -) - -var Logger *log.Logger - -func Logf(s string, args ...interface{}) { - if Logger == nil { - return - } - Logger.Output(2, fmt.Sprintf(s, args...)) -} diff --git a/vendor/github.com/go-redis/redis/internal/pool/conn.go b/vendor/github.com/go-redis/redis/internal/pool/conn.go deleted file mode 100644 index 1095bfe59..000000000 --- a/vendor/github.com/go-redis/redis/internal/pool/conn.go +++ /dev/null @@ -1,93 +0,0 @@ -package pool - -import ( - "net" - "sync/atomic" - "time" - - "github.com/go-redis/redis/internal/proto" -) - -var noDeadline = time.Time{} - -type Conn struct { - netConn net.Conn - - rd *proto.Reader - rdLocked bool - wr *proto.Writer - - InitedAt time.Time - pooled bool - usedAt atomic.Value -} - -func NewConn(netConn net.Conn) *Conn { - cn := &Conn{ - netConn: netConn, - } - cn.rd = proto.NewReader(netConn) - cn.wr = proto.NewWriter(netConn) - cn.SetUsedAt(time.Now()) - return cn -} - -func (cn *Conn) UsedAt() time.Time { - return cn.usedAt.Load().(time.Time) -} - -func (cn *Conn) SetUsedAt(tm time.Time) { - cn.usedAt.Store(tm) -} - -func (cn *Conn) SetNetConn(netConn net.Conn) { - cn.netConn = netConn - cn.rd.Reset(netConn) - cn.wr.Reset(netConn) -} - -func (cn *Conn) setReadTimeout(timeout time.Duration) error { - now := time.Now() - cn.SetUsedAt(now) - if timeout > 0 { - return cn.netConn.SetReadDeadline(now.Add(timeout)) - } - return cn.netConn.SetReadDeadline(noDeadline) -} - -func (cn *Conn) setWriteTimeout(timeout time.Duration) error { - now := time.Now() - cn.SetUsedAt(now) - if timeout > 0 { - return cn.netConn.SetWriteDeadline(now.Add(timeout)) - } - return cn.netConn.SetWriteDeadline(noDeadline) -} - -func (cn *Conn) Write(b []byte) (int, error) { - return cn.netConn.Write(b) -} - -func (cn *Conn) RemoteAddr() net.Addr { - return cn.netConn.RemoteAddr() -} - -func (cn *Conn) WithReader(timeout time.Duration, fn func(rd *proto.Reader) error) error { - _ = cn.setReadTimeout(timeout) - return fn(cn.rd) -} - -func (cn *Conn) WithWriter(timeout time.Duration, fn func(wr *proto.Writer) error) error { - _ = cn.setWriteTimeout(timeout) - - firstErr := fn(cn.wr) - err := cn.wr.Flush() - if err != nil && firstErr == nil { - firstErr = err - } - return firstErr -} - -func (cn *Conn) Close() error { - return cn.netConn.Close() -} diff --git a/vendor/github.com/go-redis/redis/internal/pool/pool_single.go b/vendor/github.com/go-redis/redis/internal/pool/pool_single.go deleted file mode 100644 index b35b78afb..000000000 --- a/vendor/github.com/go-redis/redis/internal/pool/pool_single.go +++ /dev/null @@ -1,53 +0,0 @@ -package pool - -type SingleConnPool struct { - cn *Conn -} - -var _ Pooler = (*SingleConnPool)(nil) - -func NewSingleConnPool(cn *Conn) *SingleConnPool { - return &SingleConnPool{ - cn: cn, - } -} - -func (p *SingleConnPool) NewConn() (*Conn, error) { - panic("not implemented") -} - -func (p *SingleConnPool) CloseConn(*Conn) error { - panic("not implemented") -} - -func (p *SingleConnPool) Get() (*Conn, error) { - return p.cn, nil -} - -func (p *SingleConnPool) Put(cn *Conn) { - if p.cn != cn { - panic("p.cn != cn") - } -} - -func (p *SingleConnPool) Remove(cn *Conn) { - if p.cn != cn { - panic("p.cn != cn") - } -} - -func (p *SingleConnPool) Len() int { - return 1 -} - -func (p *SingleConnPool) IdleLen() int { - return 0 -} - -func (p *SingleConnPool) Stats() *Stats { - return nil -} - -func (p *SingleConnPool) Close() error { - return nil -} diff --git a/vendor/github.com/go-redis/redis/internal/util.go b/vendor/github.com/go-redis/redis/internal/util.go deleted file mode 100644 index ffd2353e0..000000000 --- a/vendor/github.com/go-redis/redis/internal/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package internal - -import "github.com/go-redis/redis/internal/util" - -func ToLower(s string) string { - if isLower(s) { - return s - } - - b := make([]byte, len(s)) - for i := range b { - c := s[i] - if c >= 'A' && c <= 'Z' { - c += 'a' - 'A' - } - b[i] = c - } - return util.BytesToString(b) -} - -func isLower(s string) bool { - for i := 0; i < len(s); i++ { - c := s[i] - if c >= 'A' && c <= 'Z' { - return false - } - } - return true -} diff --git a/vendor/github.com/go-redis/redis/redis.go b/vendor/github.com/go-redis/redis/redis.go deleted file mode 100644 index aca30648f..000000000 --- a/vendor/github.com/go-redis/redis/redis.go +++ /dev/null @@ -1,580 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "log" - "os" - "time" - - "github.com/go-redis/redis/internal" - "github.com/go-redis/redis/internal/pool" - "github.com/go-redis/redis/internal/proto" -) - -// Nil reply Redis returns when key does not exist. -const Nil = proto.Nil - -func init() { - SetLogger(log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile)) -} - -func SetLogger(logger *log.Logger) { - internal.Logger = logger -} - -type baseClient struct { - opt *Options - connPool pool.Pooler - limiter Limiter - - process func(Cmder) error - processPipeline func([]Cmder) error - processTxPipeline func([]Cmder) error - - onClose func() error // hook called when client is closed -} - -func (c *baseClient) init() { - c.process = c.defaultProcess - c.processPipeline = c.defaultProcessPipeline - c.processTxPipeline = c.defaultProcessTxPipeline -} - -func (c *baseClient) String() string { - return fmt.Sprintf("Redis<%s db:%d>", c.getAddr(), c.opt.DB) -} - -func (c *baseClient) newConn() (*pool.Conn, error) { - cn, err := c.connPool.NewConn() - if err != nil { - return nil, err - } - - if cn.InitedAt.IsZero() { - if err := c.initConn(cn); err != nil { - _ = c.connPool.CloseConn(cn) - return nil, err - } - } - - return cn, nil -} - -func (c *baseClient) getConn() (*pool.Conn, error) { - if c.limiter != nil { - err := c.limiter.Allow() - if err != nil { - return nil, err - } - } - - cn, err := c._getConn() - if err != nil { - if c.limiter != nil { - c.limiter.ReportResult(err) - } - return nil, err - } - return cn, nil -} - -func (c *baseClient) _getConn() (*pool.Conn, error) { - cn, err := c.connPool.Get() - if err != nil { - return nil, err - } - - if cn.InitedAt.IsZero() { - err := c.initConn(cn) - if err != nil { - c.connPool.Remove(cn) - return nil, err - } - } - - return cn, nil -} - -func (c *baseClient) releaseConn(cn *pool.Conn, err error) { - if c.limiter != nil { - c.limiter.ReportResult(err) - } - - if internal.IsBadConn(err, false) { - c.connPool.Remove(cn) - } else { - c.connPool.Put(cn) - } -} - -func (c *baseClient) releaseConnStrict(cn *pool.Conn, err error) { - if c.limiter != nil { - c.limiter.ReportResult(err) - } - - if err == nil || internal.IsRedisError(err) { - c.connPool.Put(cn) - } else { - c.connPool.Remove(cn) - } -} - -func (c *baseClient) initConn(cn *pool.Conn) error { - cn.InitedAt = time.Now() - - if c.opt.Password == "" && - c.opt.DB == 0 && - !c.opt.readOnly && - c.opt.OnConnect == nil { - return nil - } - - conn := newConn(c.opt, cn) - _, err := conn.Pipelined(func(pipe Pipeliner) error { - if c.opt.Password != "" { - pipe.Auth(c.opt.Password) - } - - if c.opt.DB > 0 { - pipe.Select(c.opt.DB) - } - - if c.opt.readOnly { - pipe.ReadOnly() - } - - return nil - }) - if err != nil { - return err - } - - if c.opt.OnConnect != nil { - return c.opt.OnConnect(conn) - } - return nil -} - -// Do creates a Cmd from the args and processes the cmd. -func (c *baseClient) Do(args ...interface{}) *Cmd { - cmd := NewCmd(args...) - _ = c.Process(cmd) - return cmd -} - -// WrapProcess wraps function that processes Redis commands. -func (c *baseClient) WrapProcess( - fn func(oldProcess func(cmd Cmder) error) func(cmd Cmder) error, -) { - c.process = fn(c.process) -} - -func (c *baseClient) Process(cmd Cmder) error { - return c.process(cmd) -} - -func (c *baseClient) defaultProcess(cmd Cmder) error { - for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ { - if attempt > 0 { - time.Sleep(c.retryBackoff(attempt)) - } - - cn, err := c.getConn() - if err != nil { - cmd.setErr(err) - if internal.IsRetryableError(err, true) { - continue - } - return err - } - - err = cn.WithWriter(c.opt.WriteTimeout, func(wr *proto.Writer) error { - return writeCmd(wr, cmd) - }) - if err != nil { - c.releaseConn(cn, err) - cmd.setErr(err) - if internal.IsRetryableError(err, true) { - continue - } - return err - } - - err = cn.WithReader(c.cmdTimeout(cmd), func(rd *proto.Reader) error { - return cmd.readReply(rd) - }) - c.releaseConn(cn, err) - if err != nil && internal.IsRetryableError(err, cmd.readTimeout() == nil) { - continue - } - - return err - } - - return cmd.Err() -} - -func (c *baseClient) retryBackoff(attempt int) time.Duration { - return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff) -} - -func (c *baseClient) cmdTimeout(cmd Cmder) time.Duration { - if timeout := cmd.readTimeout(); timeout != nil { - t := *timeout - if t == 0 { - return 0 - } - return t + 10*time.Second - } - return c.opt.ReadTimeout -} - -// Close closes the client, releasing any open resources. -// -// It is rare to Close a Client, as the Client is meant to be -// long-lived and shared between many goroutines. -func (c *baseClient) Close() error { - var firstErr error - if c.onClose != nil { - if err := c.onClose(); err != nil && firstErr == nil { - firstErr = err - } - } - if err := c.connPool.Close(); err != nil && firstErr == nil { - firstErr = err - } - return firstErr -} - -func (c *baseClient) getAddr() string { - return c.opt.Addr -} - -func (c *baseClient) WrapProcessPipeline( - fn func(oldProcess func([]Cmder) error) func([]Cmder) error, -) { - c.processPipeline = fn(c.processPipeline) - c.processTxPipeline = fn(c.processTxPipeline) -} - -func (c *baseClient) defaultProcessPipeline(cmds []Cmder) error { - return c.generalProcessPipeline(cmds, c.pipelineProcessCmds) -} - -func (c *baseClient) defaultProcessTxPipeline(cmds []Cmder) error { - return c.generalProcessPipeline(cmds, c.txPipelineProcessCmds) -} - -type pipelineProcessor func(*pool.Conn, []Cmder) (bool, error) - -func (c *baseClient) generalProcessPipeline(cmds []Cmder, p pipelineProcessor) error { - for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ { - if attempt > 0 { - time.Sleep(c.retryBackoff(attempt)) - } - - cn, err := c.getConn() - if err != nil { - setCmdsErr(cmds, err) - return err - } - - canRetry, err := p(cn, cmds) - c.releaseConnStrict(cn, err) - - if !canRetry || !internal.IsRetryableError(err, true) { - break - } - } - return cmdsFirstErr(cmds) -} - -func (c *baseClient) pipelineProcessCmds(cn *pool.Conn, cmds []Cmder) (bool, error) { - err := cn.WithWriter(c.opt.WriteTimeout, func(wr *proto.Writer) error { - return writeCmd(wr, cmds...) - }) - if err != nil { - setCmdsErr(cmds, err) - return true, err - } - - err = cn.WithReader(c.opt.ReadTimeout, func(rd *proto.Reader) error { - return pipelineReadCmds(rd, cmds) - }) - return true, err -} - -func pipelineReadCmds(rd *proto.Reader, cmds []Cmder) error { - for _, cmd := range cmds { - err := cmd.readReply(rd) - if err != nil && !internal.IsRedisError(err) { - return err - } - } - return nil -} - -func (c *baseClient) txPipelineProcessCmds(cn *pool.Conn, cmds []Cmder) (bool, error) { - err := cn.WithWriter(c.opt.WriteTimeout, func(wr *proto.Writer) error { - return txPipelineWriteMulti(wr, cmds) - }) - if err != nil { - setCmdsErr(cmds, err) - return true, err - } - - err = cn.WithReader(c.opt.ReadTimeout, func(rd *proto.Reader) error { - err := txPipelineReadQueued(rd, cmds) - if err != nil { - setCmdsErr(cmds, err) - return err - } - return pipelineReadCmds(rd, cmds) - }) - return false, err -} - -func txPipelineWriteMulti(wr *proto.Writer, cmds []Cmder) error { - multiExec := make([]Cmder, 0, len(cmds)+2) - multiExec = append(multiExec, NewStatusCmd("MULTI")) - multiExec = append(multiExec, cmds...) - multiExec = append(multiExec, NewSliceCmd("EXEC")) - return writeCmd(wr, multiExec...) -} - -func txPipelineReadQueued(rd *proto.Reader, cmds []Cmder) error { - // Parse queued replies. - var statusCmd StatusCmd - err := statusCmd.readReply(rd) - if err != nil { - return err - } - - for range cmds { - err = statusCmd.readReply(rd) - if err != nil && !internal.IsRedisError(err) { - return err - } - } - - // Parse number of replies. - line, err := rd.ReadLine() - if err != nil { - if err == Nil { - err = TxFailedErr - } - return err - } - - switch line[0] { - case proto.ErrorReply: - return proto.ParseErrorReply(line) - case proto.ArrayReply: - // ok - default: - err := fmt.Errorf("redis: expected '*', but got line %q", line) - return err - } - - return nil -} - -//------------------------------------------------------------------------------ - -// Client is a Redis client representing a pool of zero or more -// underlying connections. It's safe for concurrent use by multiple -// goroutines. -type Client struct { - baseClient - cmdable - - ctx context.Context -} - -// NewClient returns a client to the Redis Server specified by Options. -func NewClient(opt *Options) *Client { - opt.init() - - c := Client{ - baseClient: baseClient{ - opt: opt, - connPool: newConnPool(opt), - }, - } - c.baseClient.init() - c.init() - - return &c -} - -func (c *Client) init() { - c.cmdable.setProcessor(c.Process) -} - -func (c *Client) Context() context.Context { - if c.ctx != nil { - return c.ctx - } - return context.Background() -} - -func (c *Client) WithContext(ctx context.Context) *Client { - if ctx == nil { - panic("nil context") - } - c2 := c.clone() - c2.ctx = ctx - return c2 -} - -func (c *Client) clone() *Client { - cp := *c - cp.init() - return &cp -} - -// Options returns read-only Options that were used to create the client. -func (c *Client) Options() *Options { - return c.opt -} - -func (c *Client) SetLimiter(l Limiter) *Client { - c.limiter = l - return c -} - -type PoolStats pool.Stats - -// PoolStats returns connection pool stats. -func (c *Client) PoolStats() *PoolStats { - stats := c.connPool.Stats() - return (*PoolStats)(stats) -} - -func (c *Client) Pipelined(fn func(Pipeliner) error) ([]Cmder, error) { - return c.Pipeline().Pipelined(fn) -} - -func (c *Client) Pipeline() Pipeliner { - pipe := Pipeline{ - exec: c.processPipeline, - } - pipe.statefulCmdable.setProcessor(pipe.Process) - return &pipe -} - -func (c *Client) TxPipelined(fn func(Pipeliner) error) ([]Cmder, error) { - return c.TxPipeline().Pipelined(fn) -} - -// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC. -func (c *Client) TxPipeline() Pipeliner { - pipe := Pipeline{ - exec: c.processTxPipeline, - } - pipe.statefulCmdable.setProcessor(pipe.Process) - return &pipe -} - -func (c *Client) pubSub() *PubSub { - pubsub := &PubSub{ - opt: c.opt, - - newConn: func(channels []string) (*pool.Conn, error) { - return c.newConn() - }, - closeConn: c.connPool.CloseConn, - } - pubsub.init() - return pubsub -} - -// Subscribe subscribes the client to the specified channels. -// Channels can be omitted to create empty subscription. -// Note that this method does not wait on a response from Redis, so the -// subscription may not be active immediately. To force the connection to wait, -// you may call the Receive() method on the returned *PubSub like so: -// -// sub := client.Subscribe(queryResp) -// iface, err := sub.Receive() -// if err != nil { -// // handle error -// } -// -// // Should be *Subscription, but others are possible if other actions have been -// // taken on sub since it was created. -// switch iface.(type) { -// case *Subscription: -// // subscribe succeeded -// case *Message: -// // received first message -// case *Pong: -// // pong received -// default: -// // handle error -// } -// -// ch := sub.Channel() -func (c *Client) Subscribe(channels ...string) *PubSub { - pubsub := c.pubSub() - if len(channels) > 0 { - _ = pubsub.Subscribe(channels...) - } - return pubsub -} - -// PSubscribe subscribes the client to the given patterns. -// Patterns can be omitted to create empty subscription. -func (c *Client) PSubscribe(channels ...string) *PubSub { - pubsub := c.pubSub() - if len(channels) > 0 { - _ = pubsub.PSubscribe(channels...) - } - return pubsub -} - -//------------------------------------------------------------------------------ - -// Conn is like Client, but its pool contains single connection. -type Conn struct { - baseClient - statefulCmdable -} - -func newConn(opt *Options, cn *pool.Conn) *Conn { - c := Conn{ - baseClient: baseClient{ - opt: opt, - connPool: pool.NewSingleConnPool(cn), - }, - } - c.baseClient.init() - c.statefulCmdable.setProcessor(c.Process) - return &c -} - -func (c *Conn) Pipelined(fn func(Pipeliner) error) ([]Cmder, error) { - return c.Pipeline().Pipelined(fn) -} - -func (c *Conn) Pipeline() Pipeliner { - pipe := Pipeline{ - exec: c.processPipeline, - } - pipe.statefulCmdable.setProcessor(pipe.Process) - return &pipe -} - -func (c *Conn) TxPipelined(fn func(Pipeliner) error) ([]Cmder, error) { - return c.TxPipeline().Pipelined(fn) -} - -// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC. -func (c *Conn) TxPipeline() Pipeliner { - pipe := Pipeline{ - exec: c.processTxPipeline, - } - pipe.statefulCmdable.setProcessor(pipe.Process) - return &pipe -} diff --git a/vendor/github.com/go-redis/redis/.gitignore b/vendor/github.com/go-redis/redis/v7/.gitignore similarity index 100% rename from vendor/github.com/go-redis/redis/.gitignore rename to vendor/github.com/go-redis/redis/v7/.gitignore diff --git a/vendor/github.com/go-redis/redis/v7/.golangci.yml b/vendor/github.com/go-redis/redis/v7/.golangci.yml new file mode 100644 index 000000000..912dab1ef --- /dev/null +++ b/vendor/github.com/go-redis/redis/v7/.golangci.yml @@ -0,0 +1,15 @@ +run: + concurrency: 8 + deadline: 5m + tests: false +linters: + enable-all: true + disable: + - funlen + - gochecknoglobals + - gocognit + - goconst + - godox + - gosec + - maligned + - wsl diff --git a/vendor/github.com/go-redis/redis/v7/.travis.yml b/vendor/github.com/go-redis/redis/v7/.travis.yml new file mode 100644 index 000000000..3f93932bc --- /dev/null +++ b/vendor/github.com/go-redis/redis/v7/.travis.yml @@ -0,0 +1,22 @@ +dist: xenial +language: go + +services: + - redis-server + +go: + - 1.12.x + - 1.13.x + - tip + +matrix: + allow_failures: + - go: tip + +env: + - GO111MODULE=on + +go_import_path: github.com/go-redis/redis + +before_install: + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.21.0 diff --git a/vendor/github.com/go-redis/redis/v7/CHANGELOG.md b/vendor/github.com/go-redis/redis/v7/CHANGELOG.md new file mode 100644 index 000000000..bd4eccff2 --- /dev/null +++ b/vendor/github.com/go-redis/redis/v7/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +## v7.2 + +- Existing `HMSet` is renamed to `HSet` and old deprecated `HMSet` is restored for Redis 3 users. + +## v7.1 + +- Existing `Cmd.String` is renamed to `Cmd.Text`. New `Cmd.String` implements `fmt.Stringer` interface. + +## v7 + +- *Important*. Tx.Pipeline now returns a non-transactional pipeline. Use Tx.TxPipeline for a transactional pipeline. +- WrapProcess is replaced with more convenient AddHook that has access to context.Context. +- WithContext now can not be used to create a shallow copy of the client. +- New methods ProcessContext, DoContext, and ExecContext. +- Client respects Context.Deadline when setting net.Conn deadline. +- Client listens on Context.Done while waiting for a connection from the pool and returns an error when context context is cancelled. +- Add PubSub.ChannelWithSubscriptions that sends `*Subscription` in addition to `*Message` to allow detecting reconnections. +- `time.Time` is now marshalled in RFC3339 format. `rdb.Get("foo").Time()` helper is added to parse the time. +- `SetLimiter` is removed and added `Options.Limiter` instead. +- `HMSet` is deprecated as of Redis v4. + +## v6.15 + +- Cluster and Ring pipelines process commands for each node in its own goroutine. + +## 6.14 + +- Added Options.MinIdleConns. +- Added Options.MaxConnAge. +- PoolStats.FreeConns is renamed to PoolStats.IdleConns. +- Add Client.Do to simplify creating custom commands. +- Add Cmd.String, Cmd.Int, Cmd.Int64, Cmd.Uint64, Cmd.Float64, and Cmd.Bool helpers. +- Lower memory usage. + +## v6.13 + +- Ring got new options called `HashReplicas` and `Hash`. It is recommended to set `HashReplicas = 1000` for better keys distribution between shards. +- Cluster client was optimized to use much less memory when reloading cluster state. +- PubSub.ReceiveMessage is re-worked to not use ReceiveTimeout so it does not lose data when timeout occurres. In most cases it is recommended to use PubSub.Channel instead. +- Dialer.KeepAlive is set to 5 minutes by default. + +## v6.12 + +- ClusterClient got new option called `ClusterSlots` which allows to build cluster of normal Redis Servers that don't have cluster mode enabled. See https://godoc.org/github.com/go-redis/redis#example-NewClusterClient--ManualSetup diff --git a/vendor/github.com/go-redis/redis/LICENSE b/vendor/github.com/go-redis/redis/v7/LICENSE similarity index 100% rename from vendor/github.com/go-redis/redis/LICENSE rename to vendor/github.com/go-redis/redis/v7/LICENSE diff --git a/vendor/github.com/go-redis/redis/Makefile b/vendor/github.com/go-redis/redis/v7/Makefile similarity index 59% rename from vendor/github.com/go-redis/redis/Makefile rename to vendor/github.com/go-redis/redis/v7/Makefile index fa3b4e004..86609c6e0 100644 --- a/vendor/github.com/go-redis/redis/Makefile +++ b/vendor/github.com/go-redis/redis/v7/Makefile @@ -1,10 +1,9 @@ all: testdeps go test ./... go test ./... -short -race + go test ./... -run=NONE -bench=. -benchmem env GOOS=linux GOARCH=386 go test ./... - go vet - go get github.com/gordonklaus/ineffassign - ineffassign . + golangci-lint run testdeps: testdata/redis/src/redis-server @@ -15,8 +14,7 @@ bench: testdeps testdata/redis: mkdir -p $@ - wget -qO- https://github.com/antirez/redis/archive/5.0.tar.gz | tar xvz --strip-components=1 -C $@ + wget -qO- http://download.redis.io/redis-stable.tar.gz | tar xvz --strip-components=1 -C $@ testdata/redis/src/redis-server: testdata/redis - sed -i.bak 's/libjemalloc.a/libjemalloc.a -lrt/g' $ threshold { @@ -558,10 +571,13 @@ func (c *clusterState) slotClosestNode(slot int) (*clusterNode, error) { return node, nil } -func (c *clusterState) slotRandomNode(slot int) *clusterNode { +func (c *clusterState) slotRandomNode(slot int) (*clusterNode, error) { nodes := c.slotNodes(slot) + if len(nodes) == 0 { + return c.nodes.Random() + } n := rand.Intn(len(nodes)) - return nodes[n] + return nodes[n], nil } func (c *clusterState) slotNodes(slot int) []*clusterNode { @@ -639,22 +655,21 @@ func (c *clusterStateHolder) ReloadOrGet() (*clusterState, error) { //------------------------------------------------------------------------------ +type clusterClient struct { + opt *ClusterOptions + nodes *clusterNodes + state *clusterStateHolder //nolint:structcheck + cmdsInfoCache *cmdsInfoCache //nolint:structcheck +} + // ClusterClient is a Redis Cluster client representing a pool of zero // or more underlying connections. It's safe for concurrent use by // multiple goroutines. type ClusterClient struct { + *clusterClient cmdable - + hooks ctx context.Context - - opt *ClusterOptions - nodes *clusterNodes - state *clusterStateHolder - cmdsInfoCache *cmdsInfoCache - - process func(Cmder) error - processPipeline func([]Cmder) error - processTxPipeline func([]Cmder) error } // NewClusterClient returns a Redis Cluster client as described in @@ -663,17 +678,16 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient { opt.init() c := &ClusterClient{ - opt: opt, - nodes: newClusterNodes(opt), + clusterClient: &clusterClient{ + opt: opt, + nodes: newClusterNodes(opt), + }, + ctx: context.Background(), } c.state = newClusterStateHolder(c.loadState) c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo) + c.cmdable = c.Process - c.process = c.defaultProcess - c.processPipeline = c.defaultProcessPipeline - c.processTxPipeline = c.defaultProcessTxPipeline - - c.init() if opt.IdleCheckFrequency > 0 { go c.reaper(opt.IdleCheckFrequency) } @@ -681,37 +695,19 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient { return c } -func (c *ClusterClient) init() { - c.cmdable.setProcessor(c.Process) -} - -// ReloadState reloads cluster state. If available it calls ClusterSlots func -// to get cluster slots information. -func (c *ClusterClient) ReloadState() error { - _, err := c.state.Reload() - return err -} - func (c *ClusterClient) Context() context.Context { - if c.ctx != nil { - return c.ctx - } - return context.Background() + return c.ctx } func (c *ClusterClient) WithContext(ctx context.Context) *ClusterClient { if ctx == nil { panic("nil context") } - c2 := c.copy() - c2.ctx = ctx - return c2 -} - -func (c *ClusterClient) copy() *ClusterClient { - cp := *c - cp.init() - return &cp + clone := *c + clone.cmdable = clone.Process + clone.hooks.lock() + clone.ctx = ctx + return &clone } // Options returns read-only Options that were used to create the client. @@ -719,164 +715,10 @@ func (c *ClusterClient) Options() *ClusterOptions { return c.opt } -func (c *ClusterClient) retryBackoff(attempt int) time.Duration { - return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff) -} - -func (c *ClusterClient) cmdsInfo() (map[string]*CommandInfo, error) { - addrs, err := c.nodes.Addrs() - if err != nil { - return nil, err - } - - var firstErr error - for _, addr := range addrs { - node, err := c.nodes.Get(addr) - if err != nil { - return nil, err - } - if node == nil { - continue - } - - info, err := node.Client.Command().Result() - if err == nil { - return info, nil - } - if firstErr == nil { - firstErr = err - } - } - return nil, firstErr -} - -func (c *ClusterClient) cmdInfo(name string) *CommandInfo { - cmdsInfo, err := c.cmdsInfoCache.Get() - if err != nil { - return nil - } - - info := cmdsInfo[name] - if info == nil { - internal.Logf("info for cmd=%s not found", name) - } - return info -} - -func cmdSlot(cmd Cmder, pos int) int { - if pos == 0 { - return hashtag.RandomSlot() - } - firstKey := cmd.stringArg(pos) - return hashtag.Slot(firstKey) -} - -func (c *ClusterClient) cmdSlot(cmd Cmder) int { - args := cmd.Args() - if args[0] == "cluster" && args[1] == "getkeysinslot" { - return args[2].(int) - } - - cmdInfo := c.cmdInfo(cmd.Name()) - return cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo)) -} - -func (c *ClusterClient) cmdSlotAndNode(cmd Cmder) (int, *clusterNode, error) { - state, err := c.state.Get() - if err != nil { - return 0, nil, err - } - - cmdInfo := c.cmdInfo(cmd.Name()) - slot := c.cmdSlot(cmd) - - if c.opt.ReadOnly && cmdInfo != nil && cmdInfo.ReadOnly { - if c.opt.RouteByLatency { - node, err := state.slotClosestNode(slot) - return slot, node, err - } - - if c.opt.RouteRandomly { - node := state.slotRandomNode(slot) - return slot, node, nil - } - - node, err := state.slotSlaveNode(slot) - return slot, node, err - } - - node, err := state.slotMasterNode(slot) - return slot, node, err -} - -func (c *ClusterClient) slotMasterNode(slot int) (*clusterNode, error) { - state, err := c.state.Get() - if err != nil { - return nil, err - } - - nodes := state.slotNodes(slot) - if len(nodes) > 0 { - return nodes[0], nil - } - return c.nodes.Random() -} - -func (c *ClusterClient) Watch(fn func(*Tx) error, keys ...string) error { - if len(keys) == 0 { - return fmt.Errorf("redis: Watch requires at least one key") - } - - slot := hashtag.Slot(keys[0]) - for _, key := range keys[1:] { - if hashtag.Slot(key) != slot { - err := fmt.Errorf("redis: Watch requires all keys to be in the same slot") - return err - } - } - - node, err := c.slotMasterNode(slot) - if err != nil { - return err - } - - for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { - if attempt > 0 { - time.Sleep(c.retryBackoff(attempt)) - } - - err = node.Client.Watch(fn, keys...) - if err == nil { - break - } - if err != Nil { - c.state.LazyReload() - } - - moved, ask, addr := internal.IsMovedError(err) - if moved || ask { - node, err = c.nodes.GetOrCreate(addr) - if err != nil { - return err - } - continue - } - - if err == pool.ErrClosed || internal.IsReadOnlyError(err) { - node, err = c.slotMasterNode(slot) - if err != nil { - return err - } - continue - } - - if internal.IsRetryableError(err, true) { - continue - } - - return err - } - +// ReloadState reloads cluster state. If available it calls ClusterSlots func +// to get cluster slots information. +func (c *ClusterClient) ReloadState() error { + _, err := c.state.Reload() return err } @@ -890,99 +732,111 @@ func (c *ClusterClient) Close() error { // Do creates a Cmd from the args and processes the cmd. func (c *ClusterClient) Do(args ...interface{}) *Cmd { + return c.DoContext(c.ctx, args...) +} + +func (c *ClusterClient) DoContext(ctx context.Context, args ...interface{}) *Cmd { cmd := NewCmd(args...) - c.Process(cmd) + _ = c.ProcessContext(ctx, cmd) return cmd } -func (c *ClusterClient) WrapProcess( - fn func(oldProcess func(Cmder) error) func(Cmder) error, -) { - c.process = fn(c.process) -} - func (c *ClusterClient) Process(cmd Cmder) error { - return c.process(cmd) + return c.ProcessContext(c.ctx, cmd) } -func (c *ClusterClient) defaultProcess(cmd Cmder) error { +func (c *ClusterClient) ProcessContext(ctx context.Context, cmd Cmder) error { + return c.hooks.process(ctx, cmd, c.process) +} + +func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { + err := c._process(ctx, cmd) + if err != nil { + cmd.SetErr(err) + return err + } + return nil +} + +func (c *ClusterClient) _process(ctx context.Context, cmd Cmder) error { + cmdInfo := c.cmdInfo(cmd.Name()) + slot := c.cmdSlot(cmd) + var node *clusterNode var ask bool + var lastErr error for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { if attempt > 0 { - time.Sleep(c.retryBackoff(attempt)) + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + return err + } } if node == nil { var err error - _, node, err = c.cmdSlotAndNode(cmd) + node, err = c.cmdNode(cmdInfo, slot) if err != nil { - cmd.setErr(err) - break + return err } } - var err error if ask { pipe := node.Client.Pipeline() - _ = pipe.Process(NewCmd("ASKING")) + _ = pipe.Process(NewCmd("asking")) _ = pipe.Process(cmd) - _, err = pipe.Exec() + _, lastErr = pipe.ExecContext(ctx) _ = pipe.Close() ask = false } else { - err = node.Client.Process(cmd) + lastErr = node.Client.ProcessContext(ctx, cmd) } // If there is no error - we are done. - if err == nil { - break + if lastErr == nil { + return nil } - if err != Nil { + if lastErr != Nil { c.state.LazyReload() } + if lastErr == pool.ErrClosed || isReadOnlyError(lastErr) { + node = nil + continue + } // If slave is loading - pick another node. - if c.opt.ReadOnly && internal.IsLoadingError(err) { - node.MarkAsLoading() + if c.opt.ReadOnly && isLoadingError(lastErr) { + node.MarkAsFailing() node = nil continue } var moved bool var addr string - moved, ask, addr = internal.IsMovedError(err) + moved, ask, addr = isMovedError(lastErr) if moved || ask { - node, err = c.nodes.GetOrCreate(addr) + var err error + node, err = c.nodes.Get(addr) if err != nil { - break + return err } continue } - if err == pool.ErrClosed || internal.IsReadOnlyError(err) { - node = nil - continue - } - - if internal.IsRetryableError(err, true) { + if isRetryableError(lastErr, cmd.readTimeout() == nil) { // First retry the same node. if attempt == 0 { continue } - // Second try random node. - node, err = c.nodes.Random() - if err != nil { - break - } + // Second try another node. + node.MarkAsFailing() + node = nil continue } - break + return lastErr } - - return cmd.Err() + return lastErr } // ForEachMaster concurrently calls the fn on each master node in the cluster. @@ -995,6 +849,7 @@ func (c *ClusterClient) ForEachMaster(fn func(client *Client) error) error { var wg sync.WaitGroup errCh := make(chan error, 1) + for _, master := range state.Masters { wg.Add(1) go func(node *clusterNode) { @@ -1008,6 +863,7 @@ func (c *ClusterClient) ForEachMaster(fn func(client *Client) error) error { } }(master) } + wg.Wait() select { @@ -1028,6 +884,7 @@ func (c *ClusterClient) ForEachSlave(fn func(client *Client) error) error { var wg sync.WaitGroup errCh := make(chan error, 1) + for _, slave := range state.Slaves { wg.Add(1) go func(node *clusterNode) { @@ -1041,6 +898,7 @@ func (c *ClusterClient) ForEachSlave(fn func(client *Client) error) error { } }(slave) } + wg.Wait() select { @@ -1061,6 +919,7 @@ func (c *ClusterClient) ForEachNode(fn func(client *Client) error) error { var wg sync.WaitGroup errCh := make(chan error, 1) + worker := func(node *clusterNode) { defer wg.Done() err := fn(node.Client) @@ -1082,6 +941,7 @@ func (c *ClusterClient) ForEachNode(fn func(client *Client) error) error { } wg.Wait() + select { case err := <-errCh: return err @@ -1140,7 +1000,7 @@ func (c *ClusterClient) loadState() (*clusterState, error) { var firstErr error for _, addr := range addrs { - node, err := c.nodes.GetOrCreate(addr) + node, err := c.nodes.Get(addr) if err != nil { if firstErr == nil { firstErr = err @@ -1176,7 +1036,7 @@ func (c *ClusterClient) reaper(idleCheckFrequency time.Duration) { for _, node := range nodes { _, err := node.Client.connPool.(*pool.ConnPool).ReapStaleConns() if err != nil { - internal.Logf("ReapStaleConns failed: %s", err) + internal.Logger.Printf("ReapStaleConns failed: %s", err) } } } @@ -1184,9 +1044,10 @@ func (c *ClusterClient) reaper(idleCheckFrequency time.Duration) { func (c *ClusterClient) Pipeline() Pipeliner { pipe := Pipeline{ + ctx: c.ctx, exec: c.processPipeline, } - pipe.statefulCmdable.setProcessor(pipe.Process) + pipe.init() return &pipe } @@ -1194,15 +1055,13 @@ func (c *ClusterClient) Pipelined(fn func(Pipeliner) error) ([]Cmder, error) { return c.Pipeline().Pipelined(fn) } -func (c *ClusterClient) WrapProcessPipeline( - fn func(oldProcess func([]Cmder) error) func([]Cmder) error, -) { - c.processPipeline = fn(c.processPipeline) +func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error { + return c.hooks.processPipeline(ctx, cmds, c._processPipeline) } -func (c *ClusterClient) defaultProcessPipeline(cmds []Cmder) error { +func (c *ClusterClient) _processPipeline(ctx context.Context, cmds []Cmder) error { cmdsMap := newCmdsMap() - err := c.mapCmdsByNode(cmds, cmdsMap) + err := c.mapCmdsByNode(cmdsMap, cmds) if err != nil { setCmdsErr(cmds, err) return err @@ -1210,7 +1069,10 @@ func (c *ClusterClient) defaultProcessPipeline(cmds []Cmder) error { for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { if attempt > 0 { - time.Sleep(c.retryBackoff(attempt)) + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + setCmdsErr(cmds, err) + return err + } } failedCmds := newCmdsMap() @@ -1221,18 +1083,17 @@ func (c *ClusterClient) defaultProcessPipeline(cmds []Cmder) error { go func(node *clusterNode, cmds []Cmder) { defer wg.Done() - cn, err := node.Client.getConn() - if err != nil { - if err == pool.ErrClosed { - c.mapCmdsByNode(cmds, failedCmds) - } else { - setCmdsErr(cmds, err) - } + err := c._processPipelineNode(ctx, node, cmds, failedCmds) + if err == nil { return } - - err = c.pipelineProcessCmds(node, cn, cmds, failedCmds) - node.Client.releaseConnStrict(cn, err) + if attempt < c.opt.MaxRedirects { + if err := c.mapCmdsByNode(failedCmds, cmds); err != nil { + setCmdsErr(cmds, err) + } + } else { + setCmdsErr(cmds, err) + } }(node, cmds) } @@ -1246,40 +1107,31 @@ func (c *ClusterClient) defaultProcessPipeline(cmds []Cmder) error { return cmdsFirstErr(cmds) } -type cmdsMap struct { - mu sync.Mutex - m map[*clusterNode][]Cmder -} - -func newCmdsMap() *cmdsMap { - return &cmdsMap{ - m: make(map[*clusterNode][]Cmder), - } -} - -func (c *ClusterClient) mapCmdsByNode(cmds []Cmder, cmdsMap *cmdsMap) error { +func (c *ClusterClient) mapCmdsByNode(cmdsMap *cmdsMap, cmds []Cmder) error { state, err := c.state.Get() if err != nil { - setCmdsErr(cmds, err) return err } - cmdsAreReadOnly := c.cmdsAreReadOnly(cmds) - for _, cmd := range cmds { - var node *clusterNode - var err error - if cmdsAreReadOnly { - _, node, err = c.cmdSlotAndNode(cmd) - } else { + if c.opt.ReadOnly && c.cmdsAreReadOnly(cmds) { + for _, cmd := range cmds { slot := c.cmdSlot(cmd) - node, err = state.slotMasterNode(slot) + node, err := c.slotReadOnlyNode(state, slot) + if err != nil { + return err + } + cmdsMap.Add(node, cmd) } + return nil + } + + for _, cmd := range cmds { + slot := c.cmdSlot(cmd) + node, err := state.slotMasterNode(slot) if err != nil { return err } - cmdsMap.mu.Lock() - cmdsMap.m[node] = append(cmdsMap.m[node], cmd) - cmdsMap.mu.Unlock() + cmdsMap.Add(node, cmd) } return nil } @@ -1294,94 +1146,83 @@ func (c *ClusterClient) cmdsAreReadOnly(cmds []Cmder) bool { return true } -func (c *ClusterClient) pipelineProcessCmds( - node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap, +func (c *ClusterClient) _processPipelineNode( + ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap, ) error { - err := cn.WithWriter(c.opt.WriteTimeout, func(wr *proto.Writer) error { - return writeCmd(wr, cmds...) - }) - if err != nil { - setCmdsErr(cmds, err) - failedCmds.mu.Lock() - failedCmds.m[node] = cmds - failedCmds.mu.Unlock() - return err - } + return node.Client.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error { + return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error { + err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error { + return writeCmds(wr, cmds) + }) + if err != nil { + return err + } - err = cn.WithReader(c.opt.ReadTimeout, func(rd *proto.Reader) error { - return c.pipelineReadCmds(node, rd, cmds, failedCmds) + return cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error { + return c.pipelineReadCmds(node, rd, cmds, failedCmds) + }) + }) }) - return err } func (c *ClusterClient) pipelineReadCmds( node *clusterNode, rd *proto.Reader, cmds []Cmder, failedCmds *cmdsMap, ) error { - var firstErr error for _, cmd := range cmds { err := cmd.readReply(rd) if err == nil { continue } - if c.checkMovedErr(cmd, err, failedCmds) { continue } - if internal.IsRedisError(err) { + if c.opt.ReadOnly && isLoadingError(err) { + node.MarkAsFailing() + return err + } + if isRedisError(err) { continue } - - failedCmds.mu.Lock() - failedCmds.m[node] = append(failedCmds.m[node], cmd) - failedCmds.mu.Unlock() - if firstErr == nil { - firstErr = err - } + return err } - return firstErr + return nil } func (c *ClusterClient) checkMovedErr( cmd Cmder, err error, failedCmds *cmdsMap, ) bool { - moved, ask, addr := internal.IsMovedError(err) + moved, ask, addr := isMovedError(err) + if !moved && !ask { + return false + } + + node, err := c.nodes.Get(addr) + if err != nil { + return false + } if moved { c.state.LazyReload() - - node, err := c.nodes.GetOrCreate(addr) - if err != nil { - return false - } - - failedCmds.mu.Lock() - failedCmds.m[node] = append(failedCmds.m[node], cmd) - failedCmds.mu.Unlock() + failedCmds.Add(node, cmd) return true } if ask { - node, err := c.nodes.GetOrCreate(addr) - if err != nil { - return false - } - - failedCmds.mu.Lock() - failedCmds.m[node] = append(failedCmds.m[node], NewCmd("ASKING"), cmd) - failedCmds.mu.Unlock() + failedCmds.Add(node, NewCmd("asking"), cmd) return true } - return false + panic("not reached") } // TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC. func (c *ClusterClient) TxPipeline() Pipeliner { pipe := Pipeline{ + ctx: c.ctx, exec: c.processTxPipeline, } - pipe.statefulCmdable.setProcessor(pipe.Process) + pipe.init() return &pipe } @@ -1389,9 +1230,14 @@ func (c *ClusterClient) TxPipelined(fn func(Pipeliner) error) ([]Cmder, error) { return c.TxPipeline().Pipelined(fn) } -func (c *ClusterClient) defaultProcessTxPipeline(cmds []Cmder) error { +func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) error { + return c.hooks.processPipeline(ctx, cmds, c._processTxPipeline) +} + +func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) error { state, err := c.state.Get() if err != nil { + setCmdsErr(cmds, err) return err } @@ -1402,11 +1248,14 @@ func (c *ClusterClient) defaultProcessTxPipeline(cmds []Cmder) error { setCmdsErr(cmds, err) continue } - cmdsMap := map[*clusterNode][]Cmder{node: cmds} + cmdsMap := map[*clusterNode][]Cmder{node: cmds} for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { if attempt > 0 { - time.Sleep(c.retryBackoff(attempt)) + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + setCmdsErr(cmds, err) + return err + } } failedCmds := newCmdsMap() @@ -1417,18 +1266,17 @@ func (c *ClusterClient) defaultProcessTxPipeline(cmds []Cmder) error { go func(node *clusterNode, cmds []Cmder) { defer wg.Done() - cn, err := node.Client.getConn() - if err != nil { - if err == pool.ErrClosed { - c.mapCmdsByNode(cmds, failedCmds) - } else { - setCmdsErr(cmds, err) - } + err := c._processTxPipelineNode(ctx, node, cmds, failedCmds) + if err == nil { return } - - err = c.txPipelineProcessCmds(node, cn, cmds, failedCmds) - node.Client.releaseConnStrict(cn, err) + if attempt < c.opt.MaxRedirects { + if err := c.mapCmdsByNode(failedCmds, cmds); err != nil { + setCmdsErr(cmds, err) + } + } else { + setCmdsErr(cmds, err) + } }(node, cmds) } @@ -1452,50 +1300,51 @@ func (c *ClusterClient) mapCmdsBySlot(cmds []Cmder) map[int][]Cmder { return cmdsMap } -func (c *ClusterClient) txPipelineProcessCmds( - node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap, +func (c *ClusterClient) _processTxPipelineNode( + ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap, ) error { - err := cn.WithWriter(c.opt.WriteTimeout, func(wr *proto.Writer) error { - return txPipelineWriteMulti(wr, cmds) - }) - if err != nil { - setCmdsErr(cmds, err) - failedCmds.mu.Lock() - failedCmds.m[node] = cmds - failedCmds.mu.Unlock() - return err - } + return node.Client.hooks.processTxPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error { + return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error { + err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error { + return writeCmds(wr, cmds) + }) + if err != nil { + return err + } - err = cn.WithReader(c.opt.ReadTimeout, func(rd *proto.Reader) error { - err := c.txPipelineReadQueued(rd, cmds, failedCmds) - if err != nil { - setCmdsErr(cmds, err) - return err - } - return pipelineReadCmds(rd, cmds) + return cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error { + statusCmd := cmds[0].(*StatusCmd) + // Trim multi and exec. + cmds = cmds[1 : len(cmds)-1] + + err := c.txPipelineReadQueued(rd, statusCmd, cmds, failedCmds) + if err != nil { + moved, ask, addr := isMovedError(err) + if moved || ask { + return c.cmdsMoved(cmds, moved, ask, addr, failedCmds) + } + return err + } + + return pipelineReadCmds(rd, cmds) + }) + }) }) - return err } func (c *ClusterClient) txPipelineReadQueued( - rd *proto.Reader, cmds []Cmder, failedCmds *cmdsMap, + rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder, failedCmds *cmdsMap, ) error { // Parse queued replies. - var statusCmd StatusCmd if err := statusCmd.readReply(rd); err != nil { return err } for _, cmd := range cmds { err := statusCmd.readReply(rd) - if err == nil { + if err == nil || c.checkMovedErr(cmd, err, failedCmds) || isRedisError(err) { continue } - - if c.checkMovedErr(cmd, err, failedCmds) || internal.IsRedisError(err) { - continue - } - return err } @@ -1510,23 +1359,106 @@ func (c *ClusterClient) txPipelineReadQueued( switch line[0] { case proto.ErrorReply: - err := proto.ParseErrorReply(line) - for _, cmd := range cmds { - if !c.checkMovedErr(cmd, err, failedCmds) { - break - } - } - return err + return proto.ParseErrorReply(line) case proto.ArrayReply: // ok default: - err := fmt.Errorf("redis: expected '*', but got line %q", line) - return err + return fmt.Errorf("redis: expected '*', but got line %q", line) } return nil } +func (c *ClusterClient) cmdsMoved( + cmds []Cmder, moved, ask bool, addr string, failedCmds *cmdsMap, +) error { + node, err := c.nodes.Get(addr) + if err != nil { + return err + } + + if moved { + c.state.LazyReload() + for _, cmd := range cmds { + failedCmds.Add(node, cmd) + } + return nil + } + + if ask { + for _, cmd := range cmds { + failedCmds.Add(node, NewCmd("asking"), cmd) + } + return nil + } + + return nil +} + +func (c *ClusterClient) Watch(fn func(*Tx) error, keys ...string) error { + return c.WatchContext(c.ctx, fn, keys...) +} + +func (c *ClusterClient) WatchContext(ctx context.Context, fn func(*Tx) error, keys ...string) error { + if len(keys) == 0 { + return fmt.Errorf("redis: Watch requires at least one key") + } + + slot := hashtag.Slot(keys[0]) + for _, key := range keys[1:] { + if hashtag.Slot(key) != slot { + err := fmt.Errorf("redis: Watch requires all keys to be in the same slot") + return err + } + } + + node, err := c.slotMasterNode(slot) + if err != nil { + return err + } + + for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + if attempt > 0 { + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + return err + } + } + + err = node.Client.WatchContext(ctx, fn, keys...) + if err == nil { + break + } + if err != Nil { + c.state.LazyReload() + } + + moved, ask, addr := isMovedError(err) + if moved || ask { + node, err = c.nodes.Get(addr) + if err != nil { + return err + } + continue + } + + if err == pool.ErrClosed || isReadOnlyError(err) { + node, err = c.slotMasterNode(slot) + if err != nil { + return err + } + continue + } + + if isRetryableError(err, true) { + continue + } + + return err + } + + return err +} + func (c *ClusterClient) pubSub() *PubSub { var node *clusterNode pubsub := &PubSub{ @@ -1537,16 +1469,21 @@ func (c *ClusterClient) pubSub() *PubSub { panic("node != nil") } - slot := hashtag.Slot(channels[0]) - var err error - node, err = c.slotMasterNode(slot) + if len(channels) > 0 { + slot := hashtag.Slot(channels[0]) + node, err = c.slotMasterNode(slot) + } else { + node, err = c.nodes.Random() + } if err != nil { return nil, err } - cn, err := node.Client.newConn() + cn, err := node.Client.newConn(context.TODO()) if err != nil { + node = nil + return nil, err } @@ -1583,6 +1520,98 @@ func (c *ClusterClient) PSubscribe(channels ...string) *PubSub { return pubsub } +func (c *ClusterClient) retryBackoff(attempt int) time.Duration { + return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff) +} + +func (c *ClusterClient) cmdsInfo() (map[string]*CommandInfo, error) { + addrs, err := c.nodes.Addrs() + if err != nil { + return nil, err + } + + var firstErr error + for _, addr := range addrs { + node, err := c.nodes.Get(addr) + if err != nil { + return nil, err + } + if node == nil { + continue + } + + info, err := node.Client.Command().Result() + if err == nil { + return info, nil + } + if firstErr == nil { + firstErr = err + } + } + return nil, firstErr +} + +func (c *ClusterClient) cmdInfo(name string) *CommandInfo { + cmdsInfo, err := c.cmdsInfoCache.Get() + if err != nil { + return nil + } + + info := cmdsInfo[name] + if info == nil { + internal.Logger.Printf("info for cmd=%s not found", name) + } + return info +} + +func (c *ClusterClient) cmdSlot(cmd Cmder) int { + args := cmd.Args() + if args[0] == "cluster" && args[1] == "getkeysinslot" { + return args[2].(int) + } + + cmdInfo := c.cmdInfo(cmd.Name()) + return cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo)) +} + +func cmdSlot(cmd Cmder, pos int) int { + if pos == 0 { + return hashtag.RandomSlot() + } + firstKey := cmd.stringArg(pos) + return hashtag.Slot(firstKey) +} + +func (c *ClusterClient) cmdNode(cmdInfo *CommandInfo, slot int) (*clusterNode, error) { + state, err := c.state.Get() + if err != nil { + return nil, err + } + + if c.opt.ReadOnly && cmdInfo != nil && cmdInfo.ReadOnly { + return c.slotReadOnlyNode(state, slot) + } + return state.slotMasterNode(slot) +} + +func (c *clusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) { + if c.opt.RouteByLatency { + return state.slotClosestNode(slot) + } + if c.opt.RouteRandomly { + return state.slotRandomNode(slot) + } + return state.slotSlaveNode(slot) +} + +func (c *ClusterClient) slotMasterNode(slot int) (*clusterNode, error) { + state, err := c.state.Get() + if err != nil { + return nil, err + } + return state.slotMasterNode(slot) +} + func appendUniqueNode(nodes []*clusterNode, node *clusterNode) []*clusterNode { for _, n := range nodes { if n == node { @@ -1619,3 +1648,22 @@ func remove(ss []string, es ...string) []string { } return ss } + +//------------------------------------------------------------------------------ + +type cmdsMap struct { + mu sync.Mutex + m map[*clusterNode][]Cmder +} + +func newCmdsMap() *cmdsMap { + return &cmdsMap{ + m: make(map[*clusterNode][]Cmder), + } +} + +func (m *cmdsMap) Add(node *clusterNode, cmds ...Cmder) { + m.mu.Lock() + m.m[node] = append(m.m[node], cmds...) + m.mu.Unlock() +} diff --git a/vendor/github.com/go-redis/redis/cluster_commands.go b/vendor/github.com/go-redis/redis/v7/cluster_commands.go similarity index 95% rename from vendor/github.com/go-redis/redis/cluster_commands.go rename to vendor/github.com/go-redis/redis/v7/cluster_commands.go index dff62c902..c9b9b9de2 100644 --- a/vendor/github.com/go-redis/redis/cluster_commands.go +++ b/vendor/github.com/go-redis/redis/v7/cluster_commands.go @@ -14,7 +14,7 @@ func (c *ClusterClient) DBSize() *IntCmd { return nil }) if err != nil { - cmd.setErr(err) + cmd.SetErr(err) return cmd } cmd.val = size diff --git a/vendor/github.com/go-redis/redis/command.go b/vendor/github.com/go-redis/redis/v7/command.go similarity index 66% rename from vendor/github.com/go-redis/redis/command.go rename to vendor/github.com/go-redis/redis/v7/command.go index cb4f94b12..dd7fe4a91 100644 --- a/vendor/github.com/go-redis/redis/command.go +++ b/vendor/github.com/go-redis/redis/v7/command.go @@ -7,27 +7,28 @@ import ( "strings" "time" - "github.com/go-redis/redis/internal" - "github.com/go-redis/redis/internal/proto" + "github.com/go-redis/redis/v7/internal" + "github.com/go-redis/redis/v7/internal/proto" + "github.com/go-redis/redis/v7/internal/util" ) type Cmder interface { Name() string Args() []interface{} + String() string stringArg(int) string - readReply(rd *proto.Reader) error - setErr(error) - readTimeout() *time.Duration + readReply(rd *proto.Reader) error + SetErr(error) Err() error } func setCmdsErr(cmds []Cmder, e error) { for _, cmd := range cmds { if cmd.Err() == nil { - cmd.setErr(e) + cmd.SetErr(e) } } } @@ -41,18 +42,21 @@ func cmdsFirstErr(cmds []Cmder) error { return nil } -func writeCmd(wr *proto.Writer, cmds ...Cmder) error { +func writeCmds(wr *proto.Writer, cmds []Cmder) error { for _, cmd := range cmds { - err := wr.WriteArgs(cmd.Args()) - if err != nil { + if err := writeCmd(wr, cmd); err != nil { return err } } return nil } +func writeCmd(wr *proto.Writer, cmd Cmder) error { + return wr.WriteArgs(cmd.Args()) +} + func cmdString(cmd Cmder, val interface{}) string { - var ss []string + ss := make([]string, 0, len(cmd.Args())) for _, arg := range cmd.Args() { ss = append(ss, fmt.Sprint(arg)) } @@ -69,7 +73,6 @@ func cmdString(cmd Cmder, val interface{}) string { } } return s - } func cmdFirstKeyPos(cmd Cmder, info *CommandInfo) int { @@ -92,38 +95,40 @@ func cmdFirstKeyPos(cmd Cmder, info *CommandInfo) int { //------------------------------------------------------------------------------ type baseCmd struct { - _args []interface{} - err error + args []interface{} + err error _readTimeout *time.Duration } var _ Cmder = (*Cmd)(nil) -func (cmd *baseCmd) Err() error { - return cmd.err +func (cmd *baseCmd) Name() string { + if len(cmd.args) == 0 { + return "" + } + // Cmd name must be lower cased. + return internal.ToLower(cmd.stringArg(0)) } func (cmd *baseCmd) Args() []interface{} { - return cmd._args + return cmd.args } func (cmd *baseCmd) stringArg(pos int) string { - if pos < 0 || pos >= len(cmd._args) { + if pos < 0 || pos >= len(cmd.args) { return "" } - s, _ := cmd._args[pos].(string) + s, _ := cmd.args[pos].(string) return s } -func (cmd *baseCmd) Name() string { - if len(cmd._args) > 0 { - // Cmd name must be lower cased. - s := internal.ToLower(cmd.stringArg(0)) - cmd._args[0] = s - return s - } - return "" +func (cmd *baseCmd) SetErr(e error) { + cmd.err = e +} + +func (cmd *baseCmd) Err() error { + return cmd.err } func (cmd *baseCmd) readTimeout() *time.Duration { @@ -134,10 +139,6 @@ func (cmd *baseCmd) setReadTimeout(d time.Duration) { cmd._readTimeout = &d } -func (cmd *baseCmd) setErr(e error) { - cmd.err = e -} - //------------------------------------------------------------------------------ type Cmd struct { @@ -148,10 +149,14 @@ type Cmd struct { func NewCmd(args ...interface{}) *Cmd { return &Cmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } +func (cmd *Cmd) String() string { + return cmdString(cmd, cmd.val) +} + func (cmd *Cmd) Val() interface{} { return cmd.val } @@ -160,7 +165,7 @@ func (cmd *Cmd) Result() (interface{}, error) { return cmd.val, cmd.err } -func (cmd *Cmd) String() (string, error) { +func (cmd *Cmd) Text() (string, error) { if cmd.err != nil { return "", cmd.err } @@ -218,6 +223,25 @@ func (cmd *Cmd) Uint64() (uint64, error) { } } +func (cmd *Cmd) Float32() (float32, error) { + if cmd.err != nil { + return 0, cmd.err + } + switch val := cmd.val.(type) { + case int64: + return float32(val), nil + case string: + f, err := strconv.ParseFloat(val, 32) + if err != nil { + return 0, err + } + return float32(f), nil + default: + err := fmt.Errorf("redis: unexpected type=%T for Float32", val) + return 0, err + } +} + func (cmd *Cmd) Float64() (float64, error) { if cmd.err != nil { return 0, cmd.err @@ -255,27 +279,21 @@ func (cmd *Cmd) readReply(rd *proto.Reader) error { // Implements proto.MultiBulkParse func sliceParser(rd *proto.Reader, n int64) (interface{}, error) { - vals := make([]interface{}, 0, n) - for i := int64(0); i < n; i++ { + vals := make([]interface{}, n) + for i := 0; i < len(vals); i++ { v, err := rd.ReadReply(sliceParser) if err != nil { if err == Nil { - vals = append(vals, nil) + vals[i] = nil continue } if err, ok := err.(proto.RedisError); ok { - vals = append(vals, err) + vals[i] = err continue } return nil, err } - - switch v := v.(type) { - case string: - vals = append(vals, v) - default: - vals = append(vals, v) - } + vals[i] = v } return vals, nil } @@ -292,7 +310,7 @@ var _ Cmder = (*SliceCmd)(nil) func NewSliceCmd(args ...interface{}) *SliceCmd { return &SliceCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -330,7 +348,7 @@ var _ Cmder = (*StatusCmd)(nil) func NewStatusCmd(args ...interface{}) *StatusCmd { return &StatusCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -363,7 +381,7 @@ var _ Cmder = (*IntCmd)(nil) func NewIntCmd(args ...interface{}) *IntCmd { return &IntCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -375,6 +393,10 @@ func (cmd *IntCmd) Result() (int64, error) { return cmd.val, cmd.err } +func (cmd *IntCmd) Uint64() (uint64, error) { + return uint64(cmd.val), cmd.err +} + func (cmd *IntCmd) String() string { return cmdString(cmd, cmd.val) } @@ -386,6 +408,49 @@ func (cmd *IntCmd) readReply(rd *proto.Reader) error { //------------------------------------------------------------------------------ +type IntSliceCmd struct { + baseCmd + + val []int64 +} + +var _ Cmder = (*IntSliceCmd)(nil) + +func NewIntSliceCmd(args ...interface{}) *IntSliceCmd { + return &IntSliceCmd{ + baseCmd: baseCmd{args: args}, + } +} + +func (cmd *IntSliceCmd) Val() []int64 { + return cmd.val +} + +func (cmd *IntSliceCmd) Result() ([]int64, error) { + return cmd.val, cmd.err +} + +func (cmd *IntSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *IntSliceCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]int64, n) + for i := 0; i < len(cmd.val); i++ { + num, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.val[i] = num + } + return nil, nil + }) + return cmd.err +} + +//------------------------------------------------------------------------------ + type DurationCmd struct { baseCmd @@ -397,7 +462,7 @@ var _ Cmder = (*DurationCmd)(nil) func NewDurationCmd(precision time.Duration, args ...interface{}) *DurationCmd { return &DurationCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, precision: precision, } } @@ -420,7 +485,14 @@ func (cmd *DurationCmd) readReply(rd *proto.Reader) error { if cmd.err != nil { return cmd.err } - cmd.val = time.Duration(n) * cmd.precision + switch n { + // -2 if the key does not exist + // -1 if the key exists but has no associated expire + case -2, -1: + cmd.val = time.Duration(n) + default: + cmd.val = time.Duration(n) * cmd.precision + } return nil } @@ -436,7 +508,7 @@ var _ Cmder = (*TimeCmd)(nil) func NewTimeCmd(args ...interface{}) *TimeCmd { return &TimeCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -453,32 +525,25 @@ func (cmd *TimeCmd) String() string { } func (cmd *TimeCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(timeParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.(time.Time) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 2 { + return nil, fmt.Errorf("got %d elements, expected 2", n) + } -// Implements proto.MultiBulkParse -func timeParser(rd *proto.Reader, n int64) (interface{}, error) { - if n != 2 { - return nil, fmt.Errorf("got %d elements, expected 2", n) - } + sec, err := rd.ReadInt() + if err != nil { + return nil, err + } - sec, err := rd.ReadInt() - if err != nil { - return nil, err - } + microsec, err := rd.ReadInt() + if err != nil { + return nil, err + } - microsec, err := rd.ReadInt() - if err != nil { - return nil, err - } - - return time.Unix(sec, microsec*1000), nil + cmd.val = time.Unix(sec, microsec*1000) + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -493,7 +558,7 @@ var _ Cmder = (*BoolCmd)(nil) func NewBoolCmd(args ...interface{}) *BoolCmd { return &BoolCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -514,7 +579,6 @@ func (cmd *BoolCmd) readReply(rd *proto.Reader) error { v, cmd.err = rd.ReadReply(nil) // `SET key value NX` returns nil when key already exists. But // `SETNX key value` returns bool (0/1). So convert nil to bool. - // TODO: is this okay? if cmd.err == Nil { cmd.val = false cmd.err = nil @@ -548,7 +612,7 @@ var _ Cmder = (*StringCmd)(nil) func NewStringCmd(args ...interface{}) *StringCmd { return &StringCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -561,7 +625,7 @@ func (cmd *StringCmd) Result() (string, error) { } func (cmd *StringCmd) Bytes() ([]byte, error) { - return []byte(cmd.val), cmd.err + return util.StringToBytes(cmd.val), cmd.err } func (cmd *StringCmd) Int() (int, error) { @@ -585,6 +649,17 @@ func (cmd *StringCmd) Uint64() (uint64, error) { return strconv.ParseUint(cmd.Val(), 10, 64) } +func (cmd *StringCmd) Float32() (float32, error) { + if cmd.err != nil { + return 0, cmd.err + } + f, err := strconv.ParseFloat(cmd.Val(), 32) + if err != nil { + return 0, err + } + return float32(f), nil +} + func (cmd *StringCmd) Float64() (float64, error) { if cmd.err != nil { return 0, cmd.err @@ -592,6 +667,13 @@ func (cmd *StringCmd) Float64() (float64, error) { return strconv.ParseFloat(cmd.Val(), 64) } +func (cmd *StringCmd) Time() (time.Time, error) { + if cmd.err != nil { + return time.Time{}, cmd.err + } + return time.Parse(time.RFC3339Nano, cmd.Val()) +} + func (cmd *StringCmd) Scan(val interface{}) error { if cmd.err != nil { return cmd.err @@ -620,7 +702,7 @@ var _ Cmder = (*FloatCmd)(nil) func NewFloatCmd(args ...interface{}) *FloatCmd { return &FloatCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -653,7 +735,7 @@ var _ Cmder = (*StringSliceCmd)(nil) func NewStringSliceCmd(args ...interface{}) *StringSliceCmd { return &StringSliceCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -674,29 +756,21 @@ func (cmd *StringSliceCmd) ScanSlice(container interface{}) error { } func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(stringSliceParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.([]string) - return nil -} - -// Implements proto.MultiBulkParse -func stringSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - ss := make([]string, 0, n) - for i := int64(0); i < n; i++ { - s, err := rd.ReadString() - if err == Nil { - ss = append(ss, "") - } else if err != nil { - return nil, err - } else { - ss = append(ss, s) + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]string, n) + for i := 0; i < len(cmd.val); i++ { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.val[i] = "" + case err != nil: + return nil, err + default: + cmd.val[i] = s + } } - } - return ss, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -711,7 +785,7 @@ var _ Cmder = (*BoolSliceCmd)(nil) func NewBoolSliceCmd(args ...interface{}) *BoolSliceCmd { return &BoolSliceCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -728,26 +802,18 @@ func (cmd *BoolSliceCmd) String() string { } func (cmd *BoolSliceCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(boolSliceParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.([]bool) - return nil -} - -// Implements proto.MultiBulkParse -func boolSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - bools := make([]bool, 0, n) - for i := int64(0); i < n; i++ { - n, err := rd.ReadIntReply() - if err != nil { - return nil, err + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]bool, n) + for i := 0; i < len(cmd.val); i++ { + n, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + cmd.val[i] = n == 1 } - bools = append(bools, n == 1) - } - return bools, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -762,7 +828,7 @@ var _ Cmder = (*StringStringMapCmd)(nil) func NewStringStringMapCmd(args ...interface{}) *StringStringMapCmd { return &StringStringMapCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -779,32 +845,24 @@ func (cmd *StringStringMapCmd) String() string { } func (cmd *StringStringMapCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(stringStringMapParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.(map[string]string) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]string, n/2) + for i := int64(0); i < n; i += 2 { + key, err := rd.ReadString() + if err != nil { + return nil, err + } -// Implements proto.MultiBulkParse -func stringStringMapParser(rd *proto.Reader, n int64) (interface{}, error) { - m := make(map[string]string, n/2) - for i := int64(0); i < n; i += 2 { - key, err := rd.ReadString() - if err != nil { - return nil, err + value, err := rd.ReadString() + if err != nil { + return nil, err + } + + cmd.val[key] = value } - - value, err := rd.ReadString() - if err != nil { - return nil, err - } - - m[key] = value - } - return m, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -819,7 +877,7 @@ var _ Cmder = (*StringIntMapCmd)(nil) func NewStringIntMapCmd(args ...interface{}) *StringIntMapCmd { return &StringIntMapCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -836,32 +894,24 @@ func (cmd *StringIntMapCmd) String() string { } func (cmd *StringIntMapCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(stringIntMapParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.(map[string]int64) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]int64, n/2) + for i := int64(0); i < n; i += 2 { + key, err := rd.ReadString() + if err != nil { + return nil, err + } -// Implements proto.MultiBulkParse -func stringIntMapParser(rd *proto.Reader, n int64) (interface{}, error) { - m := make(map[string]int64, n/2) - for i := int64(0); i < n; i += 2 { - key, err := rd.ReadString() - if err != nil { - return nil, err + n, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + + cmd.val[key] = n } - - n, err := rd.ReadIntReply() - if err != nil { - return nil, err - } - - m[key] = n - } - return m, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -876,7 +926,7 @@ var _ Cmder = (*StringStructMapCmd)(nil) func NewStringStructMapCmd(args ...interface{}) *StringStructMapCmd { return &StringStructMapCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -893,27 +943,18 @@ func (cmd *StringStructMapCmd) String() string { } func (cmd *StringStructMapCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(stringStructMapParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.(map[string]struct{}) - return nil -} - -// Implements proto.MultiBulkParse -func stringStructMapParser(rd *proto.Reader, n int64) (interface{}, error) { - m := make(map[string]struct{}, n) - for i := int64(0); i < n; i++ { - key, err := rd.ReadString() - if err != nil { - return nil, err + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]struct{}, n) + for i := int64(0); i < n; i++ { + key, err := rd.ReadString() + if err != nil { + return nil, err + } + cmd.val[key] = struct{}{} } - - m[key] = struct{}{} - } - return m, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -933,7 +974,7 @@ var _ Cmder = (*XMessageSliceCmd)(nil) func NewXMessageSliceCmd(args ...interface{}) *XMessageSliceCmd { return &XMessageSliceCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -961,23 +1002,30 @@ func (cmd *XMessageSliceCmd) readReply(rd *proto.Reader) error { // Implements proto.MultiBulkParse func xMessageSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - msgs := make([]XMessage, 0, n) - for i := int64(0); i < n; i++ { + msgs := make([]XMessage, n) + for i := 0; i < len(msgs); i++ { + i := i _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { id, err := rd.ReadString() if err != nil { return nil, err } + var values map[string]interface{} + v, err := rd.ReadArrayReply(stringInterfaceMapParser) if err != nil { - return nil, err + if err != proto.Nil { + return nil, err + } + } else { + values = v.(map[string]interface{}) } - msgs = append(msgs, XMessage{ + msgs[i] = XMessage{ ID: id, - Values: v.(map[string]interface{}), - }) + Values: values, + } return nil, nil }) if err != nil { @@ -1023,7 +1071,7 @@ var _ Cmder = (*XStreamSliceCmd)(nil) func NewXStreamSliceCmd(args ...interface{}) *XStreamSliceCmd { return &XStreamSliceCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -1040,45 +1088,38 @@ func (cmd *XStreamSliceCmd) String() string { } func (cmd *XStreamSliceCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(xStreamSliceParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.([]XStream) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]XStream, n) + for i := 0; i < len(cmd.val); i++ { + i := i + _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 2 { + return nil, fmt.Errorf("got %d, wanted 2", n) + } -// Implements proto.MultiBulkParse -func xStreamSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - ret := make([]XStream, 0, n) - for i := int64(0); i < n; i++ { - _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { - if n != 2 { - return nil, fmt.Errorf("got %d, wanted 2", n) - } + stream, err := rd.ReadString() + if err != nil { + return nil, err + } - stream, err := rd.ReadString() - if err != nil { - return nil, err - } + v, err := rd.ReadArrayReply(xMessageSliceParser) + if err != nil { + return nil, err + } - v, err := rd.ReadArrayReply(xMessageSliceParser) - if err != nil { - return nil, err - } - - ret = append(ret, XStream{ - Stream: stream, - Messages: v.([]XMessage), + cmd.val[i] = XStream{ + Stream: stream, + Messages: v.([]XMessage), + } + return nil, nil }) - return nil, nil - }) - if err != nil { - return nil, err + if err != nil { + return nil, err + } } - } - return ret, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -1099,7 +1140,7 @@ var _ Cmder = (*XPendingCmd)(nil) func NewXPendingCmd(args ...interface{}) *XPendingCmd { return &XPendingCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -1116,81 +1157,74 @@ func (cmd *XPendingCmd) String() string { } func (cmd *XPendingCmd) readReply(rd *proto.Reader) error { - var info interface{} - info, cmd.err = rd.ReadArrayReply(xPendingParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = info.(*XPending) - return nil -} - -func xPendingParser(rd *proto.Reader, n int64) (interface{}, error) { - if n != 4 { - return nil, fmt.Errorf("got %d, wanted 4", n) - } - - count, err := rd.ReadIntReply() - if err != nil { - return nil, err - } - - lower, err := rd.ReadString() - if err != nil && err != Nil { - return nil, err - } - - higher, err := rd.ReadString() - if err != nil && err != Nil { - return nil, err - } - - pending := &XPending{ - Count: count, - Lower: lower, - Higher: higher, - } - _, err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { - for i := int64(0); i < n; i++ { - _, err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { - if n != 2 { - return nil, fmt.Errorf("got %d, wanted 2", n) - } - - consumerName, err := rd.ReadString() - if err != nil { - return nil, err - } - - consumerPending, err := rd.ReadInt() - if err != nil { - return nil, err - } - - if pending.Consumers == nil { - pending.Consumers = make(map[string]int64) - } - pending.Consumers[consumerName] = consumerPending - - return nil, nil - }) - if err != nil { - return nil, err - } + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 4 { + return nil, fmt.Errorf("got %d, wanted 4", n) } + + count, err := rd.ReadIntReply() + if err != nil { + return nil, err + } + + lower, err := rd.ReadString() + if err != nil && err != Nil { + return nil, err + } + + higher, err := rd.ReadString() + if err != nil && err != Nil { + return nil, err + } + + cmd.val = &XPending{ + Count: count, + Lower: lower, + Higher: higher, + } + _, err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + for i := int64(0); i < n; i++ { + _, err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 2 { + return nil, fmt.Errorf("got %d, wanted 2", n) + } + + consumerName, err := rd.ReadString() + if err != nil { + return nil, err + } + + consumerPending, err := rd.ReadInt() + if err != nil { + return nil, err + } + + if cmd.val.Consumers == nil { + cmd.val.Consumers = make(map[string]int64) + } + cmd.val.Consumers[consumerName] = consumerPending + + return nil, nil + }) + if err != nil { + return nil, err + } + } + return nil, nil + }) + if err != nil && err != Nil { + return nil, err + } + return nil, nil }) - if err != nil && err != Nil { - return nil, err - } - - return pending, nil + return cmd.err } //------------------------------------------------------------------------------ type XPendingExt struct { - Id string + ID string Consumer string Idle time.Duration RetryCount int64 @@ -1205,7 +1239,7 @@ var _ Cmder = (*XPendingExtCmd)(nil) func NewXPendingExtCmd(args ...interface{}) *XPendingExtCmd { return &XPendingExtCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -1222,62 +1256,143 @@ func (cmd *XPendingExtCmd) String() string { } func (cmd *XPendingExtCmd) readReply(rd *proto.Reader) error { - var info interface{} - info, cmd.err = rd.ReadArrayReply(xPendingExtSliceParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = info.([]XPendingExt) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]XPendingExt, 0, n) + for i := int64(0); i < n; i++ { + _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 4 { + return nil, fmt.Errorf("got %d, wanted 4", n) + } -func xPendingExtSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - ret := make([]XPendingExt, 0, n) - for i := int64(0); i < n; i++ { - _, err := rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { - if n != 4 { - return nil, fmt.Errorf("got %d, wanted 4", n) - } + id, err := rd.ReadString() + if err != nil { + return nil, err + } - id, err := rd.ReadString() + consumer, err := rd.ReadString() + if err != nil && err != Nil { + return nil, err + } + + idle, err := rd.ReadIntReply() + if err != nil && err != Nil { + return nil, err + } + + retryCount, err := rd.ReadIntReply() + if err != nil && err != Nil { + return nil, err + } + + cmd.val = append(cmd.val, XPendingExt{ + ID: id, + Consumer: consumer, + Idle: time.Duration(idle) * time.Millisecond, + RetryCount: retryCount, + }) + return nil, nil + }) if err != nil { return nil, err } + } + return nil, nil + }) + return cmd.err +} - consumer, err := rd.ReadString() - if err != nil && err != Nil { - return nil, err +//------------------------------------------------------------------------------ + +type XInfoGroupsCmd struct { + baseCmd + val []XInfoGroups +} + +type XInfoGroups struct { + Name string + Consumers int64 + Pending int64 + LastDeliveredID string +} + +var _ Cmder = (*XInfoGroupsCmd)(nil) + +func NewXInfoGroupsCmd(stream string) *XInfoGroupsCmd { + return &XInfoGroupsCmd{ + baseCmd: baseCmd{args: []interface{}{"xinfo", "groups", stream}}, + } +} + +func (cmd *XInfoGroupsCmd) Val() []XInfoGroups { + return cmd.val +} + +func (cmd *XInfoGroupsCmd) Result() ([]XInfoGroups, error) { + return cmd.val, cmd.err +} + +func (cmd *XInfoGroupsCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error { + _, cmd.err = rd.ReadArrayReply( + func(rd *proto.Reader, n int64) (interface{}, error) { + for i := int64(0); i < n; i++ { + v, err := rd.ReadReply(xGroupInfoParser) + if err != nil { + return nil, err + } + cmd.val = append(cmd.val, v.(XInfoGroups)) } - - idle, err := rd.ReadIntReply() - if err != nil && err != Nil { - return nil, err - } - - retryCount, err := rd.ReadIntReply() - if err != nil && err != Nil { - return nil, err - } - - ret = append(ret, XPendingExt{ - Id: id, - Consumer: consumer, - Idle: time.Duration(idle) * time.Millisecond, - RetryCount: retryCount, - }) return nil, nil }) + return nil +} + +func xGroupInfoParser(rd *proto.Reader, n int64) (interface{}, error) { + if n != 8 { + return nil, fmt.Errorf("redis: got %d elements in XINFO GROUPS reply,"+ + "wanted 8", n) + } + var ( + err error + grp XInfoGroups + key string + val string + ) + + for i := 0; i < 4; i++ { + key, err = rd.ReadString() + if err != nil { + return nil, err + } + val, err = rd.ReadString() + if err != nil { + return nil, err + } + switch key { + case "name": + grp.Name = val + case "consumers": + grp.Consumers, err = strconv.ParseInt(val, 0, 64) + case "pending": + grp.Pending, err = strconv.ParseInt(val, 0, 64) + case "last-delivered-id": + grp.LastDeliveredID = val + default: + return nil, fmt.Errorf("redis: unexpected content %s "+ + "in XINFO GROUPS reply", key) + } if err != nil { return nil, err } } - return ret, nil + return grp, err } //------------------------------------------------------------------------------ -//------------------------------------------------------------------------------ - type ZSliceCmd struct { baseCmd @@ -1288,7 +1403,7 @@ var _ Cmder = (*ZSliceCmd)(nil) func NewZSliceCmd(args ...interface{}) *ZSliceCmd { return &ZSliceCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -1305,34 +1420,27 @@ func (cmd *ZSliceCmd) String() string { } func (cmd *ZSliceCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(zSliceParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.([]Z) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]Z, n/2) + for i := 0; i < len(cmd.val); i++ { + member, err := rd.ReadString() + if err != nil { + return nil, err + } -// Implements proto.MultiBulkParse -func zSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - zz := make([]Z, n/2) - for i := int64(0); i < n; i += 2 { - var err error + score, err := rd.ReadFloatReply() + if err != nil { + return nil, err + } - z := &zz[i/2] - - z.Member, err = rd.ReadString() - if err != nil { - return nil, err + cmd.val[i] = Z{ + Member: member, + Score: score, + } } - - z.Score, err = rd.ReadFloatReply() - if err != nil { - return nil, err - } - } - return zz, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -1340,22 +1448,22 @@ func zSliceParser(rd *proto.Reader, n int64) (interface{}, error) { type ZWithKeyCmd struct { baseCmd - val ZWithKey + val *ZWithKey } var _ Cmder = (*ZWithKeyCmd)(nil) func NewZWithKeyCmd(args ...interface{}) *ZWithKeyCmd { return &ZWithKeyCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } -func (cmd *ZWithKeyCmd) Val() ZWithKey { +func (cmd *ZWithKeyCmd) Val() *ZWithKey { return cmd.val } -func (cmd *ZWithKeyCmd) Result() (ZWithKey, error) { +func (cmd *ZWithKeyCmd) Result() (*ZWithKey, error) { return cmd.Val(), cmd.Err() } @@ -1364,37 +1472,32 @@ func (cmd *ZWithKeyCmd) String() string { } func (cmd *ZWithKeyCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(zWithKeyParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.(ZWithKey) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + if n != 3 { + return nil, fmt.Errorf("got %d elements, expected 3", n) + } -// Implements proto.MultiBulkParse -func zWithKeyParser(rd *proto.Reader, n int64) (interface{}, error) { - if n != 3 { - return nil, fmt.Errorf("got %d elements, expected 3", n) - } + cmd.val = &ZWithKey{} + var err error - var z ZWithKey - var err error + cmd.val.Key, err = rd.ReadString() + if err != nil { + return nil, err + } - z.Key, err = rd.ReadString() - if err != nil { - return nil, err - } - z.Member, err = rd.ReadString() - if err != nil { - return nil, err - } - z.Score, err = rd.ReadFloatReply() - if err != nil { - return nil, err - } - return z, nil + cmd.val.Member, err = rd.ReadString() + if err != nil { + return nil, err + } + + cmd.val.Score, err = rd.ReadFloatReply() + if err != nil { + return nil, err + } + + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -1412,7 +1515,7 @@ var _ Cmder = (*ScanCmd)(nil) func NewScanCmd(process func(cmd Cmder) error, args ...interface{}) *ScanCmd { return &ScanCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, process: process, } } @@ -1444,7 +1547,7 @@ func (cmd *ScanCmd) Iterator() *ScanIterator { //------------------------------------------------------------------------------ type ClusterNode struct { - Id string + ID string Addr string } @@ -1464,7 +1567,7 @@ var _ Cmder = (*ClusterSlotsCmd)(nil) func NewClusterSlotsCmd(args ...interface{}) *ClusterSlotsCmd { return &ClusterSlotsCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -1481,77 +1584,69 @@ func (cmd *ClusterSlotsCmd) String() string { } func (cmd *ClusterSlotsCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(clusterSlotsParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.([]ClusterSlot) - return nil -} - -// Implements proto.MultiBulkParse -func clusterSlotsParser(rd *proto.Reader, n int64) (interface{}, error) { - slots := make([]ClusterSlot, n) - for i := 0; i < len(slots); i++ { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - if n < 2 { - err := fmt.Errorf("redis: got %d elements in cluster info, expected at least 2", n) - return nil, err - } - - start, err := rd.ReadIntReply() - if err != nil { - return nil, err - } - - end, err := rd.ReadIntReply() - if err != nil { - return nil, err - } - - nodes := make([]ClusterNode, n-2) - for j := 0; j < len(nodes); j++ { + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]ClusterSlot, n) + for i := 0; i < len(cmd.val); i++ { n, err := rd.ReadArrayLen() if err != nil { return nil, err } - if n != 2 && n != 3 { - err := fmt.Errorf("got %d elements in cluster info address, expected 2 or 3", n) + if n < 2 { + err := fmt.Errorf("redis: got %d elements in cluster info, expected at least 2", n) return nil, err } - ip, err := rd.ReadString() + start, err := rd.ReadIntReply() if err != nil { return nil, err } - port, err := rd.ReadString() + end, err := rd.ReadIntReply() if err != nil { return nil, err } - nodes[j].Addr = net.JoinHostPort(ip, port) - - if n == 3 { - id, err := rd.ReadString() + nodes := make([]ClusterNode, n-2) + for j := 0; j < len(nodes); j++ { + n, err := rd.ReadArrayLen() if err != nil { return nil, err } - nodes[j].Id = id + if n != 2 && n != 3 { + err := fmt.Errorf("got %d elements in cluster info address, expected 2 or 3", n) + return nil, err + } + + ip, err := rd.ReadString() + if err != nil { + return nil, err + } + + port, err := rd.ReadString() + if err != nil { + return nil, err + } + + nodes[j].Addr = net.JoinHostPort(ip, port) + + if n == 3 { + id, err := rd.ReadString() + if err != nil { + return nil, err + } + nodes[j].ID = id + } + } + + cmd.val[i] = ClusterSlot{ + Start: int(start), + End: int(end), + Nodes: nodes, } } - - slots[i] = ClusterSlot{ - Start: int(start), - End: int(end), - Nodes: nodes, - } - } - return slots, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -1588,6 +1683,13 @@ type GeoLocationCmd struct { var _ Cmder = (*GeoLocationCmd)(nil) func NewGeoLocationCmd(q *GeoRadiusQuery, args ...interface{}) *GeoLocationCmd { + return &GeoLocationCmd{ + baseCmd: baseCmd{args: geoLocationArgs(q, args...)}, + q: q, + } +} + +func geoLocationArgs(q *GeoRadiusQuery, args ...interface{}) []interface{} { args = append(args, q.Radius) if q.Unit != "" { args = append(args, q.Unit) @@ -1617,10 +1719,7 @@ func NewGeoLocationCmd(q *GeoRadiusQuery, args ...interface{}) *GeoLocationCmd { args = append(args, "storedist") args = append(args, q.StoreDist) } - return &GeoLocationCmd{ - baseCmd: baseCmd{_args: args}, - q: q, - } + return args } func (cmd *GeoLocationCmd) Val() []GeoLocation { @@ -1645,6 +1744,30 @@ func (cmd *GeoLocationCmd) readReply(rd *proto.Reader) error { return nil } +func newGeoLocationSliceParser(q *GeoRadiusQuery) proto.MultiBulkParse { + return func(rd *proto.Reader, n int64) (interface{}, error) { + locs := make([]GeoLocation, 0, n) + for i := int64(0); i < n; i++ { + v, err := rd.ReadReply(newGeoLocationParser(q)) + if err != nil { + return nil, err + } + switch vv := v.(type) { + case string: + locs = append(locs, GeoLocation{ + Name: vv, + }) + case *GeoLocation: + //TODO: avoid copying + locs = append(locs, *vv) + default: + return nil, fmt.Errorf("got %T, expected string or *GeoLocation", v) + } + } + return locs, nil + } +} + func newGeoLocationParser(q *GeoRadiusQuery) proto.MultiBulkParse { return func(rd *proto.Reader, n int64) (interface{}, error) { var loc GeoLocation @@ -1689,29 +1812,6 @@ func newGeoLocationParser(q *GeoRadiusQuery) proto.MultiBulkParse { } } -func newGeoLocationSliceParser(q *GeoRadiusQuery) proto.MultiBulkParse { - return func(rd *proto.Reader, n int64) (interface{}, error) { - locs := make([]GeoLocation, 0, n) - for i := int64(0); i < n; i++ { - v, err := rd.ReadReply(newGeoLocationParser(q)) - if err != nil { - return nil, err - } - switch vv := v.(type) { - case string: - locs = append(locs, GeoLocation{ - Name: vv, - }) - case *GeoLocation: - locs = append(locs, *vv) - default: - return nil, fmt.Errorf("got %T, expected string or *GeoLocation", v) - } - } - return locs, nil - } -} - //------------------------------------------------------------------------------ type GeoPos struct { @@ -1721,19 +1821,19 @@ type GeoPos struct { type GeoPosCmd struct { baseCmd - positions []*GeoPos + val []*GeoPos } var _ Cmder = (*GeoPosCmd)(nil) func NewGeoPosCmd(args ...interface{}) *GeoPosCmd { return &GeoPosCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } func (cmd *GeoPosCmd) Val() []*GeoPos { - return cmd.positions + return cmd.val } func (cmd *GeoPosCmd) Result() ([]*GeoPos, error) { @@ -1741,55 +1841,42 @@ func (cmd *GeoPosCmd) Result() ([]*GeoPos, error) { } func (cmd *GeoPosCmd) String() string { - return cmdString(cmd, cmd.positions) + return cmdString(cmd, cmd.val) } func (cmd *GeoPosCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(geoPosSliceParser) - if cmd.err != nil { - return cmd.err - } - cmd.positions = v.([]*GeoPos) - return nil -} + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make([]*GeoPos, n) + for i := 0; i < len(cmd.val); i++ { + i := i + _, err := rd.ReadReply(func(rd *proto.Reader, n int64) (interface{}, error) { + longitude, err := rd.ReadFloatReply() + if err != nil { + return nil, err + } -func geoPosSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - positions := make([]*GeoPos, 0, n) - for i := int64(0); i < n; i++ { - v, err := rd.ReadReply(geoPosParser) - if err != nil { - if err == Nil { - positions = append(positions, nil) - continue + latitude, err := rd.ReadFloatReply() + if err != nil { + return nil, err + } + + cmd.val[i] = &GeoPos{ + Longitude: longitude, + Latitude: latitude, + } + return nil, nil + }) + if err != nil { + if err == Nil { + cmd.val[i] = nil + continue + } + return nil, err } - return nil, err } - switch v := v.(type) { - case *GeoPos: - positions = append(positions, v) - default: - return nil, fmt.Errorf("got %T, expected *GeoPos", v) - } - } - return positions, nil -} - -func geoPosParser(rd *proto.Reader, n int64) (interface{}, error) { - var pos GeoPos - var err error - - pos.Longitude, err = rd.ReadFloatReply() - if err != nil { - return nil, err - } - - pos.Latitude, err = rd.ReadFloatReply() - if err != nil { - return nil, err - } - - return &pos, nil + return nil, nil + }) + return cmd.err } //------------------------------------------------------------------------------ @@ -1798,6 +1885,7 @@ type CommandInfo struct { Name string Arity int8 Flags []string + ACLFlags []string FirstKeyPos int8 LastKeyPos int8 StepCount int8 @@ -1814,7 +1902,7 @@ var _ Cmder = (*CommandsInfoCmd)(nil) func NewCommandsInfoCmd(args ...interface{}) *CommandsInfoCmd { return &CommandsInfoCmd{ - baseCmd: baseCmd{_args: args}, + baseCmd: baseCmd{args: args}, } } @@ -1831,38 +1919,35 @@ func (cmd *CommandsInfoCmd) String() string { } func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error { - var v interface{} - v, cmd.err = rd.ReadArrayReply(commandInfoSliceParser) - if cmd.err != nil { - return cmd.err - } - cmd.val = v.(map[string]*CommandInfo) - return nil -} - -// Implements proto.MultiBulkParse -func commandInfoSliceParser(rd *proto.Reader, n int64) (interface{}, error) { - m := make(map[string]*CommandInfo, n) - for i := int64(0); i < n; i++ { - v, err := rd.ReadReply(commandInfoParser) - if err != nil { - return nil, err + _, cmd.err = rd.ReadArrayReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.val = make(map[string]*CommandInfo, n) + for i := int64(0); i < n; i++ { + v, err := rd.ReadReply(commandInfoParser) + if err != nil { + return nil, err + } + vv := v.(*CommandInfo) + cmd.val[vv.Name] = vv } - vv := v.(*CommandInfo) - m[vv.Name] = vv - - } - return m, nil + return nil, nil + }) + return cmd.err } func commandInfoParser(rd *proto.Reader, n int64) (interface{}, error) { + const numArgRedis5 = 6 + const numArgRedis6 = 7 + + switch n { + case numArgRedis5, numArgRedis6: + // continue + default: + return nil, fmt.Errorf("redis: got %d elements in COMMAND reply, wanted 7", n) + } + var cmd CommandInfo var err error - if n != 6 { - return nil, fmt.Errorf("redis: got %d elements in COMMAND reply, wanted 6", n) - } - cmd.Name, err = rd.ReadString() if err != nil { return nil, err @@ -1874,11 +1959,23 @@ func commandInfoParser(rd *proto.Reader, n int64) (interface{}, error) { } cmd.Arity = int8(arity) - flags, err := rd.ReadReply(stringSliceParser) + _, err = rd.ReadReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.Flags = make([]string, n) + for i := 0; i < len(cmd.Flags); i++ { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.Flags[i] = "" + case err != nil: + return nil, err + default: + cmd.Flags[i] = s + } + } + return nil, nil + }) if err != nil { return nil, err } - cmd.Flags = flags.([]string) firstKeyPos, err := rd.ReadIntReply() if err != nil { @@ -1905,6 +2002,28 @@ func commandInfoParser(rd *proto.Reader, n int64) (interface{}, error) { } } + if n == numArgRedis5 { + return &cmd, nil + } + + _, err = rd.ReadReply(func(rd *proto.Reader, n int64) (interface{}, error) { + cmd.ACLFlags = make([]string, n) + for i := 0; i < len(cmd.ACLFlags); i++ { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.ACLFlags[i] = "" + case err != nil: + return nil, err + default: + cmd.ACLFlags[i] = s + } + } + return nil, nil + }) + if err != nil { + return nil, err + } + return &cmd, nil } @@ -1929,6 +2048,15 @@ func (c *cmdsInfoCache) Get() (map[string]*CommandInfo, error) { if err != nil { return err } + + // Extensions have cmd names in upper case. Convert them to lower case. + for k, v := range cmds { + lower := internal.ToLower(k) + if lower != k { + cmds[lower] = v + } + } + c.cmds = cmds return nil }) diff --git a/vendor/github.com/go-redis/redis/commands.go b/vendor/github.com/go-redis/redis/v7/commands.go similarity index 63% rename from vendor/github.com/go-redis/redis/commands.go rename to vendor/github.com/go-redis/redis/v7/commands.go index 653e4abe9..da5ceda13 100644 --- a/vendor/github.com/go-redis/redis/commands.go +++ b/vendor/github.com/go-redis/redis/v7/commands.go @@ -5,7 +5,7 @@ import ( "io" "time" - "github.com/go-redis/redis/internal" + "github.com/go-redis/redis/v7/internal" ) func usePrecise(dur time.Duration) bool { @@ -14,7 +14,7 @@ func usePrecise(dur time.Duration) bool { func formatMs(dur time.Duration) int64 { if dur > 0 && dur < time.Millisecond { - internal.Logf( + internal.Logger.Printf( "specified duration is %s, but minimal supported value is %s", dur, time.Millisecond, ) @@ -24,7 +24,7 @@ func formatMs(dur time.Duration) int64 { func formatSec(dur time.Duration) int64 { if dur > 0 && dur < time.Second { - internal.Logf( + internal.Logger.Printf( "specified duration is %s, but minimal supported value is %s", dur, time.Second, ) @@ -34,17 +34,21 @@ func formatSec(dur time.Duration) int64 { func appendArgs(dst, src []interface{}) []interface{} { if len(src) == 1 { - if ss, ok := src[0].([]string); ok { - for _, s := range ss { + switch v := src[0].(type) { + case []string: + for _, s := range v { dst = append(dst, s) } return dst + case map[string]interface{}: + for k, v := range v { + dst = append(dst, k, v) + } + return dst } } - for _, v := range src { - dst = append(dst, v) - } + dst = append(dst, src...) return dst } @@ -67,8 +71,8 @@ type Cmdable interface { Expire(key string, expiration time.Duration) *BoolCmd ExpireAt(key string, tm time.Time) *BoolCmd Keys(pattern string) *StringSliceCmd - Migrate(host, port, key string, db int64, timeout time.Duration) *StatusCmd - Move(key string, db int64) *BoolCmd + Migrate(host, port, key string, db int, timeout time.Duration) *StatusCmd + Move(key string, db int) *BoolCmd ObjectRefCount(key string) *IntCmd ObjectEncoding(key string) *StringCmd ObjectIdleTime(key string) *DurationCmd @@ -98,6 +102,7 @@ type Cmdable interface { BitOpXor(destKey string, keys ...string) *IntCmd BitOpNot(destKey string, key string) *IntCmd BitPos(key string, bit int64, pos ...int64) *IntCmd + BitField(key string, args ...interface{}) *IntSliceCmd Decr(key string) *IntCmd DecrBy(key string, decrement int64) *IntCmd Get(key string) *StringCmd @@ -108,8 +113,8 @@ type Cmdable interface { IncrBy(key string, value int64) *IntCmd IncrByFloat(key string, value float64) *FloatCmd MGet(keys ...string) *SliceCmd - MSet(pairs ...interface{}) *StatusCmd - MSetNX(pairs ...interface{}) *BoolCmd + MSet(values ...interface{}) *StatusCmd + MSetNX(values ...interface{}) *BoolCmd Set(key string, value interface{}, expiration time.Duration) *StatusCmd SetBit(key string, offset int64, value int) *IntCmd SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd @@ -125,8 +130,8 @@ type Cmdable interface { HKeys(key string) *StringSliceCmd HLen(key string) *IntCmd HMGet(key string, fields ...string) *SliceCmd - HMSet(key string, fields map[string]interface{}) *StatusCmd - HSet(key, field string, value interface{}) *BoolCmd + HSet(key string, values ...interface{}) *IntCmd + HMSet(key string, values ...interface{}) *BoolCmd HSetNX(key, field string, value interface{}) *BoolCmd HVals(key string) *StringSliceCmd BLPop(timeout time.Duration, keys ...string) *StringSliceCmd @@ -139,7 +144,7 @@ type Cmdable interface { LLen(key string) *IntCmd LPop(key string) *StringCmd LPush(key string, values ...interface{}) *IntCmd - LPushX(key string, value interface{}) *IntCmd + LPushX(key string, values ...interface{}) *IntCmd LRange(key string, start, stop int64) *StringSliceCmd LRem(key string, count int64, value interface{}) *IntCmd LSet(key string, index int64, value interface{}) *StatusCmd @@ -147,7 +152,7 @@ type Cmdable interface { RPop(key string) *StringCmd RPopLPush(source, destination string) *StringCmd RPush(key string, values ...interface{}) *IntCmd - RPushX(key string, value interface{}) *IntCmd + RPushX(key string, values ...interface{}) *IntCmd SAdd(key string, members ...interface{}) *IntCmd SCard(key string) *IntCmd SDiff(keys ...string) *StringSliceCmd @@ -187,29 +192,30 @@ type Cmdable interface { XClaimJustID(a *XClaimArgs) *StringSliceCmd XTrim(key string, maxLen int64) *IntCmd XTrimApprox(key string, maxLen int64) *IntCmd + XInfoGroups(key string) *XInfoGroupsCmd BZPopMax(timeout time.Duration, keys ...string) *ZWithKeyCmd BZPopMin(timeout time.Duration, keys ...string) *ZWithKeyCmd - ZAdd(key string, members ...Z) *IntCmd - ZAddNX(key string, members ...Z) *IntCmd - ZAddXX(key string, members ...Z) *IntCmd - ZAddCh(key string, members ...Z) *IntCmd - ZAddNXCh(key string, members ...Z) *IntCmd - ZAddXXCh(key string, members ...Z) *IntCmd - ZIncr(key string, member Z) *FloatCmd - ZIncrNX(key string, member Z) *FloatCmd - ZIncrXX(key string, member Z) *FloatCmd + ZAdd(key string, members ...*Z) *IntCmd + ZAddNX(key string, members ...*Z) *IntCmd + ZAddXX(key string, members ...*Z) *IntCmd + ZAddCh(key string, members ...*Z) *IntCmd + ZAddNXCh(key string, members ...*Z) *IntCmd + ZAddXXCh(key string, members ...*Z) *IntCmd + ZIncr(key string, member *Z) *FloatCmd + ZIncrNX(key string, member *Z) *FloatCmd + ZIncrXX(key string, member *Z) *FloatCmd ZCard(key string) *IntCmd ZCount(key, min, max string) *IntCmd ZLexCount(key, min, max string) *IntCmd ZIncrBy(key string, increment float64, member string) *FloatCmd - ZInterStore(destination string, store ZStore, keys ...string) *IntCmd + ZInterStore(destination string, store *ZStore) *IntCmd ZPopMax(key string, count ...int64) *ZSliceCmd ZPopMin(key string, count ...int64) *ZSliceCmd ZRange(key string, start, stop int64) *StringSliceCmd ZRangeWithScores(key string, start, stop int64) *ZSliceCmd - ZRangeByScore(key string, opt ZRangeBy) *StringSliceCmd - ZRangeByLex(key string, opt ZRangeBy) *StringSliceCmd - ZRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCmd + ZRangeByScore(key string, opt *ZRangeBy) *StringSliceCmd + ZRangeByLex(key string, opt *ZRangeBy) *StringSliceCmd + ZRangeByScoreWithScores(key string, opt *ZRangeBy) *ZSliceCmd ZRank(key, member string) *IntCmd ZRem(key string, members ...interface{}) *IntCmd ZRemRangeByRank(key string, start, stop int64) *IntCmd @@ -217,12 +223,12 @@ type Cmdable interface { ZRemRangeByLex(key, min, max string) *IntCmd ZRevRange(key string, start, stop int64) *StringSliceCmd ZRevRangeWithScores(key string, start, stop int64) *ZSliceCmd - ZRevRangeByScore(key string, opt ZRangeBy) *StringSliceCmd - ZRevRangeByLex(key string, opt ZRangeBy) *StringSliceCmd - ZRevRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCmd + ZRevRangeByScore(key string, opt *ZRangeBy) *StringSliceCmd + ZRevRangeByLex(key string, opt *ZRangeBy) *StringSliceCmd + ZRevRangeByScoreWithScores(key string, opt *ZRangeBy) *ZSliceCmd ZRevRank(key, member string) *IntCmd ZScore(key, member string) *FloatCmd - ZUnionStore(dest string, store ZStore, keys ...string) *IntCmd + ZUnionStore(dest string, store *ZStore) *IntCmd PFAdd(key string, els ...interface{}) *IntCmd PFCount(keys ...string) *IntCmd PFMerge(dest string, keys ...string) *StatusCmd @@ -283,9 +289,9 @@ type Cmdable interface { GeoAdd(key string, geoLocation ...*GeoLocation) *IntCmd GeoPos(key string, members ...string) *GeoPosCmd GeoRadius(key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd - GeoRadiusRO(key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd + GeoRadiusStore(key string, longitude, latitude float64, query *GeoRadiusQuery) *IntCmd GeoRadiusByMember(key, member string, query *GeoRadiusQuery) *GeoLocationCmd - GeoRadiusByMemberRO(key, member string, query *GeoRadiusQuery) *GeoLocationCmd + GeoRadiusByMemberStore(key, member string, query *GeoRadiusQuery) *IntCmd GeoDist(key string, member1, member2, unit string) *FloatCmd GeoHash(key string, members ...string) *StringSliceCmd ReadOnly() *StatusCmd @@ -296,6 +302,7 @@ type Cmdable interface { type StatefulCmdable interface { Cmdable Auth(password string) *StatusCmd + AuthACL(username, password string) *StatusCmd Select(index int) *StatusCmd SwapDB(index1, index2 int) *StatusCmd ClientSetName(name string) *BoolCmd @@ -306,132 +313,127 @@ var _ Cmdable = (*Tx)(nil) var _ Cmdable = (*Ring)(nil) var _ Cmdable = (*ClusterClient)(nil) -type cmdable struct { - process func(cmd Cmder) error -} +type cmdable func(cmd Cmder) error -func (c *cmdable) setProcessor(fn func(Cmder) error) { - c.process = fn -} - -type statefulCmdable struct { - cmdable - process func(cmd Cmder) error -} - -func (c *statefulCmdable) setProcessor(fn func(Cmder) error) { - c.process = fn - c.cmdable.setProcessor(fn) -} +type statefulCmdable func(cmd Cmder) error //------------------------------------------------------------------------------ -func (c *statefulCmdable) Auth(password string) *StatusCmd { +func (c statefulCmdable) Auth(password string) *StatusCmd { cmd := NewStatusCmd("auth", password) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Echo(message interface{}) *StringCmd { +// Perform an AUTH command, using the given user and pass. +// Should be used to authenticate the current connection with one of the connections defined in the ACL list +// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system. +func (c statefulCmdable) AuthACL(username, password string) *StatusCmd { + cmd := NewStatusCmd("auth", username, password) + _ = c(cmd) + return cmd +} + +func (c cmdable) Echo(message interface{}) *StringCmd { cmd := NewStringCmd("echo", message) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Ping() *StatusCmd { +func (c cmdable) Ping() *StatusCmd { cmd := NewStatusCmd("ping") - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Wait(numSlaves int, timeout time.Duration) *IntCmd { +func (c cmdable) Wait(numSlaves int, timeout time.Duration) *IntCmd { cmd := NewIntCmd("wait", numSlaves, int(timeout/time.Millisecond)) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Quit() *StatusCmd { +func (c cmdable) Quit() *StatusCmd { panic("not implemented") } -func (c *statefulCmdable) Select(index int) *StatusCmd { +func (c statefulCmdable) Select(index int) *StatusCmd { cmd := NewStatusCmd("select", index) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *statefulCmdable) SwapDB(index1, index2 int) *StatusCmd { +func (c statefulCmdable) SwapDB(index1, index2 int) *StatusCmd { cmd := NewStatusCmd("swapdb", index1, index2) - c.process(cmd) + _ = c(cmd) return cmd } //------------------------------------------------------------------------------ -func (c *cmdable) Command() *CommandsInfoCmd { +func (c cmdable) Command() *CommandsInfoCmd { cmd := NewCommandsInfoCmd("command") - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Del(keys ...string) *IntCmd { +func (c cmdable) Del(keys ...string) *IntCmd { args := make([]interface{}, 1+len(keys)) args[0] = "del" for i, key := range keys { args[1+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Unlink(keys ...string) *IntCmd { +func (c cmdable) Unlink(keys ...string) *IntCmd { args := make([]interface{}, 1+len(keys)) args[0] = "unlink" for i, key := range keys { args[1+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Dump(key string) *StringCmd { +func (c cmdable) Dump(key string) *StringCmd { cmd := NewStringCmd("dump", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Exists(keys ...string) *IntCmd { +func (c cmdable) Exists(keys ...string) *IntCmd { args := make([]interface{}, 1+len(keys)) args[0] = "exists" for i, key := range keys { args[1+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Expire(key string, expiration time.Duration) *BoolCmd { +func (c cmdable) Expire(key string, expiration time.Duration) *BoolCmd { cmd := NewBoolCmd("expire", key, formatSec(expiration)) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ExpireAt(key string, tm time.Time) *BoolCmd { +func (c cmdable) ExpireAt(key string, tm time.Time) *BoolCmd { cmd := NewBoolCmd("expireat", key, tm.Unix()) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Keys(pattern string) *StringSliceCmd { +func (c cmdable) Keys(pattern string) *StringSliceCmd { cmd := NewStringSliceCmd("keys", pattern) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Migrate(host, port, key string, db int64, timeout time.Duration) *StatusCmd { +func (c cmdable) Migrate(host, port, key string, db int, timeout time.Duration) *StatusCmd { cmd := NewStatusCmd( "migrate", host, @@ -441,92 +443,92 @@ func (c *cmdable) Migrate(host, port, key string, db int64, timeout time.Duratio formatMs(timeout), ) cmd.setReadTimeout(timeout) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Move(key string, db int64) *BoolCmd { +func (c cmdable) Move(key string, db int) *BoolCmd { cmd := NewBoolCmd("move", key, db) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ObjectRefCount(key string) *IntCmd { +func (c cmdable) ObjectRefCount(key string) *IntCmd { cmd := NewIntCmd("object", "refcount", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ObjectEncoding(key string) *StringCmd { +func (c cmdable) ObjectEncoding(key string) *StringCmd { cmd := NewStringCmd("object", "encoding", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ObjectIdleTime(key string) *DurationCmd { +func (c cmdable) ObjectIdleTime(key string) *DurationCmd { cmd := NewDurationCmd(time.Second, "object", "idletime", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Persist(key string) *BoolCmd { +func (c cmdable) Persist(key string) *BoolCmd { cmd := NewBoolCmd("persist", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) PExpire(key string, expiration time.Duration) *BoolCmd { +func (c cmdable) PExpire(key string, expiration time.Duration) *BoolCmd { cmd := NewBoolCmd("pexpire", key, formatMs(expiration)) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) PExpireAt(key string, tm time.Time) *BoolCmd { +func (c cmdable) PExpireAt(key string, tm time.Time) *BoolCmd { cmd := NewBoolCmd( "pexpireat", key, tm.UnixNano()/int64(time.Millisecond), ) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) PTTL(key string) *DurationCmd { +func (c cmdable) PTTL(key string) *DurationCmd { cmd := NewDurationCmd(time.Millisecond, "pttl", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) RandomKey() *StringCmd { +func (c cmdable) RandomKey() *StringCmd { cmd := NewStringCmd("randomkey") - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Rename(key, newkey string) *StatusCmd { +func (c cmdable) Rename(key, newkey string) *StatusCmd { cmd := NewStatusCmd("rename", key, newkey) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) RenameNX(key, newkey string) *BoolCmd { +func (c cmdable) RenameNX(key, newkey string) *BoolCmd { cmd := NewBoolCmd("renamenx", key, newkey) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Restore(key string, ttl time.Duration, value string) *StatusCmd { +func (c cmdable) Restore(key string, ttl time.Duration, value string) *StatusCmd { cmd := NewStatusCmd( "restore", key, formatMs(ttl), value, ) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) RestoreReplace(key string, ttl time.Duration, value string) *StatusCmd { +func (c cmdable) RestoreReplace(key string, ttl time.Duration, value string) *StatusCmd { cmd := NewStatusCmd( "restore", key, @@ -534,7 +536,7 @@ func (c *cmdable) RestoreReplace(key string, ttl time.Duration, value string) *S value, "replace", ) - c.process(cmd) + _ = c(cmd) return cmd } @@ -566,52 +568,52 @@ func (sort *Sort) args(key string) []interface{} { return args } -func (c *cmdable) Sort(key string, sort *Sort) *StringSliceCmd { +func (c cmdable) Sort(key string, sort *Sort) *StringSliceCmd { cmd := NewStringSliceCmd(sort.args(key)...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SortStore(key, store string, sort *Sort) *IntCmd { +func (c cmdable) SortStore(key, store string, sort *Sort) *IntCmd { args := sort.args(key) if store != "" { args = append(args, "store", store) } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SortInterfaces(key string, sort *Sort) *SliceCmd { +func (c cmdable) SortInterfaces(key string, sort *Sort) *SliceCmd { cmd := NewSliceCmd(sort.args(key)...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Touch(keys ...string) *IntCmd { +func (c cmdable) Touch(keys ...string) *IntCmd { args := make([]interface{}, len(keys)+1) args[0] = "touch" for i, key := range keys { args[i+1] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) TTL(key string) *DurationCmd { +func (c cmdable) TTL(key string) *DurationCmd { cmd := NewDurationCmd(time.Second, "ttl", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Type(key string) *StatusCmd { +func (c cmdable) Type(key string) *StatusCmd { cmd := NewStatusCmd("type", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Scan(cursor uint64, match string, count int64) *ScanCmd { +func (c cmdable) Scan(cursor uint64, match string, count int64) *ScanCmd { args := []interface{}{"scan", cursor} if match != "" { args = append(args, "match", match) @@ -619,12 +621,12 @@ func (c *cmdable) Scan(cursor uint64, match string, count int64) *ScanCmd { if count > 0 { args = append(args, "count", count) } - cmd := NewScanCmd(c.process, args...) - c.process(cmd) + cmd := NewScanCmd(c, args...) + _ = c(cmd) return cmd } -func (c *cmdable) SScan(key string, cursor uint64, match string, count int64) *ScanCmd { +func (c cmdable) SScan(key string, cursor uint64, match string, count int64) *ScanCmd { args := []interface{}{"sscan", key, cursor} if match != "" { args = append(args, "match", match) @@ -632,12 +634,12 @@ func (c *cmdable) SScan(key string, cursor uint64, match string, count int64) *S if count > 0 { args = append(args, "count", count) } - cmd := NewScanCmd(c.process, args...) - c.process(cmd) + cmd := NewScanCmd(c, args...) + _ = c(cmd) return cmd } -func (c *cmdable) HScan(key string, cursor uint64, match string, count int64) *ScanCmd { +func (c cmdable) HScan(key string, cursor uint64, match string, count int64) *ScanCmd { args := []interface{}{"hscan", key, cursor} if match != "" { args = append(args, "match", match) @@ -645,12 +647,12 @@ func (c *cmdable) HScan(key string, cursor uint64, match string, count int64) *S if count > 0 { args = append(args, "count", count) } - cmd := NewScanCmd(c.process, args...) - c.process(cmd) + cmd := NewScanCmd(c, args...) + _ = c(cmd) return cmd } -func (c *cmdable) ZScan(key string, cursor uint64, match string, count int64) *ScanCmd { +func (c cmdable) ZScan(key string, cursor uint64, match string, count int64) *ScanCmd { args := []interface{}{"zscan", key, cursor} if match != "" { args = append(args, "match", match) @@ -658,16 +660,16 @@ func (c *cmdable) ZScan(key string, cursor uint64, match string, count int64) *S if count > 0 { args = append(args, "count", count) } - cmd := NewScanCmd(c.process, args...) - c.process(cmd) + cmd := NewScanCmd(c, args...) + _ = c(cmd) return cmd } //------------------------------------------------------------------------------ -func (c *cmdable) Append(key, value string) *IntCmd { +func (c cmdable) Append(key, value string) *IntCmd { cmd := NewIntCmd("append", key, value) - c.process(cmd) + _ = c(cmd) return cmd } @@ -675,7 +677,7 @@ type BitCount struct { Start, End int64 } -func (c *cmdable) BitCount(key string, bitCount *BitCount) *IntCmd { +func (c cmdable) BitCount(key string, bitCount *BitCount) *IntCmd { args := []interface{}{"bitcount", key} if bitCount != nil { args = append( @@ -685,11 +687,11 @@ func (c *cmdable) BitCount(key string, bitCount *BitCount) *IntCmd { ) } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) bitOp(op, destKey string, keys ...string) *IntCmd { +func (c cmdable) bitOp(op, destKey string, keys ...string) *IntCmd { args := make([]interface{}, 3+len(keys)) args[0] = "bitop" args[1] = op @@ -698,27 +700,27 @@ func (c *cmdable) bitOp(op, destKey string, keys ...string) *IntCmd { args[3+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) BitOpAnd(destKey string, keys ...string) *IntCmd { +func (c cmdable) BitOpAnd(destKey string, keys ...string) *IntCmd { return c.bitOp("and", destKey, keys...) } -func (c *cmdable) BitOpOr(destKey string, keys ...string) *IntCmd { +func (c cmdable) BitOpOr(destKey string, keys ...string) *IntCmd { return c.bitOp("or", destKey, keys...) } -func (c *cmdable) BitOpXor(destKey string, keys ...string) *IntCmd { +func (c cmdable) BitOpXor(destKey string, keys ...string) *IntCmd { return c.bitOp("xor", destKey, keys...) } -func (c *cmdable) BitOpNot(destKey string, key string) *IntCmd { +func (c cmdable) BitOpNot(destKey string, key string) *IntCmd { return c.bitOp("not", destKey, key) } -func (c *cmdable) BitPos(key string, bit int64, pos ...int64) *IntCmd { +func (c cmdable) BitPos(key string, bit int64, pos ...int64) *IntCmd { args := make([]interface{}, 3+len(pos)) args[0] = "bitpos" args[1] = key @@ -734,91 +736,109 @@ func (c *cmdable) BitPos(key string, bit int64, pos ...int64) *IntCmd { panic("too many arguments") } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Decr(key string) *IntCmd { +func (c cmdable) BitField(key string, args ...interface{}) *IntSliceCmd { + a := make([]interface{}, 0, 2+len(args)) + a = append(a, "bitfield") + a = append(a, key) + a = append(a, args...) + cmd := NewIntSliceCmd(a...) + _ = c(cmd) + return cmd +} + +func (c cmdable) Decr(key string) *IntCmd { cmd := NewIntCmd("decr", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) DecrBy(key string, decrement int64) *IntCmd { +func (c cmdable) DecrBy(key string, decrement int64) *IntCmd { cmd := NewIntCmd("decrby", key, decrement) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `GET key` command. It returns redis.Nil error when key does not exist. -func (c *cmdable) Get(key string) *StringCmd { +func (c cmdable) Get(key string) *StringCmd { cmd := NewStringCmd("get", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) GetBit(key string, offset int64) *IntCmd { +func (c cmdable) GetBit(key string, offset int64) *IntCmd { cmd := NewIntCmd("getbit", key, offset) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) GetRange(key string, start, end int64) *StringCmd { +func (c cmdable) GetRange(key string, start, end int64) *StringCmd { cmd := NewStringCmd("getrange", key, start, end) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) GetSet(key string, value interface{}) *StringCmd { +func (c cmdable) GetSet(key string, value interface{}) *StringCmd { cmd := NewStringCmd("getset", key, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) Incr(key string) *IntCmd { +func (c cmdable) Incr(key string) *IntCmd { cmd := NewIntCmd("incr", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) IncrBy(key string, value int64) *IntCmd { +func (c cmdable) IncrBy(key string, value int64) *IntCmd { cmd := NewIntCmd("incrby", key, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) IncrByFloat(key string, value float64) *FloatCmd { +func (c cmdable) IncrByFloat(key string, value float64) *FloatCmd { cmd := NewFloatCmd("incrbyfloat", key, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) MGet(keys ...string) *SliceCmd { +func (c cmdable) MGet(keys ...string) *SliceCmd { args := make([]interface{}, 1+len(keys)) args[0] = "mget" for i, key := range keys { args[1+i] = key } cmd := NewSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) MSet(pairs ...interface{}) *StatusCmd { - args := make([]interface{}, 1, 1+len(pairs)) +// MSet is like Set but accepts multiple values: +// - MSet("key1", "value1", "key2", "value2") +// - MSet([]string{"key1", "value1", "key2", "value2"}) +// - MSet(map[string]interface{}{"key1": "value1", "key2": "value2"}) +func (c cmdable) MSet(values ...interface{}) *StatusCmd { + args := make([]interface{}, 1, 1+len(values)) args[0] = "mset" - args = appendArgs(args, pairs) + args = appendArgs(args, values) cmd := NewStatusCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) MSetNX(pairs ...interface{}) *BoolCmd { - args := make([]interface{}, 1, 1+len(pairs)) +// MSetNX is like SetNX but accepts multiple values: +// - MSetNX("key1", "value1", "key2", "value2") +// - MSetNX([]string{"key1", "value1", "key2", "value2"}) +// - MSetNX(map[string]interface{}{"key1": "value1", "key2": "value2"}) +func (c cmdable) MSetNX(values ...interface{}) *BoolCmd { + args := make([]interface{}, 1, 1+len(values)) args[0] = "msetnx" - args = appendArgs(args, pairs) + args = appendArgs(args, values) cmd := NewBoolCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } @@ -826,8 +846,8 @@ func (c *cmdable) MSetNX(pairs ...interface{}) *BoolCmd { // // Use expiration for `SETEX`-like behavior. // Zero expiration means the key has no expiration time. -func (c *cmdable) Set(key string, value interface{}, expiration time.Duration) *StatusCmd { - args := make([]interface{}, 3, 4) +func (c cmdable) Set(key string, value interface{}, expiration time.Duration) *StatusCmd { + args := make([]interface{}, 3, 5) args[0] = "set" args[1] = key args[2] = value @@ -839,25 +859,25 @@ func (c *cmdable) Set(key string, value interface{}, expiration time.Duration) * } } cmd := NewStatusCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SetBit(key string, offset int64, value int) *IntCmd { +func (c cmdable) SetBit(key string, offset int64, value int) *IntCmd { cmd := NewIntCmd( "setbit", key, offset, value, ) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SET key value [expiration] NX` command. // // Zero expiration means the key has no expiration time. -func (c *cmdable) SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd { +func (c cmdable) SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd { var cmd *BoolCmd if expiration == 0 { // Use old `SETNX` to support old Redis versions. @@ -869,14 +889,14 @@ func (c *cmdable) SetNX(key string, value interface{}, expiration time.Duration) cmd = NewBoolCmd("set", key, value, "ex", formatSec(expiration), "nx") } } - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SET key value [expiration] XX` command. // // Zero expiration means the key has no expiration time. -func (c *cmdable) SetXX(key string, value interface{}, expiration time.Duration) *BoolCmd { +func (c cmdable) SetXX(key string, value interface{}, expiration time.Duration) *BoolCmd { var cmd *BoolCmd if expiration == 0 { cmd = NewBoolCmd("set", key, value, "xx") @@ -887,25 +907,25 @@ func (c *cmdable) SetXX(key string, value interface{}, expiration time.Duration) cmd = NewBoolCmd("set", key, value, "ex", formatSec(expiration), "xx") } } - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SetRange(key string, offset int64, value string) *IntCmd { +func (c cmdable) SetRange(key string, offset int64, value string) *IntCmd { cmd := NewIntCmd("setrange", key, offset, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) StrLen(key string) *IntCmd { +func (c cmdable) StrLen(key string) *IntCmd { cmd := NewIntCmd("strlen", key) - c.process(cmd) + _ = c(cmd) return cmd } //------------------------------------------------------------------------------ -func (c *cmdable) HDel(key string, fields ...string) *IntCmd { +func (c cmdable) HDel(key string, fields ...string) *IntCmd { args := make([]interface{}, 2+len(fields)) args[0] = "hdel" args[1] = key @@ -913,53 +933,55 @@ func (c *cmdable) HDel(key string, fields ...string) *IntCmd { args[2+i] = field } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HExists(key, field string) *BoolCmd { +func (c cmdable) HExists(key, field string) *BoolCmd { cmd := NewBoolCmd("hexists", key, field) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HGet(key, field string) *StringCmd { +func (c cmdable) HGet(key, field string) *StringCmd { cmd := NewStringCmd("hget", key, field) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HGetAll(key string) *StringStringMapCmd { +func (c cmdable) HGetAll(key string) *StringStringMapCmd { cmd := NewStringStringMapCmd("hgetall", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HIncrBy(key, field string, incr int64) *IntCmd { +func (c cmdable) HIncrBy(key, field string, incr int64) *IntCmd { cmd := NewIntCmd("hincrby", key, field, incr) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HIncrByFloat(key, field string, incr float64) *FloatCmd { +func (c cmdable) HIncrByFloat(key, field string, incr float64) *FloatCmd { cmd := NewFloatCmd("hincrbyfloat", key, field, incr) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HKeys(key string) *StringSliceCmd { +func (c cmdable) HKeys(key string) *StringSliceCmd { cmd := NewStringSliceCmd("hkeys", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HLen(key string) *IntCmd { +func (c cmdable) HLen(key string) *IntCmd { cmd := NewIntCmd("hlen", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HMGet(key string, fields ...string) *SliceCmd { +// HMGet returns the values for the specified fields in the hash stored at key. +// It returns an interface{} to distinguish between empty string and nil value. +func (c cmdable) HMGet(key string, fields ...string) *SliceCmd { args := make([]interface{}, 2+len(fields)) args[0] = "hmget" args[1] = key @@ -967,46 +989,52 @@ func (c *cmdable) HMGet(key string, fields ...string) *SliceCmd { args[2+i] = field } cmd := NewSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HMSet(key string, fields map[string]interface{}) *StatusCmd { - args := make([]interface{}, 2+len(fields)*2) +// HSet accepts values in following formats: +// - HMSet("myhash", "key1", "value1", "key2", "value2") +// - HMSet("myhash", []string{"key1", "value1", "key2", "value2"}) +// - HMSet("myhash", map[string]interface{}{"key1": "value1", "key2": "value2"}) +// +// Note that it requires Redis v4 for multiple field/value pairs support. +func (c cmdable) HSet(key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "hset" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(args...) + _ = c(cmd) + return cmd +} + +// HMSet is a deprecated version of HSet left for compatibility with Redis 3. +func (c cmdable) HMSet(key string, values ...interface{}) *BoolCmd { + args := make([]interface{}, 2, 2+len(values)) args[0] = "hmset" args[1] = key - i := 2 - for k, v := range fields { - args[i] = k - args[i+1] = v - i += 2 - } - cmd := NewStatusCmd(args...) - c.process(cmd) + args = appendArgs(args, values) + cmd := NewBoolCmd(args...) + _ = c(cmd) return cmd } -func (c *cmdable) HSet(key, field string, value interface{}) *BoolCmd { - cmd := NewBoolCmd("hset", key, field, value) - c.process(cmd) - return cmd -} - -func (c *cmdable) HSetNX(key, field string, value interface{}) *BoolCmd { +func (c cmdable) HSetNX(key, field string, value interface{}) *BoolCmd { cmd := NewBoolCmd("hsetnx", key, field, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) HVals(key string) *StringSliceCmd { +func (c cmdable) HVals(key string) *StringSliceCmd { cmd := NewStringSliceCmd("hvals", key) - c.process(cmd) + _ = c(cmd) return cmd } //------------------------------------------------------------------------------ -func (c *cmdable) BLPop(timeout time.Duration, keys ...string) *StringSliceCmd { +func (c cmdable) BLPop(timeout time.Duration, keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)+1) args[0] = "blpop" for i, key := range keys { @@ -1015,11 +1043,11 @@ func (c *cmdable) BLPop(timeout time.Duration, keys ...string) *StringSliceCmd { args[len(args)-1] = formatSec(timeout) cmd := NewStringSliceCmd(args...) cmd.setReadTimeout(timeout) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) BRPop(timeout time.Duration, keys ...string) *StringSliceCmd { +func (c cmdable) BRPop(timeout time.Duration, keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)+1) args[0] = "brpop" for i, key := range keys { @@ -1028,11 +1056,11 @@ func (c *cmdable) BRPop(timeout time.Duration, keys ...string) *StringSliceCmd { args[len(keys)+1] = formatSec(timeout) cmd := NewStringSliceCmd(args...) cmd.setReadTimeout(timeout) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) BRPopLPush(source, destination string, timeout time.Duration) *StringCmd { +func (c cmdable) BRPopLPush(source, destination string, timeout time.Duration) *StringCmd { cmd := NewStringCmd( "brpoplpush", source, @@ -1040,154 +1068,162 @@ func (c *cmdable) BRPopLPush(source, destination string, timeout time.Duration) formatSec(timeout), ) cmd.setReadTimeout(timeout) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LIndex(key string, index int64) *StringCmd { +func (c cmdable) LIndex(key string, index int64) *StringCmd { cmd := NewStringCmd("lindex", key, index) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LInsert(key, op string, pivot, value interface{}) *IntCmd { +func (c cmdable) LInsert(key, op string, pivot, value interface{}) *IntCmd { cmd := NewIntCmd("linsert", key, op, pivot, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LInsertBefore(key string, pivot, value interface{}) *IntCmd { +func (c cmdable) LInsertBefore(key string, pivot, value interface{}) *IntCmd { cmd := NewIntCmd("linsert", key, "before", pivot, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LInsertAfter(key string, pivot, value interface{}) *IntCmd { +func (c cmdable) LInsertAfter(key string, pivot, value interface{}) *IntCmd { cmd := NewIntCmd("linsert", key, "after", pivot, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LLen(key string) *IntCmd { +func (c cmdable) LLen(key string) *IntCmd { cmd := NewIntCmd("llen", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LPop(key string) *StringCmd { +func (c cmdable) LPop(key string) *StringCmd { cmd := NewStringCmd("lpop", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LPush(key string, values ...interface{}) *IntCmd { +func (c cmdable) LPush(key string, values ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(values)) args[0] = "lpush" args[1] = key args = appendArgs(args, values) cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LPushX(key string, value interface{}) *IntCmd { - cmd := NewIntCmd("lpushx", key, value) - c.process(cmd) +func (c cmdable) LPushX(key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "lpushx" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(args...) + _ = c(cmd) return cmd } -func (c *cmdable) LRange(key string, start, stop int64) *StringSliceCmd { +func (c cmdable) LRange(key string, start, stop int64) *StringSliceCmd { cmd := NewStringSliceCmd( "lrange", key, start, stop, ) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LRem(key string, count int64, value interface{}) *IntCmd { +func (c cmdable) LRem(key string, count int64, value interface{}) *IntCmd { cmd := NewIntCmd("lrem", key, count, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LSet(key string, index int64, value interface{}) *StatusCmd { +func (c cmdable) LSet(key string, index int64, value interface{}) *StatusCmd { cmd := NewStatusCmd("lset", key, index, value) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) LTrim(key string, start, stop int64) *StatusCmd { +func (c cmdable) LTrim(key string, start, stop int64) *StatusCmd { cmd := NewStatusCmd( "ltrim", key, start, stop, ) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) RPop(key string) *StringCmd { +func (c cmdable) RPop(key string) *StringCmd { cmd := NewStringCmd("rpop", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) RPopLPush(source, destination string) *StringCmd { +func (c cmdable) RPopLPush(source, destination string) *StringCmd { cmd := NewStringCmd("rpoplpush", source, destination) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) RPush(key string, values ...interface{}) *IntCmd { +func (c cmdable) RPush(key string, values ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(values)) args[0] = "rpush" args[1] = key args = appendArgs(args, values) cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) RPushX(key string, value interface{}) *IntCmd { - cmd := NewIntCmd("rpushx", key, value) - c.process(cmd) +func (c cmdable) RPushX(key string, values ...interface{}) *IntCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "rpushx" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntCmd(args...) + _ = c(cmd) return cmd } //------------------------------------------------------------------------------ -func (c *cmdable) SAdd(key string, members ...interface{}) *IntCmd { +func (c cmdable) SAdd(key string, members ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(members)) args[0] = "sadd" args[1] = key args = appendArgs(args, members) cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SCard(key string) *IntCmd { +func (c cmdable) SCard(key string) *IntCmd { cmd := NewIntCmd("scard", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SDiff(keys ...string) *StringSliceCmd { +func (c cmdable) SDiff(keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)) args[0] = "sdiff" for i, key := range keys { args[1+i] = key } cmd := NewStringSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SDiffStore(destination string, keys ...string) *IntCmd { +func (c cmdable) SDiffStore(destination string, keys ...string) *IntCmd { args := make([]interface{}, 2+len(keys)) args[0] = "sdiffstore" args[1] = destination @@ -1195,22 +1231,22 @@ func (c *cmdable) SDiffStore(destination string, keys ...string) *IntCmd { args[2+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SInter(keys ...string) *StringSliceCmd { +func (c cmdable) SInter(keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)) args[0] = "sinter" for i, key := range keys { args[1+i] = key } cmd := NewStringSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SInterStore(destination string, keys ...string) *IntCmd { +func (c cmdable) SInterStore(destination string, keys ...string) *IntCmd { args := make([]interface{}, 2+len(keys)) args[0] = "sinterstore" args[1] = destination @@ -1218,86 +1254,86 @@ func (c *cmdable) SInterStore(destination string, keys ...string) *IntCmd { args[2+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SIsMember(key string, member interface{}) *BoolCmd { +func (c cmdable) SIsMember(key string, member interface{}) *BoolCmd { cmd := NewBoolCmd("sismember", key, member) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SMEMBERS key` command output as a slice -func (c *cmdable) SMembers(key string) *StringSliceCmd { +func (c cmdable) SMembers(key string) *StringSliceCmd { cmd := NewStringSliceCmd("smembers", key) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SMEMBERS key` command output as a map -func (c *cmdable) SMembersMap(key string) *StringStructMapCmd { +func (c cmdable) SMembersMap(key string) *StringStructMapCmd { cmd := NewStringStructMapCmd("smembers", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SMove(source, destination string, member interface{}) *BoolCmd { +func (c cmdable) SMove(source, destination string, member interface{}) *BoolCmd { cmd := NewBoolCmd("smove", source, destination, member) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SPOP key` command. -func (c *cmdable) SPop(key string) *StringCmd { +func (c cmdable) SPop(key string) *StringCmd { cmd := NewStringCmd("spop", key) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SPOP key count` command. -func (c *cmdable) SPopN(key string, count int64) *StringSliceCmd { +func (c cmdable) SPopN(key string, count int64) *StringSliceCmd { cmd := NewStringSliceCmd("spop", key, count) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SRANDMEMBER key` command. -func (c *cmdable) SRandMember(key string) *StringCmd { +func (c cmdable) SRandMember(key string) *StringCmd { cmd := NewStringCmd("srandmember", key) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `SRANDMEMBER key count` command. -func (c *cmdable) SRandMemberN(key string, count int64) *StringSliceCmd { +func (c cmdable) SRandMemberN(key string, count int64) *StringSliceCmd { cmd := NewStringSliceCmd("srandmember", key, count) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SRem(key string, members ...interface{}) *IntCmd { +func (c cmdable) SRem(key string, members ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(members)) args[0] = "srem" args[1] = key args = appendArgs(args, members) cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SUnion(keys ...string) *StringSliceCmd { +func (c cmdable) SUnion(keys ...string) *StringSliceCmd { args := make([]interface{}, 1+len(keys)) args[0] = "sunion" for i, key := range keys { args[1+i] = key } cmd := NewStringSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) SUnionStore(destination string, keys ...string) *IntCmd { +func (c cmdable) SUnionStore(destination string, keys ...string) *IntCmd { args := make([]interface{}, 2+len(keys)) args[0] = "sunionstore" args[1] = destination @@ -1305,7 +1341,7 @@ func (c *cmdable) SUnionStore(destination string, keys ...string) *IntCmd { args[2+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } @@ -1319,7 +1355,7 @@ type XAddArgs struct { Values map[string]interface{} } -func (c *cmdable) XAdd(a *XAddArgs) *StringCmd { +func (c cmdable) XAdd(a *XAddArgs) *StringCmd { args := make([]interface{}, 0, 6+len(a.Values)*2) args = append(args, "xadd") args = append(args, a.Stream) @@ -1339,57 +1375,57 @@ func (c *cmdable) XAdd(a *XAddArgs) *StringCmd { } cmd := NewStringCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XDel(stream string, ids ...string) *IntCmd { +func (c cmdable) XDel(stream string, ids ...string) *IntCmd { args := []interface{}{"xdel", stream} for _, id := range ids { args = append(args, id) } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XLen(stream string) *IntCmd { +func (c cmdable) XLen(stream string) *IntCmd { cmd := NewIntCmd("xlen", stream) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XRange(stream, start, stop string) *XMessageSliceCmd { +func (c cmdable) XRange(stream, start, stop string) *XMessageSliceCmd { cmd := NewXMessageSliceCmd("xrange", stream, start, stop) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XRangeN(stream, start, stop string, count int64) *XMessageSliceCmd { +func (c cmdable) XRangeN(stream, start, stop string, count int64) *XMessageSliceCmd { cmd := NewXMessageSliceCmd("xrange", stream, start, stop, "count", count) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XRevRange(stream, start, stop string) *XMessageSliceCmd { +func (c cmdable) XRevRange(stream, start, stop string) *XMessageSliceCmd { cmd := NewXMessageSliceCmd("xrevrange", stream, start, stop) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XRevRangeN(stream, start, stop string, count int64) *XMessageSliceCmd { +func (c cmdable) XRevRangeN(stream, start, stop string, count int64) *XMessageSliceCmd { cmd := NewXMessageSliceCmd("xrevrange", stream, start, stop, "count", count) - c.process(cmd) + _ = c(cmd) return cmd } type XReadArgs struct { - Streams []string + Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2 Count int64 Block time.Duration } -func (c *cmdable) XRead(a *XReadArgs) *XStreamSliceCmd { +func (c cmdable) XRead(a *XReadArgs) *XStreamSliceCmd { args := make([]interface{}, 0, 5+len(a.Streams)) args = append(args, "xread") if a.Count > 0 { @@ -1400,6 +1436,7 @@ func (c *cmdable) XRead(a *XReadArgs) *XStreamSliceCmd { args = append(args, "block") args = append(args, int64(a.Block/time.Millisecond)) } + args = append(args, "streams") for _, s := range a.Streams { args = append(args, s) @@ -1409,58 +1446,57 @@ func (c *cmdable) XRead(a *XReadArgs) *XStreamSliceCmd { if a.Block >= 0 { cmd.setReadTimeout(a.Block) } - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XReadStreams(streams ...string) *XStreamSliceCmd { +func (c cmdable) XReadStreams(streams ...string) *XStreamSliceCmd { return c.XRead(&XReadArgs{ Streams: streams, Block: -1, }) } -func (c *cmdable) XGroupCreate(stream, group, start string) *StatusCmd { +func (c cmdable) XGroupCreate(stream, group, start string) *StatusCmd { cmd := NewStatusCmd("xgroup", "create", stream, group, start) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XGroupCreateMkStream(stream, group, start string) *StatusCmd { +func (c cmdable) XGroupCreateMkStream(stream, group, start string) *StatusCmd { cmd := NewStatusCmd("xgroup", "create", stream, group, start, "mkstream") - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XGroupSetID(stream, group, start string) *StatusCmd { +func (c cmdable) XGroupSetID(stream, group, start string) *StatusCmd { cmd := NewStatusCmd("xgroup", "setid", stream, group, start) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XGroupDestroy(stream, group string) *IntCmd { +func (c cmdable) XGroupDestroy(stream, group string) *IntCmd { cmd := NewIntCmd("xgroup", "destroy", stream, group) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XGroupDelConsumer(stream, group, consumer string) *IntCmd { +func (c cmdable) XGroupDelConsumer(stream, group, consumer string) *IntCmd { cmd := NewIntCmd("xgroup", "delconsumer", stream, group, consumer) - c.process(cmd) + _ = c(cmd) return cmd } type XReadGroupArgs struct { Group string Consumer string - // List of streams and ids. - Streams []string - Count int64 - Block time.Duration - NoAck bool + Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2 + Count int64 + Block time.Duration + NoAck bool } -func (c *cmdable) XReadGroup(a *XReadGroupArgs) *XStreamSliceCmd { +func (c cmdable) XReadGroup(a *XReadGroupArgs) *XStreamSliceCmd { args := make([]interface{}, 0, 8+len(a.Streams)) args = append(args, "xreadgroup", "group", a.Group, a.Consumer) if a.Count > 0 { @@ -1481,23 +1517,23 @@ func (c *cmdable) XReadGroup(a *XReadGroupArgs) *XStreamSliceCmd { if a.Block >= 0 { cmd.setReadTimeout(a.Block) } - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XAck(stream, group string, ids ...string) *IntCmd { +func (c cmdable) XAck(stream, group string, ids ...string) *IntCmd { args := []interface{}{"xack", stream, group} for _, id := range ids { args = append(args, id) } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XPending(stream, group string) *XPendingCmd { +func (c cmdable) XPending(stream, group string) *XPendingCmd { cmd := NewXPendingCmd("xpending", stream, group) - c.process(cmd) + _ = c(cmd) return cmd } @@ -1510,14 +1546,14 @@ type XPendingExtArgs struct { Consumer string } -func (c *cmdable) XPendingExt(a *XPendingExtArgs) *XPendingExtCmd { +func (c cmdable) XPendingExt(a *XPendingExtArgs) *XPendingExtCmd { args := make([]interface{}, 0, 7) args = append(args, "xpending", a.Stream, a.Group, a.Start, a.End, a.Count) if a.Consumer != "" { args = append(args, a.Consumer) } cmd := NewXPendingExtCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } @@ -1529,18 +1565,18 @@ type XClaimArgs struct { Messages []string } -func (c *cmdable) XClaim(a *XClaimArgs) *XMessageSliceCmd { +func (c cmdable) XClaim(a *XClaimArgs) *XMessageSliceCmd { args := xClaimArgs(a) cmd := NewXMessageSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XClaimJustID(a *XClaimArgs) *StringSliceCmd { +func (c cmdable) XClaimJustID(a *XClaimArgs) *StringSliceCmd { args := xClaimArgs(a) args = append(args, "justid") cmd := NewStringSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } @@ -1557,15 +1593,21 @@ func xClaimArgs(a *XClaimArgs) []interface{} { return args } -func (c *cmdable) XTrim(key string, maxLen int64) *IntCmd { +func (c cmdable) XTrim(key string, maxLen int64) *IntCmd { cmd := NewIntCmd("xtrim", key, "maxlen", maxLen) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) XTrimApprox(key string, maxLen int64) *IntCmd { +func (c cmdable) XTrimApprox(key string, maxLen int64) *IntCmd { cmd := NewIntCmd("xtrim", key, "maxlen", "~", maxLen) - c.process(cmd) + _ = c(cmd) + return cmd +} + +func (c cmdable) XInfoGroups(key string) *XInfoGroupsCmd { + cmd := NewXInfoGroupsCmd(key) + _ = c(cmd) return cmd } @@ -1585,13 +1627,14 @@ type ZWithKey struct { // ZStore is used as an arg to ZInterStore and ZUnionStore. type ZStore struct { + Keys []string Weights []float64 // Can be SUM, MIN or MAX. Aggregate string } // Redis `BZPOPMAX key [key ...] timeout` command. -func (c *cmdable) BZPopMax(timeout time.Duration, keys ...string) *ZWithKeyCmd { +func (c cmdable) BZPopMax(timeout time.Duration, keys ...string) *ZWithKeyCmd { args := make([]interface{}, 1+len(keys)+1) args[0] = "bzpopmax" for i, key := range keys { @@ -1600,12 +1643,12 @@ func (c *cmdable) BZPopMax(timeout time.Duration, keys ...string) *ZWithKeyCmd { args[len(args)-1] = formatSec(timeout) cmd := NewZWithKeyCmd(args...) cmd.setReadTimeout(timeout) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `BZPOPMIN key [key ...] timeout` command. -func (c *cmdable) BZPopMin(timeout time.Duration, keys ...string) *ZWithKeyCmd { +func (c cmdable) BZPopMin(timeout time.Duration, keys ...string) *ZWithKeyCmd { args := make([]interface{}, 1+len(keys)+1) args[0] = "bzpopmin" for i, key := range keys { @@ -1614,22 +1657,22 @@ func (c *cmdable) BZPopMin(timeout time.Duration, keys ...string) *ZWithKeyCmd { args[len(args)-1] = formatSec(timeout) cmd := NewZWithKeyCmd(args...) cmd.setReadTimeout(timeout) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) zAdd(a []interface{}, n int, members ...Z) *IntCmd { +func (c cmdable) zAdd(a []interface{}, n int, members ...*Z) *IntCmd { for i, m := range members { a[n+2*i] = m.Score a[n+2*i+1] = m.Member } cmd := NewIntCmd(a...) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `ZADD key score member [score member ...]` command. -func (c *cmdable) ZAdd(key string, members ...Z) *IntCmd { +func (c cmdable) ZAdd(key string, members ...*Z) *IntCmd { const n = 2 a := make([]interface{}, n+2*len(members)) a[0], a[1] = "zadd", key @@ -1637,7 +1680,7 @@ func (c *cmdable) ZAdd(key string, members ...Z) *IntCmd { } // Redis `ZADD key NX score member [score member ...]` command. -func (c *cmdable) ZAddNX(key string, members ...Z) *IntCmd { +func (c cmdable) ZAddNX(key string, members ...*Z) *IntCmd { const n = 3 a := make([]interface{}, n+2*len(members)) a[0], a[1], a[2] = "zadd", key, "nx" @@ -1645,7 +1688,7 @@ func (c *cmdable) ZAddNX(key string, members ...Z) *IntCmd { } // Redis `ZADD key XX score member [score member ...]` command. -func (c *cmdable) ZAddXX(key string, members ...Z) *IntCmd { +func (c cmdable) ZAddXX(key string, members ...*Z) *IntCmd { const n = 3 a := make([]interface{}, n+2*len(members)) a[0], a[1], a[2] = "zadd", key, "xx" @@ -1653,7 +1696,7 @@ func (c *cmdable) ZAddXX(key string, members ...Z) *IntCmd { } // Redis `ZADD key CH score member [score member ...]` command. -func (c *cmdable) ZAddCh(key string, members ...Z) *IntCmd { +func (c cmdable) ZAddCh(key string, members ...*Z) *IntCmd { const n = 3 a := make([]interface{}, n+2*len(members)) a[0], a[1], a[2] = "zadd", key, "ch" @@ -1661,7 +1704,7 @@ func (c *cmdable) ZAddCh(key string, members ...Z) *IntCmd { } // Redis `ZADD key NX CH score member [score member ...]` command. -func (c *cmdable) ZAddNXCh(key string, members ...Z) *IntCmd { +func (c cmdable) ZAddNXCh(key string, members ...*Z) *IntCmd { const n = 4 a := make([]interface{}, n+2*len(members)) a[0], a[1], a[2], a[3] = "zadd", key, "nx", "ch" @@ -1669,25 +1712,25 @@ func (c *cmdable) ZAddNXCh(key string, members ...Z) *IntCmd { } // Redis `ZADD key XX CH score member [score member ...]` command. -func (c *cmdable) ZAddXXCh(key string, members ...Z) *IntCmd { +func (c cmdable) ZAddXXCh(key string, members ...*Z) *IntCmd { const n = 4 a := make([]interface{}, n+2*len(members)) a[0], a[1], a[2], a[3] = "zadd", key, "xx", "ch" return c.zAdd(a, n, members...) } -func (c *cmdable) zIncr(a []interface{}, n int, members ...Z) *FloatCmd { +func (c cmdable) zIncr(a []interface{}, n int, members ...*Z) *FloatCmd { for i, m := range members { a[n+2*i] = m.Score a[n+2*i+1] = m.Member } cmd := NewFloatCmd(a...) - c.process(cmd) + _ = c(cmd) return cmd } // Redis `ZADD key INCR score member` command. -func (c *cmdable) ZIncr(key string, member Z) *FloatCmd { +func (c cmdable) ZIncr(key string, member *Z) *FloatCmd { const n = 3 a := make([]interface{}, n+2) a[0], a[1], a[2] = "zadd", key, "incr" @@ -1695,7 +1738,7 @@ func (c *cmdable) ZIncr(key string, member Z) *FloatCmd { } // Redis `ZADD key NX INCR score member` command. -func (c *cmdable) ZIncrNX(key string, member Z) *FloatCmd { +func (c cmdable) ZIncrNX(key string, member *Z) *FloatCmd { const n = 4 a := make([]interface{}, n+2) a[0], a[1], a[2], a[3] = "zadd", key, "incr", "nx" @@ -1703,43 +1746,43 @@ func (c *cmdable) ZIncrNX(key string, member Z) *FloatCmd { } // Redis `ZADD key XX INCR score member` command. -func (c *cmdable) ZIncrXX(key string, member Z) *FloatCmd { +func (c cmdable) ZIncrXX(key string, member *Z) *FloatCmd { const n = 4 a := make([]interface{}, n+2) a[0], a[1], a[2], a[3] = "zadd", key, "incr", "xx" return c.zIncr(a, n, member) } -func (c *cmdable) ZCard(key string) *IntCmd { +func (c cmdable) ZCard(key string) *IntCmd { cmd := NewIntCmd("zcard", key) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZCount(key, min, max string) *IntCmd { +func (c cmdable) ZCount(key, min, max string) *IntCmd { cmd := NewIntCmd("zcount", key, min, max) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZLexCount(key, min, max string) *IntCmd { +func (c cmdable) ZLexCount(key, min, max string) *IntCmd { cmd := NewIntCmd("zlexcount", key, min, max) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZIncrBy(key string, increment float64, member string) *FloatCmd { +func (c cmdable) ZIncrBy(key string, increment float64, member string) *FloatCmd { cmd := NewFloatCmd("zincrby", key, increment, member) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZInterStore(destination string, store ZStore, keys ...string) *IntCmd { - args := make([]interface{}, 3+len(keys)) +func (c cmdable) ZInterStore(destination string, store *ZStore) *IntCmd { + args := make([]interface{}, 3+len(store.Keys)) args[0] = "zinterstore" args[1] = destination - args[2] = len(keys) - for i, key := range keys { + args[2] = len(store.Keys) + for i, key := range store.Keys { args[3+i] = key } if len(store.Weights) > 0 { @@ -1752,11 +1795,11 @@ func (c *cmdable) ZInterStore(destination string, store ZStore, keys ...string) args = append(args, "aggregate", store.Aggregate) } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZPopMax(key string, count ...int64) *ZSliceCmd { +func (c cmdable) ZPopMax(key string, count ...int64) *ZSliceCmd { args := []interface{}{ "zpopmax", key, @@ -1772,11 +1815,11 @@ func (c *cmdable) ZPopMax(key string, count ...int64) *ZSliceCmd { } cmd := NewZSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZPopMin(key string, count ...int64) *ZSliceCmd { +func (c cmdable) ZPopMin(key string, count ...int64) *ZSliceCmd { args := []interface{}{ "zpopmin", key, @@ -1792,11 +1835,11 @@ func (c *cmdable) ZPopMin(key string, count ...int64) *ZSliceCmd { } cmd := NewZSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) zRange(key string, start, stop int64, withScores bool) *StringSliceCmd { +func (c cmdable) zRange(key string, start, stop int64, withScores bool) *StringSliceCmd { args := []interface{}{ "zrange", key, @@ -1807,17 +1850,17 @@ func (c *cmdable) zRange(key string, start, stop int64, withScores bool) *String args = append(args, "withscores") } cmd := NewStringSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRange(key string, start, stop int64) *StringSliceCmd { +func (c cmdable) ZRange(key string, start, stop int64) *StringSliceCmd { return c.zRange(key, start, stop, false) } -func (c *cmdable) ZRangeWithScores(key string, start, stop int64) *ZSliceCmd { +func (c cmdable) ZRangeWithScores(key string, start, stop int64) *ZSliceCmd { cmd := NewZSliceCmd("zrange", key, start, stop, "withscores") - c.process(cmd) + _ = c(cmd) return cmd } @@ -1826,7 +1869,7 @@ type ZRangeBy struct { Offset, Count int64 } -func (c *cmdable) zRangeBy(zcmd, key string, opt ZRangeBy, withScores bool) *StringSliceCmd { +func (c cmdable) zRangeBy(zcmd, key string, opt *ZRangeBy, withScores bool) *StringSliceCmd { args := []interface{}{zcmd, key, opt.Min, opt.Max} if withScores { args = append(args, "withscores") @@ -1840,19 +1883,19 @@ func (c *cmdable) zRangeBy(zcmd, key string, opt ZRangeBy, withScores bool) *Str ) } cmd := NewStringSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRangeByScore(key string, opt ZRangeBy) *StringSliceCmd { +func (c cmdable) ZRangeByScore(key string, opt *ZRangeBy) *StringSliceCmd { return c.zRangeBy("zrangebyscore", key, opt, false) } -func (c *cmdable) ZRangeByLex(key string, opt ZRangeBy) *StringSliceCmd { +func (c cmdable) ZRangeByLex(key string, opt *ZRangeBy) *StringSliceCmd { return c.zRangeBy("zrangebylex", key, opt, false) } -func (c *cmdable) ZRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCmd { +func (c cmdable) ZRangeByScoreWithScores(key string, opt *ZRangeBy) *ZSliceCmd { args := []interface{}{"zrangebyscore", key, opt.Min, opt.Max, "withscores"} if opt.Offset != 0 || opt.Count != 0 { args = append( @@ -1863,62 +1906,62 @@ func (c *cmdable) ZRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCmd { ) } cmd := NewZSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRank(key, member string) *IntCmd { +func (c cmdable) ZRank(key, member string) *IntCmd { cmd := NewIntCmd("zrank", key, member) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRem(key string, members ...interface{}) *IntCmd { +func (c cmdable) ZRem(key string, members ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(members)) args[0] = "zrem" args[1] = key args = appendArgs(args, members) cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRemRangeByRank(key string, start, stop int64) *IntCmd { +func (c cmdable) ZRemRangeByRank(key string, start, stop int64) *IntCmd { cmd := NewIntCmd( "zremrangebyrank", key, start, stop, ) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRemRangeByScore(key, min, max string) *IntCmd { +func (c cmdable) ZRemRangeByScore(key, min, max string) *IntCmd { cmd := NewIntCmd("zremrangebyscore", key, min, max) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRemRangeByLex(key, min, max string) *IntCmd { +func (c cmdable) ZRemRangeByLex(key, min, max string) *IntCmd { cmd := NewIntCmd("zremrangebylex", key, min, max) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRevRange(key string, start, stop int64) *StringSliceCmd { +func (c cmdable) ZRevRange(key string, start, stop int64) *StringSliceCmd { cmd := NewStringSliceCmd("zrevrange", key, start, stop) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRevRangeWithScores(key string, start, stop int64) *ZSliceCmd { +func (c cmdable) ZRevRangeWithScores(key string, start, stop int64) *ZSliceCmd { cmd := NewZSliceCmd("zrevrange", key, start, stop, "withscores") - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) zRevRangeBy(zcmd, key string, opt ZRangeBy) *StringSliceCmd { +func (c cmdable) zRevRangeBy(zcmd, key string, opt *ZRangeBy) *StringSliceCmd { args := []interface{}{zcmd, key, opt.Max, opt.Min} if opt.Offset != 0 || opt.Count != 0 { args = append( @@ -1929,19 +1972,19 @@ func (c *cmdable) zRevRangeBy(zcmd, key string, opt ZRangeBy) *StringSliceCmd { ) } cmd := NewStringSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRevRangeByScore(key string, opt ZRangeBy) *StringSliceCmd { +func (c cmdable) ZRevRangeByScore(key string, opt *ZRangeBy) *StringSliceCmd { return c.zRevRangeBy("zrevrangebyscore", key, opt) } -func (c *cmdable) ZRevRangeByLex(key string, opt ZRangeBy) *StringSliceCmd { +func (c cmdable) ZRevRangeByLex(key string, opt *ZRangeBy) *StringSliceCmd { return c.zRevRangeBy("zrevrangebylex", key, opt) } -func (c *cmdable) ZRevRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCmd { +func (c cmdable) ZRevRangeByScoreWithScores(key string, opt *ZRangeBy) *ZSliceCmd { args := []interface{}{"zrevrangebyscore", key, opt.Max, opt.Min, "withscores"} if opt.Offset != 0 || opt.Count != 0 { args = append( @@ -1952,28 +1995,28 @@ func (c *cmdable) ZRevRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCm ) } cmd := NewZSliceCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZRevRank(key, member string) *IntCmd { +func (c cmdable) ZRevRank(key, member string) *IntCmd { cmd := NewIntCmd("zrevrank", key, member) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZScore(key, member string) *FloatCmd { +func (c cmdable) ZScore(key, member string) *FloatCmd { cmd := NewFloatCmd("zscore", key, member) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ZUnionStore(dest string, store ZStore, keys ...string) *IntCmd { - args := make([]interface{}, 3+len(keys)) +func (c cmdable) ZUnionStore(dest string, store *ZStore) *IntCmd { + args := make([]interface{}, 3+len(store.Keys)) args[0] = "zunionstore" args[1] = dest - args[2] = len(keys) - for i, key := range keys { + args[2] = len(store.Keys) + for i, key := range store.Keys { args[3+i] = key } if len(store.Weights) > 0 { @@ -1986,34 +2029,34 @@ func (c *cmdable) ZUnionStore(dest string, store ZStore, keys ...string) *IntCmd args = append(args, "aggregate", store.Aggregate) } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } //------------------------------------------------------------------------------ -func (c *cmdable) PFAdd(key string, els ...interface{}) *IntCmd { +func (c cmdable) PFAdd(key string, els ...interface{}) *IntCmd { args := make([]interface{}, 2, 2+len(els)) args[0] = "pfadd" args[1] = key args = appendArgs(args, els) cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) PFCount(keys ...string) *IntCmd { +func (c cmdable) PFCount(keys ...string) *IntCmd { args := make([]interface{}, 1+len(keys)) args[0] = "pfcount" for i, key := range keys { args[1+i] = key } cmd := NewIntCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) PFMerge(dest string, keys ...string) *StatusCmd { +func (c cmdable) PFMerge(dest string, keys ...string) *StatusCmd { args := make([]interface{}, 2+len(keys)) args[0] = "pfmerge" args[1] = dest @@ -2021,33 +2064,33 @@ func (c *cmdable) PFMerge(dest string, keys ...string) *StatusCmd { args[2+i] = key } cmd := NewStatusCmd(args...) - c.process(cmd) + _ = c(cmd) return cmd } //------------------------------------------------------------------------------ -func (c *cmdable) BgRewriteAOF() *StatusCmd { +func (c cmdable) BgRewriteAOF() *StatusCmd { cmd := NewStatusCmd("bgrewriteaof") - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) BgSave() *StatusCmd { +func (c cmdable) BgSave() *StatusCmd { cmd := NewStatusCmd("bgsave") - c.process(cmd) + _ = c(cmd) return cmd } -func (c *cmdable) ClientKill(ipPort string) *StatusCmd { +func (c cmdable) ClientKill(ipPort string) *StatusCmd { cmd := NewStatusCmd("client", "kill", ipPort) - c.process(cmd) + _ = c(cmd) return cmd } // ClientKillByFilter is new style synx, while the ClientKill is old // CLIENT KILL