* add redis queue * finished indexer redis queue * add redis vendor * fix vet * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: lunny <xiaolunwen@gmail.com> * switch to go mod * Update required changes for new logging func signaturestags/v1.9.0-rc1
; When ISSUE_INDEXER_QUEUE_TYPE is levelqueue, this will be the queue will be saved path, | ; When ISSUE_INDEXER_QUEUE_TYPE is levelqueue, this will be the queue will be saved path, | ||||
; default is indexers/issues.queue | ; default is indexers/issues.queue | ||||
ISSUE_INDEXER_QUEUE_DIR = indexers/issues.queue | ISSUE_INDEXER_QUEUE_DIR = indexers/issues.queue | ||||
; When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string. | |||||
ISSUE_INDEXER_QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0" | |||||
; Batch queue number, default is 20 | ; Batch queue number, default is 20 | ||||
ISSUE_INDEXER_QUEUE_BATCH_NUMBER = 20 | ISSUE_INDEXER_QUEUE_BATCH_NUMBER = 20 | ||||
- `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently support: bleve or db, if it's db, below issue indexer item will be invalid. | - `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently support: bleve or db, if it's db, below issue indexer item will be invalid. | ||||
- `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search. | - `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search. | ||||
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: Issue indexer queue, currently support: channel or levelqueue | |||||
- `ISSUE_INDEXER_QUEUE_DIR`: **indexers/issues.queue**: When ISSUE_INDEXER_QUEUE_TYPE is levelqueue, this will be the queue will be saved path | |||||
- `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: Batch queue number | |||||
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: Issue indexer queue, currently supports:`channel`, `levelqueue`, `redis`. | |||||
- `ISSUE_INDEXER_QUEUE_DIR`: **indexers/issues.queue**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the queue will be saved path. | |||||
- `ISSUE_INDEXER_QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string. | |||||
- `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: Batch queue number. | |||||
- `REPO_INDEXER_ENABLED`: **false**: Enables code search (uses a lot of disk space, about 6 times more than the repository size). | - `REPO_INDEXER_ENABLED`: **false**: Enables code search (uses a lot of disk space, about 6 times more than the repository size). | ||||
- `REPO_INDEXER_PATH`: **indexers/repos.bleve**: Index file used for code search. | - `REPO_INDEXER_PATH`: **indexers/repos.bleve**: Index file used for code search. |
- `PATH`: Tidb 或者 SQLite3 数据文件存放路径。 | - `PATH`: Tidb 或者 SQLite3 数据文件存放路径。 | ||||
- `LOG_SQL`: **true**: 显示生成的SQL,默认为真。 | - `LOG_SQL`: **true**: 显示生成的SQL,默认为真。 | ||||
## Indexer (`indexer`) | ## Indexer (`indexer`) | ||||
- `ISSUE_INDEXER_TYPE`: **bleve**: 工单索引类型,当前支持 `bleve` 或 `db`,当为 `db` 时其它工单索引项可不用设置。 | - `ISSUE_INDEXER_TYPE`: **bleve**: 工单索引类型,当前支持 `bleve` 或 `db`,当为 `db` 时其它工单索引项可不用设置。 | ||||
- `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: 工单索引文件存放路径,当索引类型为 `bleve` 时有效。 | - `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: 工单索引文件存放路径,当索引类型为 `bleve` 时有效。 | ||||
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: 工单索引队列类型,当前支持 `channel` 或 `levelqueue`。 | |||||
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: 工单索引队列类型,当前支持 `channel`, `levelqueue` 或 `redis`。 | |||||
- `ISSUE_INDEXER_QUEUE_DIR`: **indexers/issues.queue**: 当 `ISSUE_INDEXER_QUEUE_TYPE` 为 `levelqueue` 时,保存索引队列的磁盘路径。 | - `ISSUE_INDEXER_QUEUE_DIR`: **indexers/issues.queue**: 当 `ISSUE_INDEXER_QUEUE_TYPE` 为 `levelqueue` 时,保存索引队列的磁盘路径。 | ||||
- `ISSUE_INDEXER_QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: 当 `ISSUE_INDEXER_QUEUE_TYPE` 为 `redis` 时,保存Redis队列的连接字符串。 | |||||
- `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: 队列处理中批量提交数量。 | - `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: 队列处理中批量提交数量。 | ||||
- `REPO_INDEXER_ENABLED`: **false**: 是否启用代码搜索(启用后会占用比较大的磁盘空间)。 | - `REPO_INDEXER_ENABLED`: **false**: 是否启用代码搜索(启用后会占用比较大的磁盘空间)。 |
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 | github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 | ||||
github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 | github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 | ||||
github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90 | github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90 | ||||
github.com/go-redis/redis v6.15.2+incompatible | |||||
github.com/go-sql-driver/mysql v1.4.0 | github.com/go-sql-driver/mysql v1.4.0 | ||||
github.com/go-xorm/builder v0.3.3 | github.com/go-xorm/builder v0.3.3 | ||||
github.com/go-xorm/core v0.6.0 | github.com/go-xorm/core v0.6.0 |
github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193/go.mod h1:ScEJm9Gk+ez5JJTml5WlBIqavAfuE5nF8e4Gvyz/X+A= | github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193/go.mod h1:ScEJm9Gk+ez5JJTml5WlBIqavAfuE5nF8e4Gvyz/X+A= | ||||
github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90 h1:3wYKrRg9IjUMfaf3H0Hh7M5Li9ge79Y7aw2yujHa2jQ= | github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90 h1:3wYKrRg9IjUMfaf3H0Hh7M5Li9ge79Y7aw2yujHa2jQ= | ||||
github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90/go.mod h1:Ut/NmkIMGVYlEdJBzEZgWVWG5ZpYS9BLmUgXfAgi+qM= | github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90/go.mod h1:Ut/NmkIMGVYlEdJBzEZgWVWG5ZpYS9BLmUgXfAgi+qM= | ||||
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-sql-driver/mysql v0.0.0-20181218123637-c45f530f8e7f h1:fbIzwEaXt5b2bl9mm+PIufKTSGKk6ZuwSSTQ7iZj7Lo= | github.com/go-sql-driver/mysql v0.0.0-20181218123637-c45f530f8e7f h1:fbIzwEaXt5b2bl9mm+PIufKTSGKk6ZuwSSTQ7iZj7Lo= | ||||
github.com/go-sql-driver/mysql v0.0.0-20181218123637-c45f530f8e7f/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | github.com/go-sql-driver/mysql v0.0.0-20181218123637-c45f530f8e7f/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | ||||
github.com/go-xorm/builder v0.3.2/go.mod h1:v8mE3MFBgtL+RGFNfUnAMUqqfk/Y4W5KuwCFQIEpQLk= | github.com/go-xorm/builder v0.3.2/go.mod h1:v8mE3MFBgtL+RGFNfUnAMUqqfk/Y4W5KuwCFQIEpQLk= |
} | } | ||||
var ( | var ( | ||||
// issueIndexerUpdateQueue queue of issue ids to be updated | |||||
issueIndexerUpdateQueue Queue | |||||
issueIndexer Indexer | |||||
// issueIndexerQueue queue of issue ids to be updated | |||||
issueIndexerQueue Queue | |||||
issueIndexer Indexer | |||||
) | ) | ||||
// InitIssueIndexer initialize issue indexer, syncReindex is true then reindex until | // InitIssueIndexer initialize issue indexer, syncReindex is true then reindex until | ||||
} | } | ||||
if dummyQueue { | if dummyQueue { | ||||
issueIndexerUpdateQueue = &DummyQueue{} | |||||
issueIndexerQueue = &DummyQueue{} | |||||
return nil | return nil | ||||
} | } | ||||
var err error | var err error | ||||
switch setting.Indexer.IssueIndexerQueueType { | |||||
switch setting.Indexer.IssueQueueType { | |||||
case setting.LevelQueueType: | case setting.LevelQueueType: | ||||
issueIndexerUpdateQueue, err = NewLevelQueue( | |||||
issueIndexerQueue, err = NewLevelQueue( | |||||
issueIndexer, | issueIndexer, | ||||
setting.Indexer.IssueIndexerQueueDir, | |||||
setting.Indexer.IssueIndexerQueueBatchNumber) | |||||
setting.Indexer.IssueQueueDir, | |||||
setting.Indexer.IssueQueueBatchNumber) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
case setting.ChannelQueueType: | case setting.ChannelQueueType: | ||||
issueIndexerUpdateQueue = NewChannelQueue(issueIndexer, setting.Indexer.IssueIndexerQueueBatchNumber) | |||||
issueIndexerQueue = NewChannelQueue(issueIndexer, setting.Indexer.IssueQueueBatchNumber) | |||||
case setting.RedisQueueType: | |||||
addrs, pass, idx, err := parseConnStr(setting.Indexer.IssueQueueConnStr) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
issueIndexerQueue, err = NewRedisQueue(addrs, pass, idx, issueIndexer, setting.Indexer.IssueQueueBatchNumber) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
default: | default: | ||||
return fmt.Errorf("Unsupported indexer queue type: %v", setting.Indexer.IssueIndexerQueueType) | |||||
return fmt.Errorf("Unsupported indexer queue type: %v", setting.Indexer.IssueQueueType) | |||||
} | } | ||||
go issueIndexerUpdateQueue.Run() | |||||
go issueIndexerQueue.Run() | |||||
if populate { | if populate { | ||||
if syncReindex { | if syncReindex { | ||||
comments = append(comments, comment.Content) | comments = append(comments, comment.Content) | ||||
} | } | ||||
} | } | ||||
issueIndexerUpdateQueue.Push(&IndexerData{ | |||||
issueIndexerQueue.Push(&IndexerData{ | |||||
ID: issue.ID, | ID: issue.ID, | ||||
RepoID: issue.RepoID, | RepoID: issue.RepoID, | ||||
Title: issue.Title, | Title: issue.Title, | ||||
return | return | ||||
} | } | ||||
issueIndexerUpdateQueue.Push(&IndexerData{ | |||||
issueIndexerQueue.Push(&IndexerData{ | |||||
IDs: ids, | IDs: ids, | ||||
IsDelete: true, | IsDelete: true, | ||||
}) | }) |
func TestBleveSearchIssues(t *testing.T) { | func TestBleveSearchIssues(t *testing.T) { | ||||
assert.NoError(t, models.PrepareTestDatabase()) | assert.NoError(t, models.PrepareTestDatabase()) | ||||
os.RemoveAll(setting.Indexer.IssueIndexerQueueDir) | |||||
os.RemoveAll(setting.Indexer.IssueQueueDir) | |||||
os.RemoveAll(setting.Indexer.IssuePath) | os.RemoveAll(setting.Indexer.IssuePath) | ||||
setting.Indexer.IssueType = "bleve" | setting.Indexer.IssueType = "bleve" | ||||
if err := InitIssueIndexer(true); err != nil { | if err := InitIssueIndexer(true); err != nil { |
// Copyright 2019 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 issues | |||||
import ( | |||||
"encoding/json" | |||||
"errors" | |||||
"strconv" | |||||
"strings" | |||||
"time" | |||||
"code.gitea.io/gitea/modules/log" | |||||
"github.com/go-redis/redis" | |||||
) | |||||
var ( | |||||
_ Queue = &RedisQueue{} | |||||
) | |||||
type redisClient interface { | |||||
RPush(key string, args ...interface{}) *redis.IntCmd | |||||
LPop(key string) *redis.StringCmd | |||||
Ping() *redis.StatusCmd | |||||
} | |||||
// RedisQueue redis queue | |||||
type RedisQueue struct { | |||||
client redisClient | |||||
queueName string | |||||
indexer Indexer | |||||
batchNumber int | |||||
} | |||||
func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { | |||||
fields := strings.Fields(connStr) | |||||
for _, f := range fields { | |||||
items := strings.SplitN(f, "=", 2) | |||||
if len(items) < 2 { | |||||
continue | |||||
} | |||||
switch strings.ToLower(items[0]) { | |||||
case "addrs": | |||||
addrs = items[1] | |||||
case "password": | |||||
password = items[1] | |||||
case "db": | |||||
dbIdx, err = strconv.Atoi(items[1]) | |||||
if err != nil { | |||||
return | |||||
} | |||||
} | |||||
} | |||||
return | |||||
} | |||||
// NewRedisQueue creates single redis or cluster redis queue | |||||
func NewRedisQueue(addrs string, password string, dbIdx int, indexer Indexer, batchNumber int) (*RedisQueue, error) { | |||||
dbs := strings.Split(addrs, ",") | |||||
var queue = RedisQueue{ | |||||
queueName: "issue_indexer_queue", | |||||
indexer: indexer, | |||||
batchNumber: batchNumber, | |||||
} | |||||
if len(dbs) == 0 { | |||||
return nil, errors.New("no redis host found") | |||||
} else if len(dbs) == 1 { | |||||
queue.client = redis.NewClient(&redis.Options{ | |||||
Addr: strings.TrimSpace(dbs[0]), // use default Addr | |||||
Password: password, // no password set | |||||
DB: dbIdx, // use default DB | |||||
}) | |||||
} else { | |||||
queue.client = redis.NewClusterClient(&redis.ClusterOptions{ | |||||
Addrs: dbs, | |||||
}) | |||||
} | |||||
if err := queue.client.Ping().Err(); err != nil { | |||||
return nil, err | |||||
} | |||||
return &queue, nil | |||||
} | |||||
// Run runs the redis queue | |||||
func (r *RedisQueue) Run() error { | |||||
var i int | |||||
var datas = make([]*IndexerData, 0, r.batchNumber) | |||||
for { | |||||
bs, err := r.client.LPop(r.queueName).Bytes() | |||||
if err != nil && err != redis.Nil { | |||||
log.Error("LPop faile: %v", err) | |||||
time.Sleep(time.Millisecond * 100) | |||||
continue | |||||
} | |||||
i++ | |||||
if len(datas) > r.batchNumber || (len(datas) > 0 && i > 3) { | |||||
r.indexer.Index(datas) | |||||
datas = make([]*IndexerData, 0, r.batchNumber) | |||||
i = 0 | |||||
} | |||||
if len(bs) <= 0 { | |||||
time.Sleep(time.Millisecond * 100) | |||||
continue | |||||
} | |||||
var data IndexerData | |||||
err = json.Unmarshal(bs, &data) | |||||
if err != nil { | |||||
log.Error("Unmarshal: %v", err) | |||||
time.Sleep(time.Millisecond * 100) | |||||
continue | |||||
} | |||||
log.Trace("RedisQueue: task found: %#v", data) | |||||
if data.IsDelete { | |||||
if data.ID > 0 { | |||||
if err = r.indexer.Delete(data.ID); err != nil { | |||||
log.Error("indexer.Delete: %v", err) | |||||
} | |||||
} else if len(data.IDs) > 0 { | |||||
if err = r.indexer.Delete(data.IDs...); err != nil { | |||||
log.Error("indexer.Delete: %v", err) | |||||
} | |||||
} | |||||
time.Sleep(time.Millisecond * 100) | |||||
continue | |||||
} | |||||
datas = append(datas, &data) | |||||
time.Sleep(time.Millisecond * 100) | |||||
} | |||||
} | |||||
// Push implements Queue | |||||
func (r *RedisQueue) Push(data *IndexerData) error { | |||||
bs, err := json.Marshal(data) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return r.client.RPush(r.queueName, bs).Err() | |||||
} |
const ( | const ( | ||||
LevelQueueType = "levelqueue" | LevelQueueType = "levelqueue" | ||||
ChannelQueueType = "channel" | ChannelQueueType = "channel" | ||||
RedisQueueType = "redis" | |||||
) | ) | ||||
var ( | var ( | ||||
// Indexer settings | // Indexer settings | ||||
Indexer = struct { | Indexer = struct { | ||||
IssueType string | |||||
IssuePath string | |||||
RepoIndexerEnabled bool | |||||
RepoPath string | |||||
UpdateQueueLength int | |||||
MaxIndexerFileSize int64 | |||||
IssueIndexerQueueType string | |||||
IssueIndexerQueueDir string | |||||
IssueIndexerQueueBatchNumber int | |||||
IssueType string | |||||
IssuePath string | |||||
RepoIndexerEnabled bool | |||||
RepoPath string | |||||
UpdateQueueLength int | |||||
MaxIndexerFileSize int64 | |||||
IssueQueueType string | |||||
IssueQueueDir string | |||||
IssueQueueConnStr string | |||||
IssueQueueBatchNumber int | |||||
}{ | }{ | ||||
IssueType: "bleve", | |||||
IssuePath: "indexers/issues.bleve", | |||||
IssueIndexerQueueType: LevelQueueType, | |||||
IssueIndexerQueueDir: "indexers/issues.queue", | |||||
IssueIndexerQueueBatchNumber: 20, | |||||
IssueType: "bleve", | |||||
IssuePath: "indexers/issues.bleve", | |||||
IssueQueueType: LevelQueueType, | |||||
IssueQueueDir: "indexers/issues.queue", | |||||
IssueQueueConnStr: "", | |||||
IssueQueueBatchNumber: 20, | |||||
} | } | ||||
) | ) | ||||
} | } | ||||
Indexer.UpdateQueueLength = sec.Key("UPDATE_BUFFER_LEN").MustInt(20) | Indexer.UpdateQueueLength = sec.Key("UPDATE_BUFFER_LEN").MustInt(20) | ||||
Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024) | Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024) | ||||
Indexer.IssueIndexerQueueType = sec.Key("ISSUE_INDEXER_QUEUE_TYPE").MustString(LevelQueueType) | |||||
Indexer.IssueIndexerQueueDir = sec.Key("ISSUE_INDEXER_QUEUE_DIR").MustString(path.Join(AppDataPath, "indexers/issues.queue")) | |||||
Indexer.IssueIndexerQueueBatchNumber = sec.Key("ISSUE_INDEXER_QUEUE_BATCH_NUMBER").MustInt(20) | |||||
Indexer.IssueQueueType = sec.Key("ISSUE_INDEXER_QUEUE_TYPE").MustString(LevelQueueType) | |||||
Indexer.IssueQueueDir = sec.Key("ISSUE_INDEXER_QUEUE_DIR").MustString(path.Join(AppDataPath, "indexers/issues.queue")) | |||||
Indexer.IssueQueueConnStr = sec.Key("ISSUE_INDEXER_QUEUE_CONN_STR").MustString(path.Join(AppDataPath, "")) | |||||
Indexer.IssueQueueBatchNumber = sec.Key("ISSUE_INDEXER_QUEUE_BATCH_NUMBER").MustInt(20) | |||||
} | } |
*.rdb | |||||
testdata/*/ |
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 |
# 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 |
Copyright (c) 2013 The github.com/go-redis/redis Authors. | |||||
All rights reserved. | |||||
Redistribution and use in source and binary forms, with or without | |||||
modification, are permitted provided that the following conditions are | |||||
met: | |||||
* Redistributions of source code must retain the above copyright | |||||
notice, this list of conditions and the following disclaimer. | |||||
* Redistributions in binary form must reproduce the above | |||||
copyright notice, this list of conditions and the following disclaimer | |||||
in the documentation and/or other materials provided with the | |||||
distribution. | |||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
all: testdeps | |||||
go test ./... | |||||
go test ./... -short -race | |||||
env GOOS=linux GOARCH=386 go test ./... | |||||
go vet | |||||
go get github.com/gordonklaus/ineffassign | |||||
ineffassign . | |||||
testdeps: testdata/redis/src/redis-server | |||||
bench: testdeps | |||||
go test ./... -test.run=NONE -test.bench=. -test.benchmem | |||||
.PHONY: all test testdeps bench | |||||
testdata/redis: | |||||
mkdir -p $@ | |||||
wget -qO- https://github.com/antirez/redis/archive/5.0.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' $</src/Makefile | |||||
cd $< && make all |
# Redis client for Golang | |||||
[![Build Status](https://travis-ci.org/go-redis/redis.png?branch=master)](https://travis-ci.org/go-redis/redis) | |||||
[![GoDoc](https://godoc.org/github.com/go-redis/redis?status.svg)](https://godoc.org/github.com/go-redis/redis) | |||||
[![Airbrake](https://img.shields.io/badge/kudos-airbrake.io-orange.svg)](https://airbrake.io) | |||||
Supports: | |||||
- Redis 3 commands except QUIT, MONITOR, SLOWLOG and SYNC. | |||||
- Automatic connection pooling with [circuit breaker](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern) support. | |||||
- [Pub/Sub](https://godoc.org/github.com/go-redis/redis#PubSub). | |||||
- [Transactions](https://godoc.org/github.com/go-redis/redis#Multi). | |||||
- [Pipeline](https://godoc.org/github.com/go-redis/redis#example-Client-Pipeline) and [TxPipeline](https://godoc.org/github.com/go-redis/redis#example-Client-TxPipeline). | |||||
- [Scripting](https://godoc.org/github.com/go-redis/redis#Script). | |||||
- [Timeouts](https://godoc.org/github.com/go-redis/redis#Options). | |||||
- [Redis Sentinel](https://godoc.org/github.com/go-redis/redis#NewFailoverClient). | |||||
- [Redis Cluster](https://godoc.org/github.com/go-redis/redis#NewClusterClient). | |||||
- [Cluster of Redis Servers](https://godoc.org/github.com/go-redis/redis#example-NewClusterClient--ManualSetup) without using cluster mode and Redis Sentinel. | |||||
- [Ring](https://godoc.org/github.com/go-redis/redis#NewRing). | |||||
- [Instrumentation](https://godoc.org/github.com/go-redis/redis#ex-package--Instrumentation). | |||||
- [Cache friendly](https://github.com/go-redis/cache). | |||||
- [Rate limiting](https://github.com/go-redis/redis_rate). | |||||
- [Distributed Locks](https://github.com/bsm/redis-lock). | |||||
API docs: https://godoc.org/github.com/go-redis/redis. | |||||
Examples: https://godoc.org/github.com/go-redis/redis#pkg-examples. | |||||
## Installation | |||||
Install: | |||||
```shell | |||||
go get -u github.com/go-redis/redis | |||||
``` | |||||
Import: | |||||
```go | |||||
import "github.com/go-redis/redis" | |||||
``` | |||||
## Quickstart | |||||
```go | |||||
func ExampleNewClient() { | |||||
client := redis.NewClient(&redis.Options{ | |||||
Addr: "localhost:6379", | |||||
Password: "", // no password set | |||||
DB: 0, // use default DB | |||||
}) | |||||
pong, err := client.Ping().Result() | |||||
fmt.Println(pong, err) | |||||
// Output: PONG <nil> | |||||
} | |||||
func ExampleClient() { | |||||
err := client.Set("key", "value", 0).Err() | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
val, err := client.Get("key").Result() | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
fmt.Println("key", val) | |||||
val2, err := client.Get("key2").Result() | |||||
if err == redis.Nil { | |||||
fmt.Println("key2 does not exist") | |||||
} else if err != nil { | |||||
panic(err) | |||||
} else { | |||||
fmt.Println("key2", val2) | |||||
} | |||||
// Output: key value | |||||
// key2 does not exist | |||||
} | |||||
``` | |||||
## Howto | |||||
Please go through [examples](https://godoc.org/github.com/go-redis/redis#pkg-examples) to get an idea how to use this package. | |||||
## Look and feel | |||||
Some corner cases: | |||||
```go | |||||
// SET key value EX 10 NX | |||||
set, err := client.SetNX("key", "value", 10*time.Second).Result() | |||||
// SORT list LIMIT 0 2 ASC | |||||
vals, err := client.Sort("list", redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result() | |||||
// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2 | |||||
vals, err := client.ZRangeByScoreWithScores("zset", redis.ZRangeBy{ | |||||
Min: "-inf", | |||||
Max: "+inf", | |||||
Offset: 0, | |||||
Count: 2, | |||||
}).Result() | |||||
// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM | |||||
vals, err := client.ZInterStore("out", redis.ZStore{Weights: []int64{2, 3}}, "zset1", "zset2").Result() | |||||
// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello" | |||||
vals, err := client.Eval("return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result() | |||||
``` | |||||
## Benchmark | |||||
go-redis vs redigo: | |||||
``` | |||||
BenchmarkSetGoRedis10Conns64Bytes-4 200000 7621 ns/op 210 B/op 6 allocs/op | |||||
BenchmarkSetGoRedis100Conns64Bytes-4 200000 7554 ns/op 210 B/op 6 allocs/op | |||||
BenchmarkSetGoRedis10Conns1KB-4 200000 7697 ns/op 210 B/op 6 allocs/op | |||||
BenchmarkSetGoRedis100Conns1KB-4 200000 7688 ns/op 210 B/op 6 allocs/op | |||||
BenchmarkSetGoRedis10Conns10KB-4 200000 9214 ns/op 210 B/op 6 allocs/op | |||||
BenchmarkSetGoRedis100Conns10KB-4 200000 9181 ns/op 210 B/op 6 allocs/op | |||||
BenchmarkSetGoRedis10Conns1MB-4 2000 583242 ns/op 2337 B/op 6 allocs/op | |||||
BenchmarkSetGoRedis100Conns1MB-4 2000 583089 ns/op 2338 B/op 6 allocs/op | |||||
BenchmarkSetRedigo10Conns64Bytes-4 200000 7576 ns/op 208 B/op 7 allocs/op | |||||
BenchmarkSetRedigo100Conns64Bytes-4 200000 7782 ns/op 208 B/op 7 allocs/op | |||||
BenchmarkSetRedigo10Conns1KB-4 200000 7958 ns/op 208 B/op 7 allocs/op | |||||
BenchmarkSetRedigo100Conns1KB-4 200000 7725 ns/op 208 B/op 7 allocs/op | |||||
BenchmarkSetRedigo10Conns10KB-4 100000 18442 ns/op 208 B/op 7 allocs/op | |||||
BenchmarkSetRedigo100Conns10KB-4 100000 18818 ns/op 208 B/op 7 allocs/op | |||||
BenchmarkSetRedigo10Conns1MB-4 2000 668829 ns/op 226 B/op 7 allocs/op | |||||
BenchmarkSetRedigo100Conns1MB-4 2000 679542 ns/op 226 B/op 7 allocs/op | |||||
``` | |||||
Redis Cluster: | |||||
``` | |||||
BenchmarkRedisPing-4 200000 6983 ns/op 116 B/op 4 allocs/op | |||||
BenchmarkRedisClusterPing-4 100000 11535 ns/op 117 B/op 4 allocs/op | |||||
``` | |||||
## See also | |||||
- [Golang PostgreSQL ORM](https://github.com/go-pg/pg) | |||||
- [Golang msgpack](https://github.com/vmihailenco/msgpack) | |||||
- [Golang message task queue](https://github.com/go-msgqueue/msgqueue) |
package redis | |||||
import "sync/atomic" | |||||
func (c *ClusterClient) DBSize() *IntCmd { | |||||
cmd := NewIntCmd("dbsize") | |||||
var size int64 | |||||
err := c.ForEachMaster(func(master *Client) error { | |||||
n, err := master.DBSize().Result() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
atomic.AddInt64(&size, n) | |||||
return nil | |||||
}) | |||||
if err != nil { | |||||
cmd.setErr(err) | |||||
return cmd | |||||
} | |||||
cmd.val = size | |||||
return cmd | |||||
} |
/* | |||||
Package redis implements a Redis client. | |||||
*/ | |||||
package redis |
/* | |||||
Copyright 2013 Google Inc. | |||||
Licensed under the Apache License, Version 2.0 (the "License"); | |||||
you may not use this file except in compliance with the License. | |||||
You may obtain a copy of the License at | |||||
http://www.apache.org/licenses/LICENSE-2.0 | |||||
Unless required by applicable law or agreed to in writing, software | |||||
distributed under the License is distributed on an "AS IS" BASIS, | |||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
See the License for the specific language governing permissions and | |||||
limitations under the License. | |||||
*/ | |||||
// Package consistenthash provides an implementation of a ring hash. | |||||
package consistenthash | |||||
import ( | |||||
"hash/crc32" | |||||
"sort" | |||||
"strconv" | |||||
) | |||||
type Hash func(data []byte) uint32 | |||||
type Map struct { | |||||
hash Hash | |||||
replicas int | |||||
keys []int // Sorted | |||||
hashMap map[int]string | |||||
} | |||||
func New(replicas int, fn Hash) *Map { | |||||
m := &Map{ | |||||
replicas: replicas, | |||||
hash: fn, | |||||
hashMap: make(map[int]string), | |||||
} | |||||
if m.hash == nil { | |||||
m.hash = crc32.ChecksumIEEE | |||||
} | |||||
return m | |||||
} | |||||
// Returns true if there are no items available. | |||||
func (m *Map) IsEmpty() bool { | |||||
return len(m.keys) == 0 | |||||
} | |||||
// Adds some keys to the hash. | |||||
func (m *Map) Add(keys ...string) { | |||||
for _, key := range keys { | |||||
for i := 0; i < m.replicas; i++ { | |||||
hash := int(m.hash([]byte(strconv.Itoa(i) + key))) | |||||
m.keys = append(m.keys, hash) | |||||
m.hashMap[hash] = key | |||||
} | |||||
} | |||||
sort.Ints(m.keys) | |||||
} | |||||
// Gets the closest item in the hash to the provided key. | |||||
func (m *Map) Get(key string) string { | |||||
if m.IsEmpty() { | |||||
return "" | |||||
} | |||||
hash := int(m.hash([]byte(key))) | |||||
// Binary search for appropriate replica. | |||||
idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash }) | |||||
// Means we have cycled back to the first replica. | |||||
if idx == len(m.keys) { | |||||
idx = 0 | |||||
} | |||||
return m.hashMap[m.keys[idx]] | |||||
} |
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 ") | |||||
} |
package hashtag | |||||
import ( | |||||
"math/rand" | |||||
"strings" | |||||
) | |||||
const slotNumber = 16384 | |||||
// CRC16 implementation according to CCITT standards. | |||||
// Copyright 2001-2010 Georges Menie (www.menie.org) | |||||
// Copyright 2013 The Go Authors. All rights reserved. | |||||
// http://redis.io/topics/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c | |||||
var crc16tab = [256]uint16{ | |||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, | |||||
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, | |||||
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, | |||||
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, | |||||
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, | |||||
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, | |||||
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, | |||||
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, | |||||
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, | |||||
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, | |||||
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, | |||||
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, | |||||
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, | |||||
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, | |||||
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, | |||||
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, | |||||
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, | |||||
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, | |||||
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, | |||||
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, | |||||
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, | |||||
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, | |||||
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, | |||||
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, | |||||
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, | |||||
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, | |||||
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, | |||||
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, | |||||
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, | |||||
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, | |||||
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, | |||||
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, | |||||
} | |||||
func Key(key string) string { | |||||
if s := strings.IndexByte(key, '{'); s > -1 { | |||||
if e := strings.IndexByte(key[s+1:], '}'); e > 0 { | |||||
return key[s+1 : s+e+1] | |||||
} | |||||
} | |||||
return key | |||||
} | |||||
func RandomSlot() int { | |||||
return rand.Intn(slotNumber) | |||||
} | |||||
// hashSlot returns a consistent slot number between 0 and 16383 | |||||
// for any given string key. | |||||
func Slot(key string) int { | |||||
if key == "" { | |||||
return RandomSlot() | |||||
} | |||||
key = Key(key) | |||||
return int(crc16sum(key)) % slotNumber | |||||
} | |||||
func crc16sum(key string) (crc uint16) { | |||||
for i := 0; i < len(key); i++ { | |||||
crc = (crc << 8) ^ crc16tab[(byte(crc>>8)^key[i])&0x00ff] | |||||
} | |||||
return | |||||
} |
package internal | |||||
import ( | |||||
"math/rand" | |||||
"time" | |||||
) | |||||
// Retry backoff with jitter sleep to prevent overloaded conditions during intervals | |||||
// https://www.awsarchitectureblog.com/2015/03/backoff.html | |||||
func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration { | |||||
if retry < 0 { | |||||
retry = 0 | |||||
} | |||||
backoff := minBackoff << uint(retry) | |||||
if backoff > maxBackoff || backoff < minBackoff { | |||||
backoff = maxBackoff | |||||
} | |||||
if backoff == 0 { | |||||
return 0 | |||||
} | |||||
return time.Duration(rand.Int63n(int64(backoff))) | |||||
} |
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...)) | |||||
} |
/* | |||||
Copyright 2014 The Camlistore 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. | |||||
*/ | |||||
package internal | |||||
import ( | |||||
"sync" | |||||
"sync/atomic" | |||||
) | |||||
// A Once will perform a successful action exactly once. | |||||
// | |||||
// Unlike a sync.Once, this Once's func returns an error | |||||
// and is re-armed on failure. | |||||
type Once struct { | |||||
m sync.Mutex | |||||
done uint32 | |||||
} | |||||
// Do calls the function f if and only if Do has not been invoked | |||||
// without error for this instance of Once. In other words, given | |||||
// var once Once | |||||
// if once.Do(f) is called multiple times, only the first call will | |||||
// invoke f, even if f has a different value in each invocation unless | |||||
// f returns an error. A new instance of Once is required for each | |||||
// function to execute. | |||||
// | |||||
// Do is intended for initialization that must be run exactly once. Since f | |||||
// is niladic, it may be necessary to use a function literal to capture the | |||||
// arguments to a function to be invoked by Do: | |||||
// err := config.once.Do(func() error { return config.init(filename) }) | |||||
func (o *Once) Do(f func() error) error { | |||||
if atomic.LoadUint32(&o.done) == 1 { | |||||
return nil | |||||
} | |||||
// Slow-path. | |||||
o.m.Lock() | |||||
defer o.m.Unlock() | |||||
var err error | |||||
if o.done == 0 { | |||||
err = f() | |||||
if err == nil { | |||||
atomic.StoreUint32(&o.done, 1) | |||||
} | |||||
} | |||||
return err | |||||
} |
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() | |||||
} |
package pool | |||||
import ( | |||||
"errors" | |||||
"net" | |||||
"sync" | |||||
"sync/atomic" | |||||
"time" | |||||
"github.com/go-redis/redis/internal" | |||||
) | |||||
var ErrClosed = errors.New("redis: client is closed") | |||||
var ErrPoolTimeout = errors.New("redis: connection pool timeout") | |||||
var timers = sync.Pool{ | |||||
New: func() interface{} { | |||||
t := time.NewTimer(time.Hour) | |||||
t.Stop() | |||||
return t | |||||
}, | |||||
} | |||||
// Stats contains pool state information and accumulated stats. | |||||
type Stats struct { | |||||
Hits uint32 // number of times free connection was found in the pool | |||||
Misses uint32 // number of times free connection was NOT found in the pool | |||||
Timeouts uint32 // number of times a wait timeout occurred | |||||
TotalConns uint32 // number of total connections in the pool | |||||
IdleConns uint32 // number of idle connections in the pool | |||||
StaleConns uint32 // number of stale connections removed from the pool | |||||
} | |||||
type Pooler interface { | |||||
NewConn() (*Conn, error) | |||||
CloseConn(*Conn) error | |||||
Get() (*Conn, error) | |||||
Put(*Conn) | |||||
Remove(*Conn) | |||||
Len() int | |||||
IdleLen() int | |||||
Stats() *Stats | |||||
Close() error | |||||
} | |||||
type Options struct { | |||||
Dialer func() (net.Conn, error) | |||||
OnClose func(*Conn) error | |||||
PoolSize int | |||||
MinIdleConns int | |||||
MaxConnAge time.Duration | |||||
PoolTimeout time.Duration | |||||
IdleTimeout time.Duration | |||||
IdleCheckFrequency time.Duration | |||||
} | |||||
type ConnPool struct { | |||||
opt *Options | |||||
dialErrorsNum uint32 // atomic | |||||
lastDialErrorMu sync.RWMutex | |||||
lastDialError error | |||||
queue chan struct{} | |||||
connsMu sync.Mutex | |||||
conns []*Conn | |||||
idleConns []*Conn | |||||
poolSize int | |||||
idleConnsLen int | |||||
stats Stats | |||||
_closed uint32 // atomic | |||||
} | |||||
var _ Pooler = (*ConnPool)(nil) | |||||
func NewConnPool(opt *Options) *ConnPool { | |||||
p := &ConnPool{ | |||||
opt: opt, | |||||
queue: make(chan struct{}, opt.PoolSize), | |||||
conns: make([]*Conn, 0, opt.PoolSize), | |||||
idleConns: make([]*Conn, 0, opt.PoolSize), | |||||
} | |||||
for i := 0; i < opt.MinIdleConns; i++ { | |||||
p.checkMinIdleConns() | |||||
} | |||||
if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 { | |||||
go p.reaper(opt.IdleCheckFrequency) | |||||
} | |||||
return p | |||||
} | |||||
func (p *ConnPool) checkMinIdleConns() { | |||||
if p.opt.MinIdleConns == 0 { | |||||
return | |||||
} | |||||
if p.poolSize < p.opt.PoolSize && p.idleConnsLen < p.opt.MinIdleConns { | |||||
p.poolSize++ | |||||
p.idleConnsLen++ | |||||
go p.addIdleConn() | |||||
} | |||||
} | |||||
func (p *ConnPool) addIdleConn() { | |||||
cn, err := p.newConn(true) | |||||
if err != nil { | |||||
return | |||||
} | |||||
p.connsMu.Lock() | |||||
p.conns = append(p.conns, cn) | |||||
p.idleConns = append(p.idleConns, cn) | |||||
p.connsMu.Unlock() | |||||
} | |||||
func (p *ConnPool) NewConn() (*Conn, error) { | |||||
return p._NewConn(false) | |||||
} | |||||
func (p *ConnPool) _NewConn(pooled bool) (*Conn, error) { | |||||
cn, err := p.newConn(pooled) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
p.connsMu.Lock() | |||||
p.conns = append(p.conns, cn) | |||||
if pooled { | |||||
if p.poolSize < p.opt.PoolSize { | |||||
p.poolSize++ | |||||
} else { | |||||
cn.pooled = false | |||||
} | |||||
} | |||||
p.connsMu.Unlock() | |||||
return cn, nil | |||||
} | |||||
func (p *ConnPool) newConn(pooled bool) (*Conn, error) { | |||||
if p.closed() { | |||||
return nil, ErrClosed | |||||
} | |||||
if atomic.LoadUint32(&p.dialErrorsNum) >= uint32(p.opt.PoolSize) { | |||||
return nil, p.getLastDialError() | |||||
} | |||||
netConn, err := p.opt.Dialer() | |||||
if err != nil { | |||||
p.setLastDialError(err) | |||||
if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.opt.PoolSize) { | |||||
go p.tryDial() | |||||
} | |||||
return nil, err | |||||
} | |||||
cn := NewConn(netConn) | |||||
cn.pooled = pooled | |||||
return cn, nil | |||||
} | |||||
func (p *ConnPool) tryDial() { | |||||
for { | |||||
if p.closed() { | |||||
return | |||||
} | |||||
conn, err := p.opt.Dialer() | |||||
if err != nil { | |||||
p.setLastDialError(err) | |||||
time.Sleep(time.Second) | |||||
continue | |||||
} | |||||
atomic.StoreUint32(&p.dialErrorsNum, 0) | |||||
_ = conn.Close() | |||||
return | |||||
} | |||||
} | |||||
func (p *ConnPool) setLastDialError(err error) { | |||||
p.lastDialErrorMu.Lock() | |||||
p.lastDialError = err | |||||
p.lastDialErrorMu.Unlock() | |||||
} | |||||
func (p *ConnPool) getLastDialError() error { | |||||
p.lastDialErrorMu.RLock() | |||||
err := p.lastDialError | |||||
p.lastDialErrorMu.RUnlock() | |||||
return err | |||||
} | |||||
// Get returns existed connection from the pool or creates a new one. | |||||
func (p *ConnPool) Get() (*Conn, error) { | |||||
if p.closed() { | |||||
return nil, ErrClosed | |||||
} | |||||
err := p.waitTurn() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
for { | |||||
p.connsMu.Lock() | |||||
cn := p.popIdle() | |||||
p.connsMu.Unlock() | |||||
if cn == nil { | |||||
break | |||||
} | |||||
if p.isStaleConn(cn) { | |||||
_ = p.CloseConn(cn) | |||||
continue | |||||
} | |||||
atomic.AddUint32(&p.stats.Hits, 1) | |||||
return cn, nil | |||||
} | |||||
atomic.AddUint32(&p.stats.Misses, 1) | |||||
newcn, err := p._NewConn(true) | |||||
if err != nil { | |||||
p.freeTurn() | |||||
return nil, err | |||||
} | |||||
return newcn, nil | |||||
} | |||||
func (p *ConnPool) getTurn() { | |||||
p.queue <- struct{}{} | |||||
} | |||||
func (p *ConnPool) waitTurn() error { | |||||
select { | |||||
case p.queue <- struct{}{}: | |||||
return nil | |||||
default: | |||||
timer := timers.Get().(*time.Timer) | |||||
timer.Reset(p.opt.PoolTimeout) | |||||
select { | |||||
case p.queue <- struct{}{}: | |||||
if !timer.Stop() { | |||||
<-timer.C | |||||
} | |||||
timers.Put(timer) | |||||
return nil | |||||
case <-timer.C: | |||||
timers.Put(timer) | |||||
atomic.AddUint32(&p.stats.Timeouts, 1) | |||||
return ErrPoolTimeout | |||||
} | |||||
} | |||||
} | |||||
func (p *ConnPool) freeTurn() { | |||||
<-p.queue | |||||
} | |||||
func (p *ConnPool) popIdle() *Conn { | |||||
if len(p.idleConns) == 0 { | |||||
return nil | |||||
} | |||||
idx := len(p.idleConns) - 1 | |||||
cn := p.idleConns[idx] | |||||
p.idleConns = p.idleConns[:idx] | |||||
p.idleConnsLen-- | |||||
p.checkMinIdleConns() | |||||
return cn | |||||
} | |||||
func (p *ConnPool) Put(cn *Conn) { | |||||
if !cn.pooled { | |||||
p.Remove(cn) | |||||
return | |||||
} | |||||
p.connsMu.Lock() | |||||
p.idleConns = append(p.idleConns, cn) | |||||
p.idleConnsLen++ | |||||
p.connsMu.Unlock() | |||||
p.freeTurn() | |||||
} | |||||
func (p *ConnPool) Remove(cn *Conn) { | |||||
p.removeConn(cn) | |||||
p.freeTurn() | |||||
_ = p.closeConn(cn) | |||||
} | |||||
func (p *ConnPool) CloseConn(cn *Conn) error { | |||||
p.removeConn(cn) | |||||
return p.closeConn(cn) | |||||
} | |||||
func (p *ConnPool) removeConn(cn *Conn) { | |||||
p.connsMu.Lock() | |||||
for i, c := range p.conns { | |||||
if c == cn { | |||||
p.conns = append(p.conns[:i], p.conns[i+1:]...) | |||||
if cn.pooled { | |||||
p.poolSize-- | |||||
p.checkMinIdleConns() | |||||
} | |||||
break | |||||
} | |||||
} | |||||
p.connsMu.Unlock() | |||||
} | |||||
func (p *ConnPool) closeConn(cn *Conn) error { | |||||
if p.opt.OnClose != nil { | |||||
_ = p.opt.OnClose(cn) | |||||
} | |||||
return cn.Close() | |||||
} | |||||
// Len returns total number of connections. | |||||
func (p *ConnPool) Len() int { | |||||
p.connsMu.Lock() | |||||
n := len(p.conns) | |||||
p.connsMu.Unlock() | |||||
return n | |||||
} | |||||
// IdleLen returns number of idle connections. | |||||
func (p *ConnPool) IdleLen() int { | |||||
p.connsMu.Lock() | |||||
n := p.idleConnsLen | |||||
p.connsMu.Unlock() | |||||
return n | |||||
} | |||||
func (p *ConnPool) Stats() *Stats { | |||||
idleLen := p.IdleLen() | |||||
return &Stats{ | |||||
Hits: atomic.LoadUint32(&p.stats.Hits), | |||||
Misses: atomic.LoadUint32(&p.stats.Misses), | |||||
Timeouts: atomic.LoadUint32(&p.stats.Timeouts), | |||||
TotalConns: uint32(p.Len()), | |||||
IdleConns: uint32(idleLen), | |||||
StaleConns: atomic.LoadUint32(&p.stats.StaleConns), | |||||
} | |||||
} | |||||
func (p *ConnPool) closed() bool { | |||||
return atomic.LoadUint32(&p._closed) == 1 | |||||
} | |||||
func (p *ConnPool) Filter(fn func(*Conn) bool) error { | |||||
var firstErr error | |||||
p.connsMu.Lock() | |||||
for _, cn := range p.conns { | |||||
if fn(cn) { | |||||
if err := p.closeConn(cn); err != nil && firstErr == nil { | |||||
firstErr = err | |||||
} | |||||
} | |||||
} | |||||
p.connsMu.Unlock() | |||||
return firstErr | |||||
} | |||||
func (p *ConnPool) Close() error { | |||||
if !atomic.CompareAndSwapUint32(&p._closed, 0, 1) { | |||||
return ErrClosed | |||||
} | |||||
var firstErr error | |||||
p.connsMu.Lock() | |||||
for _, cn := range p.conns { | |||||
if err := p.closeConn(cn); err != nil && firstErr == nil { | |||||
firstErr = err | |||||
} | |||||
} | |||||
p.conns = nil | |||||
p.poolSize = 0 | |||||
p.idleConns = nil | |||||
p.idleConnsLen = 0 | |||||
p.connsMu.Unlock() | |||||
return firstErr | |||||
} | |||||
func (p *ConnPool) reapStaleConn() *Conn { | |||||
if len(p.idleConns) == 0 { | |||||
return nil | |||||
} | |||||
cn := p.idleConns[0] | |||||
if !p.isStaleConn(cn) { | |||||
return nil | |||||
} | |||||
p.idleConns = append(p.idleConns[:0], p.idleConns[1:]...) | |||||
p.idleConnsLen-- | |||||
return cn | |||||
} | |||||
func (p *ConnPool) ReapStaleConns() (int, error) { | |||||
var n int | |||||
for { | |||||
p.getTurn() | |||||
p.connsMu.Lock() | |||||
cn := p.reapStaleConn() | |||||
p.connsMu.Unlock() | |||||
if cn != nil { | |||||
p.removeConn(cn) | |||||
} | |||||
p.freeTurn() | |||||
if cn != nil { | |||||
p.closeConn(cn) | |||||
n++ | |||||
} else { | |||||
break | |||||
} | |||||
} | |||||
return n, nil | |||||
} | |||||
func (p *ConnPool) reaper(frequency time.Duration) { | |||||
ticker := time.NewTicker(frequency) | |||||
defer ticker.Stop() | |||||
for range ticker.C { | |||||
if p.closed() { | |||||
break | |||||
} | |||||
n, err := p.ReapStaleConns() | |||||
if err != nil { | |||||
internal.Logf("ReapStaleConns failed: %s", err) | |||||
continue | |||||
} | |||||
atomic.AddUint32(&p.stats.StaleConns, uint32(n)) | |||||
} | |||||
} | |||||
func (p *ConnPool) isStaleConn(cn *Conn) bool { | |||||
if p.opt.IdleTimeout == 0 && p.opt.MaxConnAge == 0 { | |||||
return false | |||||
} | |||||
now := time.Now() | |||||
if p.opt.IdleTimeout > 0 && now.Sub(cn.UsedAt()) >= p.opt.IdleTimeout { | |||||
return true | |||||
} | |||||
if p.opt.MaxConnAge > 0 && now.Sub(cn.InitedAt) >= p.opt.MaxConnAge { | |||||
return true | |||||
} | |||||
return false | |||||
} |
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 | |||||
} |
package pool | |||||
import "sync" | |||||
type StickyConnPool struct { | |||||
pool *ConnPool | |||||
reusable bool | |||||
cn *Conn | |||||
closed bool | |||||
mu sync.Mutex | |||||
} | |||||
var _ Pooler = (*StickyConnPool)(nil) | |||||
func NewStickyConnPool(pool *ConnPool, reusable bool) *StickyConnPool { | |||||
return &StickyConnPool{ | |||||
pool: pool, | |||||
reusable: reusable, | |||||
} | |||||
} | |||||
func (p *StickyConnPool) NewConn() (*Conn, error) { | |||||
panic("not implemented") | |||||
} | |||||
func (p *StickyConnPool) CloseConn(*Conn) error { | |||||
panic("not implemented") | |||||
} | |||||
func (p *StickyConnPool) Get() (*Conn, error) { | |||||
p.mu.Lock() | |||||
defer p.mu.Unlock() | |||||
if p.closed { | |||||
return nil, ErrClosed | |||||
} | |||||
if p.cn != nil { | |||||
return p.cn, nil | |||||
} | |||||
cn, err := p.pool.Get() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
p.cn = cn | |||||
return cn, nil | |||||
} | |||||
func (p *StickyConnPool) putUpstream() { | |||||
p.pool.Put(p.cn) | |||||
p.cn = nil | |||||
} | |||||
func (p *StickyConnPool) Put(cn *Conn) {} | |||||
func (p *StickyConnPool) removeUpstream() { | |||||
p.pool.Remove(p.cn) | |||||
p.cn = nil | |||||
} | |||||
func (p *StickyConnPool) Remove(cn *Conn) { | |||||
p.removeUpstream() | |||||
} | |||||
func (p *StickyConnPool) Len() int { | |||||
p.mu.Lock() | |||||
defer p.mu.Unlock() | |||||
if p.cn == nil { | |||||
return 0 | |||||
} | |||||
return 1 | |||||
} | |||||
func (p *StickyConnPool) IdleLen() int { | |||||
p.mu.Lock() | |||||
defer p.mu.Unlock() | |||||
if p.cn == nil { | |||||
return 1 | |||||
} | |||||
return 0 | |||||
} | |||||
func (p *StickyConnPool) Stats() *Stats { | |||||
return nil | |||||
} | |||||
func (p *StickyConnPool) Close() error { | |||||
p.mu.Lock() | |||||
defer p.mu.Unlock() | |||||
if p.closed { | |||||
return ErrClosed | |||||
} | |||||
p.closed = true | |||||
if p.cn != nil { | |||||
if p.reusable { | |||||
p.putUpstream() | |||||
} else { | |||||
p.removeUpstream() | |||||
} | |||||
} | |||||
return nil | |||||
} |
package proto | |||||
import ( | |||||
"bufio" | |||||
"fmt" | |||||
"io" | |||||
"strconv" | |||||
"github.com/go-redis/redis/internal/util" | |||||
) | |||||
const ( | |||||
ErrorReply = '-' | |||||
StatusReply = '+' | |||||
IntReply = ':' | |||||
StringReply = '$' | |||||
ArrayReply = '*' | |||||
) | |||||
//------------------------------------------------------------------------------ | |||||
const Nil = RedisError("redis: nil") | |||||
type RedisError string | |||||
func (e RedisError) Error() string { return string(e) } | |||||
//------------------------------------------------------------------------------ | |||||
type MultiBulkParse func(*Reader, int64) (interface{}, error) | |||||
type Reader struct { | |||||
rd *bufio.Reader | |||||
_buf []byte | |||||
} | |||||
func NewReader(rd io.Reader) *Reader { | |||||
return &Reader{ | |||||
rd: bufio.NewReader(rd), | |||||
_buf: make([]byte, 64), | |||||
} | |||||
} | |||||
func (r *Reader) Reset(rd io.Reader) { | |||||
r.rd.Reset(rd) | |||||
} | |||||
func (r *Reader) ReadLine() ([]byte, error) { | |||||
line, isPrefix, err := r.rd.ReadLine() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if isPrefix { | |||||
return nil, bufio.ErrBufferFull | |||||
} | |||||
if len(line) == 0 { | |||||
return nil, fmt.Errorf("redis: reply is empty") | |||||
} | |||||
if isNilReply(line) { | |||||
return nil, Nil | |||||
} | |||||
return line, nil | |||||
} | |||||
func (r *Reader) ReadReply(m MultiBulkParse) (interface{}, error) { | |||||
line, err := r.ReadLine() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
switch line[0] { | |||||
case ErrorReply: | |||||
return nil, ParseErrorReply(line) | |||||
case StatusReply: | |||||
return string(line[1:]), nil | |||||
case IntReply: | |||||
return util.ParseInt(line[1:], 10, 64) | |||||
case StringReply: | |||||
return r.readStringReply(line) | |||||
case ArrayReply: | |||||
n, err := parseArrayLen(line) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return m(r, n) | |||||
} | |||||
return nil, fmt.Errorf("redis: can't parse %.100q", line) | |||||
} | |||||
func (r *Reader) ReadIntReply() (int64, error) { | |||||
line, err := r.ReadLine() | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
switch line[0] { | |||||
case ErrorReply: | |||||
return 0, ParseErrorReply(line) | |||||
case IntReply: | |||||
return util.ParseInt(line[1:], 10, 64) | |||||
default: | |||||
return 0, fmt.Errorf("redis: can't parse int reply: %.100q", line) | |||||
} | |||||
} | |||||
func (r *Reader) ReadString() (string, error) { | |||||
line, err := r.ReadLine() | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
switch line[0] { | |||||
case ErrorReply: | |||||
return "", ParseErrorReply(line) | |||||
case StringReply: | |||||
return r.readStringReply(line) | |||||
case StatusReply: | |||||
return string(line[1:]), nil | |||||
case IntReply: | |||||
return string(line[1:]), nil | |||||
default: | |||||
return "", fmt.Errorf("redis: can't parse reply=%.100q reading string", line) | |||||
} | |||||
} | |||||
func (r *Reader) readStringReply(line []byte) (string, error) { | |||||
if isNilReply(line) { | |||||
return "", Nil | |||||
} | |||||
replyLen, err := strconv.Atoi(string(line[1:])) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
b := make([]byte, replyLen+2) | |||||
_, err = io.ReadFull(r.rd, b) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
return util.BytesToString(b[:replyLen]), nil | |||||
} | |||||
func (r *Reader) ReadArrayReply(m MultiBulkParse) (interface{}, error) { | |||||
line, err := r.ReadLine() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
switch line[0] { | |||||
case ErrorReply: | |||||
return nil, ParseErrorReply(line) | |||||
case ArrayReply: | |||||
n, err := parseArrayLen(line) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return m(r, n) | |||||
default: | |||||
return nil, fmt.Errorf("redis: can't parse array reply: %.100q", line) | |||||
} | |||||
} | |||||
func (r *Reader) ReadArrayLen() (int64, error) { | |||||
line, err := r.ReadLine() | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
switch line[0] { | |||||
case ErrorReply: | |||||
return 0, ParseErrorReply(line) | |||||
case ArrayReply: | |||||
return parseArrayLen(line) | |||||
default: | |||||
return 0, fmt.Errorf("redis: can't parse array reply: %.100q", line) | |||||
} | |||||
} | |||||
func (r *Reader) ReadScanReply() ([]string, uint64, error) { | |||||
n, err := r.ReadArrayLen() | |||||
if err != nil { | |||||
return nil, 0, err | |||||
} | |||||
if n != 2 { | |||||
return nil, 0, fmt.Errorf("redis: got %d elements in scan reply, expected 2", n) | |||||
} | |||||
cursor, err := r.ReadUint() | |||||
if err != nil { | |||||
return nil, 0, err | |||||
} | |||||
n, err = r.ReadArrayLen() | |||||
if err != nil { | |||||
return nil, 0, err | |||||
} | |||||
keys := make([]string, n) | |||||
for i := int64(0); i < n; i++ { | |||||
key, err := r.ReadString() | |||||
if err != nil { | |||||
return nil, 0, err | |||||
} | |||||
keys[i] = key | |||||
} | |||||
return keys, cursor, err | |||||
} | |||||
func (r *Reader) ReadInt() (int64, error) { | |||||
b, err := r.readTmpBytesReply() | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return util.ParseInt(b, 10, 64) | |||||
} | |||||
func (r *Reader) ReadUint() (uint64, error) { | |||||
b, err := r.readTmpBytesReply() | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return util.ParseUint(b, 10, 64) | |||||
} | |||||
func (r *Reader) ReadFloatReply() (float64, error) { | |||||
b, err := r.readTmpBytesReply() | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return util.ParseFloat(b, 64) | |||||
} | |||||
func (r *Reader) readTmpBytesReply() ([]byte, error) { | |||||
line, err := r.ReadLine() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
switch line[0] { | |||||
case ErrorReply: | |||||
return nil, ParseErrorReply(line) | |||||
case StringReply: | |||||
return r._readTmpBytesReply(line) | |||||
case StatusReply: | |||||
return line[1:], nil | |||||
default: | |||||
return nil, fmt.Errorf("redis: can't parse string reply: %.100q", line) | |||||
} | |||||
} | |||||
func (r *Reader) _readTmpBytesReply(line []byte) ([]byte, error) { | |||||
if isNilReply(line) { | |||||
return nil, Nil | |||||
} | |||||
replyLen, err := strconv.Atoi(string(line[1:])) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
buf := r.buf(replyLen + 2) | |||||
_, err = io.ReadFull(r.rd, buf) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return buf[:replyLen], nil | |||||
} | |||||
func (r *Reader) buf(n int) []byte { | |||||
if d := n - cap(r._buf); d > 0 { | |||||
r._buf = append(r._buf, make([]byte, d)...) | |||||
} | |||||
return r._buf[:n] | |||||
} | |||||
func isNilReply(b []byte) bool { | |||||
return len(b) == 3 && | |||||
(b[0] == StringReply || b[0] == ArrayReply) && | |||||
b[1] == '-' && b[2] == '1' | |||||
} | |||||
func ParseErrorReply(line []byte) error { | |||||
return RedisError(string(line[1:])) | |||||
} | |||||
func parseArrayLen(line []byte) (int64, error) { | |||||
if isNilReply(line) { | |||||
return 0, Nil | |||||
} | |||||
return util.ParseInt(line[1:], 10, 64) | |||||
} |
package proto | |||||
import ( | |||||
"encoding" | |||||
"fmt" | |||||
"reflect" | |||||
"github.com/go-redis/redis/internal/util" | |||||
) | |||||
func Scan(b []byte, v interface{}) error { | |||||
switch v := v.(type) { | |||||
case nil: | |||||
return fmt.Errorf("redis: Scan(nil)") | |||||
case *string: | |||||
*v = util.BytesToString(b) | |||||
return nil | |||||
case *[]byte: | |||||
*v = b | |||||
return nil | |||||
case *int: | |||||
var err error | |||||
*v, err = util.Atoi(b) | |||||
return err | |||||
case *int8: | |||||
n, err := util.ParseInt(b, 10, 8) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = int8(n) | |||||
return nil | |||||
case *int16: | |||||
n, err := util.ParseInt(b, 10, 16) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = int16(n) | |||||
return nil | |||||
case *int32: | |||||
n, err := util.ParseInt(b, 10, 32) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = int32(n) | |||||
return nil | |||||
case *int64: | |||||
n, err := util.ParseInt(b, 10, 64) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = n | |||||
return nil | |||||
case *uint: | |||||
n, err := util.ParseUint(b, 10, 64) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = uint(n) | |||||
return nil | |||||
case *uint8: | |||||
n, err := util.ParseUint(b, 10, 8) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = uint8(n) | |||||
return nil | |||||
case *uint16: | |||||
n, err := util.ParseUint(b, 10, 16) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = uint16(n) | |||||
return nil | |||||
case *uint32: | |||||
n, err := util.ParseUint(b, 10, 32) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = uint32(n) | |||||
return nil | |||||
case *uint64: | |||||
n, err := util.ParseUint(b, 10, 64) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = n | |||||
return nil | |||||
case *float32: | |||||
n, err := util.ParseFloat(b, 32) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
*v = float32(n) | |||||
return err | |||||
case *float64: | |||||
var err error | |||||
*v, err = util.ParseFloat(b, 64) | |||||
return err | |||||
case *bool: | |||||
*v = len(b) == 1 && b[0] == '1' | |||||
return nil | |||||
case encoding.BinaryUnmarshaler: | |||||
return v.UnmarshalBinary(b) | |||||
default: | |||||
return fmt.Errorf( | |||||
"redis: can't unmarshal %T (consider implementing BinaryUnmarshaler)", v) | |||||
} | |||||
} | |||||
func ScanSlice(data []string, slice interface{}) error { | |||||
v := reflect.ValueOf(slice) | |||||
if !v.IsValid() { | |||||
return fmt.Errorf("redis: ScanSlice(nil)") | |||||
} | |||||
if v.Kind() != reflect.Ptr { | |||||
return fmt.Errorf("redis: ScanSlice(non-pointer %T)", slice) | |||||
} | |||||
v = v.Elem() | |||||
if v.Kind() != reflect.Slice { | |||||
return fmt.Errorf("redis: ScanSlice(non-slice %T)", slice) | |||||
} | |||||
next := makeSliceNextElemFunc(v) | |||||
for i, s := range data { | |||||
elem := next() | |||||
if err := Scan([]byte(s), elem.Addr().Interface()); err != nil { | |||||
err = fmt.Errorf("redis: ScanSlice index=%d value=%q failed: %s", i, s, err) | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func makeSliceNextElemFunc(v reflect.Value) func() reflect.Value { | |||||
elemType := v.Type().Elem() | |||||
if elemType.Kind() == reflect.Ptr { | |||||
elemType = elemType.Elem() | |||||
return func() reflect.Value { | |||||
if v.Len() < v.Cap() { | |||||
v.Set(v.Slice(0, v.Len()+1)) | |||||
elem := v.Index(v.Len() - 1) | |||||
if elem.IsNil() { | |||||
elem.Set(reflect.New(elemType)) | |||||
} | |||||
return elem.Elem() | |||||
} | |||||
elem := reflect.New(elemType) | |||||
v.Set(reflect.Append(v, elem)) | |||||
return elem.Elem() | |||||
} | |||||
} | |||||
zero := reflect.Zero(elemType) | |||||
return func() reflect.Value { | |||||
if v.Len() < v.Cap() { | |||||
v.Set(v.Slice(0, v.Len()+1)) | |||||
return v.Index(v.Len() - 1) | |||||
} | |||||
v.Set(reflect.Append(v, zero)) | |||||
return v.Index(v.Len() - 1) | |||||
} | |||||
} |
package proto | |||||
import ( | |||||
"bufio" | |||||
"encoding" | |||||
"fmt" | |||||
"io" | |||||
"strconv" | |||||
"github.com/go-redis/redis/internal/util" | |||||
) | |||||
type Writer struct { | |||||
wr *bufio.Writer | |||||
lenBuf []byte | |||||
numBuf []byte | |||||
} | |||||
func NewWriter(wr io.Writer) *Writer { | |||||
return &Writer{ | |||||
wr: bufio.NewWriter(wr), | |||||
lenBuf: make([]byte, 64), | |||||
numBuf: make([]byte, 64), | |||||
} | |||||
} | |||||
func (w *Writer) WriteArgs(args []interface{}) error { | |||||
err := w.wr.WriteByte(ArrayReply) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = w.writeLen(len(args)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
for _, arg := range args { | |||||
err := w.writeArg(arg) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
func (w *Writer) writeLen(n int) error { | |||||
w.lenBuf = strconv.AppendUint(w.lenBuf[:0], uint64(n), 10) | |||||
w.lenBuf = append(w.lenBuf, '\r', '\n') | |||||
_, err := w.wr.Write(w.lenBuf) | |||||
return err | |||||
} | |||||
func (w *Writer) writeArg(v interface{}) error { | |||||
switch v := v.(type) { | |||||
case nil: | |||||
return w.string("") | |||||
case string: | |||||
return w.string(v) | |||||
case []byte: | |||||
return w.bytes(v) | |||||
case int: | |||||
return w.int(int64(v)) | |||||
case int8: | |||||
return w.int(int64(v)) | |||||
case int16: | |||||
return w.int(int64(v)) | |||||
case int32: | |||||
return w.int(int64(v)) | |||||
case int64: | |||||
return w.int(v) | |||||
case uint: | |||||
return w.uint(uint64(v)) | |||||
case uint8: | |||||
return w.uint(uint64(v)) | |||||
case uint16: | |||||
return w.uint(uint64(v)) | |||||
case uint32: | |||||
return w.uint(uint64(v)) | |||||
case uint64: | |||||
return w.uint(v) | |||||
case float32: | |||||
return w.float(float64(v)) | |||||
case float64: | |||||
return w.float(v) | |||||
case bool: | |||||
if v { | |||||
return w.int(1) | |||||
} else { | |||||
return w.int(0) | |||||
} | |||||
case encoding.BinaryMarshaler: | |||||
b, err := v.MarshalBinary() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return w.bytes(b) | |||||
default: | |||||
return fmt.Errorf( | |||||
"redis: can't marshal %T (implement encoding.BinaryMarshaler)", v) | |||||
} | |||||
} | |||||
func (w *Writer) bytes(b []byte) error { | |||||
err := w.wr.WriteByte(StringReply) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = w.writeLen(len(b)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
_, err = w.wr.Write(b) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return w.crlf() | |||||
} | |||||
func (w *Writer) string(s string) error { | |||||
return w.bytes(util.StringToBytes(s)) | |||||
} | |||||
func (w *Writer) uint(n uint64) error { | |||||
w.numBuf = strconv.AppendUint(w.numBuf[:0], n, 10) | |||||
return w.bytes(w.numBuf) | |||||
} | |||||
func (w *Writer) int(n int64) error { | |||||
w.numBuf = strconv.AppendInt(w.numBuf[:0], n, 10) | |||||
return w.bytes(w.numBuf) | |||||
} | |||||
func (w *Writer) float(f float64) error { | |||||
w.numBuf = strconv.AppendFloat(w.numBuf[:0], f, 'f', -1, 64) | |||||
return w.bytes(w.numBuf) | |||||
} | |||||
func (w *Writer) crlf() error { | |||||
err := w.wr.WriteByte('\r') | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return w.wr.WriteByte('\n') | |||||
} | |||||
func (w *Writer) Reset(wr io.Writer) { | |||||
w.wr.Reset(wr) | |||||
} | |||||
func (w *Writer) Flush() error { | |||||
return w.wr.Flush() | |||||
} |
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 | |||||
} |
// +build appengine | |||||
package util | |||||
func BytesToString(b []byte) string { | |||||
return string(b) | |||||
} | |||||
func StringToBytes(s string) []byte { | |||||
return []byte(s) | |||||
} |
package util | |||||
import "strconv" | |||||
func Atoi(b []byte) (int, error) { | |||||
return strconv.Atoi(BytesToString(b)) | |||||
} | |||||
func ParseInt(b []byte, base int, bitSize int) (int64, error) { | |||||
return strconv.ParseInt(BytesToString(b), base, bitSize) | |||||
} | |||||
func ParseUint(b []byte, base int, bitSize int) (uint64, error) { | |||||
return strconv.ParseUint(BytesToString(b), base, bitSize) | |||||
} | |||||
func ParseFloat(b []byte, bitSize int) (float64, error) { | |||||
return strconv.ParseFloat(BytesToString(b), bitSize) | |||||
} |
// +build !appengine | |||||
package util | |||||
import ( | |||||
"unsafe" | |||||
) | |||||
// BytesToString converts byte slice to string. | |||||
func BytesToString(b []byte) string { | |||||
return *(*string)(unsafe.Pointer(&b)) | |||||
} | |||||
// StringToBytes converts string to byte slice. | |||||
func StringToBytes(s string) []byte { | |||||
return *(*[]byte)(unsafe.Pointer( | |||||
&struct { | |||||
string | |||||
Cap int | |||||
}{s, len(s)}, | |||||
)) | |||||
} |
package redis | |||||
import "sync" | |||||
// ScanIterator is used to incrementally iterate over a collection of elements. | |||||
// It's safe for concurrent use by multiple goroutines. | |||||
type ScanIterator struct { | |||||
mu sync.Mutex // protects Scanner and pos | |||||
cmd *ScanCmd | |||||
pos int | |||||
} | |||||
// Err returns the last iterator error, if any. | |||||
func (it *ScanIterator) Err() error { | |||||
it.mu.Lock() | |||||
err := it.cmd.Err() | |||||
it.mu.Unlock() | |||||
return err | |||||
} | |||||
// Next advances the cursor and returns true if more values can be read. | |||||
func (it *ScanIterator) Next() bool { | |||||
it.mu.Lock() | |||||
defer it.mu.Unlock() | |||||
// Instantly return on errors. | |||||
if it.cmd.Err() != nil { | |||||
return false | |||||
} | |||||
// Advance cursor, check if we are still within range. | |||||
if it.pos < len(it.cmd.page) { | |||||
it.pos++ | |||||
return true | |||||
} | |||||
for { | |||||
// Return if there is no more data to fetch. | |||||
if it.cmd.cursor == 0 { | |||||
return false | |||||
} | |||||
// Fetch next page. | |||||
if it.cmd._args[0] == "scan" { | |||||
it.cmd._args[1] = it.cmd.cursor | |||||
} else { | |||||
it.cmd._args[2] = it.cmd.cursor | |||||
} | |||||
err := it.cmd.process(it.cmd) | |||||
if err != nil { | |||||
return false | |||||
} | |||||
it.pos = 1 | |||||
// Redis can occasionally return empty page. | |||||
if len(it.cmd.page) > 0 { | |||||
return true | |||||
} | |||||
} | |||||
} | |||||
// Val returns the key/field at the current cursor position. | |||||
func (it *ScanIterator) Val() string { | |||||
var v string | |||||
it.mu.Lock() | |||||
if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) { | |||||
v = it.cmd.page[it.pos-1] | |||||
} | |||||
it.mu.Unlock() | |||||
return v | |||||
} |
package redis | |||||
import ( | |||||
"crypto/tls" | |||||
"errors" | |||||
"fmt" | |||||
"net" | |||||
"net/url" | |||||
"runtime" | |||||
"strconv" | |||||
"strings" | |||||
"time" | |||||
"github.com/go-redis/redis/internal/pool" | |||||
) | |||||
// Limiter is the interface of a rate limiter or a circuit breaker. | |||||
type Limiter interface { | |||||
// Allow returns a nil if operation is allowed or an error otherwise. | |||||
// If operation is allowed client must report the result of operation | |||||
// whether is a success or a failure. | |||||
Allow() error | |||||
// ReportResult reports the result of previously allowed operation. | |||||
// nil indicates a success, non-nil error indicates a failure. | |||||
ReportResult(result error) | |||||
} | |||||
type Options struct { | |||||
// The network type, either tcp or unix. | |||||
// Default is tcp. | |||||
Network string | |||||
// host:port address. | |||||
Addr string | |||||
// Dialer creates new network connection and has priority over | |||||
// Network and Addr options. | |||||
Dialer func() (net.Conn, error) | |||||
// Hook that is called when new connection is established. | |||||
OnConnect func(*Conn) error | |||||
// Optional password. Must match the password specified in the | |||||
// requirepass server configuration option. | |||||
Password string | |||||
// Database to be selected after connecting to the server. | |||||
DB int | |||||
// Maximum number of retries before giving up. | |||||
// Default is to not retry failed commands. | |||||
MaxRetries int | |||||
// Minimum backoff between each retry. | |||||
// Default is 8 milliseconds; -1 disables backoff. | |||||
MinRetryBackoff time.Duration | |||||
// Maximum backoff between each retry. | |||||
// Default is 512 milliseconds; -1 disables backoff. | |||||
MaxRetryBackoff time.Duration | |||||
// Dial timeout for establishing new connections. | |||||
// Default is 5 seconds. | |||||
DialTimeout time.Duration | |||||
// Timeout for socket reads. If reached, commands will fail | |||||
// with a timeout instead of blocking. Use value -1 for no timeout and 0 for default. | |||||
// Default is 3 seconds. | |||||
ReadTimeout time.Duration | |||||
// Timeout for socket writes. If reached, commands will fail | |||||
// with a timeout instead of blocking. | |||||
// Default is ReadTimeout. | |||||
WriteTimeout time.Duration | |||||
// Maximum number of socket connections. | |||||
// Default is 10 connections per every CPU as reported by runtime.NumCPU. | |||||
PoolSize int | |||||
// Minimum number of idle connections which is useful when establishing | |||||
// new connection is slow. | |||||
MinIdleConns int | |||||
// Connection age at which client retires (closes) the connection. | |||||
// Default is to not close aged connections. | |||||
MaxConnAge time.Duration | |||||
// Amount of time client waits for connection if all connections | |||||
// are busy before returning an error. | |||||
// Default is ReadTimeout + 1 second. | |||||
PoolTimeout time.Duration | |||||
// Amount of time after which client closes idle connections. | |||||
// Should be less than server's timeout. | |||||
// Default is 5 minutes. -1 disables idle timeout check. | |||||
IdleTimeout time.Duration | |||||
// Frequency of idle checks made by idle connections reaper. | |||||
// Default is 1 minute. -1 disables idle connections reaper, | |||||
// but idle connections are still discarded by the client | |||||
// if IdleTimeout is set. | |||||
IdleCheckFrequency time.Duration | |||||
// Enables read only queries on slave nodes. | |||||
readOnly bool | |||||
// TLS Config to use. When set TLS will be negotiated. | |||||
TLSConfig *tls.Config | |||||
} | |||||
func (opt *Options) init() { | |||||
if opt.Network == "" { | |||||
opt.Network = "tcp" | |||||
} | |||||
if opt.Addr == "" { | |||||
opt.Addr = "localhost:6379" | |||||
} | |||||
if opt.Dialer == nil { | |||||
opt.Dialer = func() (net.Conn, error) { | |||||
netDialer := &net.Dialer{ | |||||
Timeout: opt.DialTimeout, | |||||
KeepAlive: 5 * time.Minute, | |||||
} | |||||
if opt.TLSConfig == nil { | |||||
return netDialer.Dial(opt.Network, opt.Addr) | |||||
} else { | |||||
return tls.DialWithDialer(netDialer, opt.Network, opt.Addr, opt.TLSConfig) | |||||
} | |||||
} | |||||
} | |||||
if opt.PoolSize == 0 { | |||||
opt.PoolSize = 10 * runtime.NumCPU() | |||||
} | |||||
if opt.DialTimeout == 0 { | |||||
opt.DialTimeout = 5 * time.Second | |||||
} | |||||
switch opt.ReadTimeout { | |||||
case -1: | |||||
opt.ReadTimeout = 0 | |||||
case 0: | |||||
opt.ReadTimeout = 3 * time.Second | |||||
} | |||||
switch opt.WriteTimeout { | |||||
case -1: | |||||
opt.WriteTimeout = 0 | |||||
case 0: | |||||
opt.WriteTimeout = opt.ReadTimeout | |||||
} | |||||
if opt.PoolTimeout == 0 { | |||||
opt.PoolTimeout = opt.ReadTimeout + time.Second | |||||
} | |||||
if opt.IdleTimeout == 0 { | |||||
opt.IdleTimeout = 5 * time.Minute | |||||
} | |||||
if opt.IdleCheckFrequency == 0 { | |||||
opt.IdleCheckFrequency = time.Minute | |||||
} | |||||
switch opt.MinRetryBackoff { | |||||
case -1: | |||||
opt.MinRetryBackoff = 0 | |||||
case 0: | |||||
opt.MinRetryBackoff = 8 * time.Millisecond | |||||
} | |||||
switch opt.MaxRetryBackoff { | |||||
case -1: | |||||
opt.MaxRetryBackoff = 0 | |||||
case 0: | |||||
opt.MaxRetryBackoff = 512 * time.Millisecond | |||||
} | |||||
} | |||||
// ParseURL parses an URL into Options that can be used to connect to Redis. | |||||
func ParseURL(redisURL string) (*Options, error) { | |||||
o := &Options{Network: "tcp"} | |||||
u, err := url.Parse(redisURL) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if u.Scheme != "redis" && u.Scheme != "rediss" { | |||||
return nil, errors.New("invalid redis URL scheme: " + u.Scheme) | |||||
} | |||||
if u.User != nil { | |||||
if p, ok := u.User.Password(); ok { | |||||
o.Password = p | |||||
} | |||||
} | |||||
if len(u.Query()) > 0 { | |||||
return nil, errors.New("no options supported") | |||||
} | |||||
h, p, err := net.SplitHostPort(u.Host) | |||||
if err != nil { | |||||
h = u.Host | |||||
} | |||||
if h == "" { | |||||
h = "localhost" | |||||
} | |||||
if p == "" { | |||||
p = "6379" | |||||
} | |||||
o.Addr = net.JoinHostPort(h, p) | |||||
f := strings.FieldsFunc(u.Path, func(r rune) bool { | |||||
return r == '/' | |||||
}) | |||||
switch len(f) { | |||||
case 0: | |||||
o.DB = 0 | |||||
case 1: | |||||
if o.DB, err = strconv.Atoi(f[0]); err != nil { | |||||
return nil, fmt.Errorf("invalid redis database number: %q", f[0]) | |||||
} | |||||
default: | |||||
return nil, errors.New("invalid redis URL path: " + u.Path) | |||||
} | |||||
if u.Scheme == "rediss" { | |||||
o.TLSConfig = &tls.Config{ServerName: h} | |||||
} | |||||
return o, nil | |||||
} | |||||
func newConnPool(opt *Options) *pool.ConnPool { | |||||
return pool.NewConnPool(&pool.Options{ | |||||
Dialer: opt.Dialer, | |||||
PoolSize: opt.PoolSize, | |||||
MinIdleConns: opt.MinIdleConns, | |||||
MaxConnAge: opt.MaxConnAge, | |||||
PoolTimeout: opt.PoolTimeout, | |||||
IdleTimeout: opt.IdleTimeout, | |||||
IdleCheckFrequency: opt.IdleCheckFrequency, | |||||
}) | |||||
} |
package redis | |||||
import ( | |||||
"sync" | |||||
"github.com/go-redis/redis/internal/pool" | |||||
) | |||||
type pipelineExecer func([]Cmder) error | |||||
type Pipeliner interface { | |||||
StatefulCmdable | |||||
Do(args ...interface{}) *Cmd | |||||
Process(cmd Cmder) error | |||||
Close() error | |||||
Discard() error | |||||
Exec() ([]Cmder, error) | |||||
} | |||||
var _ Pipeliner = (*Pipeline)(nil) | |||||
// Pipeline implements pipelining as described in | |||||
// http://redis.io/topics/pipelining. It's safe for concurrent use | |||||
// by multiple goroutines. | |||||
type Pipeline struct { | |||||
statefulCmdable | |||||
exec pipelineExecer | |||||
mu sync.Mutex | |||||
cmds []Cmder | |||||
closed bool | |||||
} | |||||
func (c *Pipeline) Do(args ...interface{}) *Cmd { | |||||
cmd := NewCmd(args...) | |||||
_ = c.Process(cmd) | |||||
return cmd | |||||
} | |||||
// Process queues the cmd for later execution. | |||||
func (c *Pipeline) Process(cmd Cmder) error { | |||||
c.mu.Lock() | |||||
c.cmds = append(c.cmds, cmd) | |||||
c.mu.Unlock() | |||||
return nil | |||||
} | |||||
// Close closes the pipeline, releasing any open resources. | |||||
func (c *Pipeline) Close() error { | |||||
c.mu.Lock() | |||||
c.discard() | |||||
c.closed = true | |||||
c.mu.Unlock() | |||||
return nil | |||||
} | |||||
// Discard resets the pipeline and discards queued commands. | |||||
func (c *Pipeline) Discard() error { | |||||
c.mu.Lock() | |||||
err := c.discard() | |||||
c.mu.Unlock() | |||||
return err | |||||
} | |||||
func (c *Pipeline) discard() error { | |||||
if c.closed { | |||||
return pool.ErrClosed | |||||
} | |||||
c.cmds = c.cmds[:0] | |||||
return nil | |||||
} | |||||
// Exec executes all previously queued commands using one | |||||
// client-server roundtrip. | |||||
// | |||||
// Exec always returns list of commands and error of the first failed | |||||
// command if any. | |||||
func (c *Pipeline) Exec() ([]Cmder, error) { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
if c.closed { | |||||
return nil, pool.ErrClosed | |||||
} | |||||
if len(c.cmds) == 0 { | |||||
return nil, nil | |||||
} | |||||
cmds := c.cmds | |||||
c.cmds = nil | |||||
return cmds, c.exec(cmds) | |||||
} | |||||
func (c *Pipeline) pipelined(fn func(Pipeliner) error) ([]Cmder, error) { | |||||
if err := fn(c); err != nil { | |||||
return nil, err | |||||
} | |||||
cmds, err := c.Exec() | |||||
_ = c.Close() | |||||
return cmds, err | |||||
} | |||||
func (c *Pipeline) Pipelined(fn func(Pipeliner) error) ([]Cmder, error) { | |||||
return c.pipelined(fn) | |||||
} | |||||
func (c *Pipeline) Pipeline() Pipeliner { | |||||
return c | |||||
} | |||||
func (c *Pipeline) TxPipelined(fn func(Pipeliner) error) ([]Cmder, error) { | |||||
return c.pipelined(fn) | |||||
} | |||||
func (c *Pipeline) TxPipeline() Pipeliner { | |||||
return c | |||||
} |
package redis | |||||
import ( | |||||
"errors" | |||||
"fmt" | |||||
"sync" | |||||
"time" | |||||
"github.com/go-redis/redis/internal" | |||||
"github.com/go-redis/redis/internal/pool" | |||||
"github.com/go-redis/redis/internal/proto" | |||||
) | |||||
var errPingTimeout = errors.New("redis: ping timeout") | |||||
// PubSub implements Pub/Sub commands bas described in | |||||
// http://redis.io/topics/pubsub. Message receiving is NOT safe | |||||
// for concurrent use by multiple goroutines. | |||||
// | |||||
// PubSub automatically reconnects to Redis Server and resubscribes | |||||
// to the channels in case of network errors. | |||||
type PubSub struct { | |||||
opt *Options | |||||
newConn func([]string) (*pool.Conn, error) | |||||
closeConn func(*pool.Conn) error | |||||
mu sync.Mutex | |||||
cn *pool.Conn | |||||
channels map[string]struct{} | |||||
patterns map[string]struct{} | |||||
closed bool | |||||
exit chan struct{} | |||||
cmd *Cmd | |||||
chOnce sync.Once | |||||
ch chan *Message | |||||
ping chan struct{} | |||||
} | |||||
func (c *PubSub) init() { | |||||
c.exit = make(chan struct{}) | |||||
} | |||||
func (c *PubSub) conn() (*pool.Conn, error) { | |||||
c.mu.Lock() | |||||
cn, err := c._conn(nil) | |||||
c.mu.Unlock() | |||||
return cn, err | |||||
} | |||||
func (c *PubSub) _conn(newChannels []string) (*pool.Conn, error) { | |||||
if c.closed { | |||||
return nil, pool.ErrClosed | |||||
} | |||||
if c.cn != nil { | |||||
return c.cn, nil | |||||
} | |||||
channels := mapKeys(c.channels) | |||||
channels = append(channels, newChannels...) | |||||
cn, err := c.newConn(channels) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if err := c.resubscribe(cn); err != nil { | |||||
_ = c.closeConn(cn) | |||||
return nil, err | |||||
} | |||||
c.cn = cn | |||||
return cn, nil | |||||
} | |||||
func (c *PubSub) writeCmd(cn *pool.Conn, cmd Cmder) error { | |||||
return cn.WithWriter(c.opt.WriteTimeout, func(wr *proto.Writer) error { | |||||
return writeCmd(wr, cmd) | |||||
}) | |||||
} | |||||
func (c *PubSub) resubscribe(cn *pool.Conn) error { | |||||
var firstErr error | |||||
if len(c.channels) > 0 { | |||||
err := c._subscribe(cn, "subscribe", mapKeys(c.channels)) | |||||
if err != nil && firstErr == nil { | |||||
firstErr = err | |||||
} | |||||
} | |||||
if len(c.patterns) > 0 { | |||||
err := c._subscribe(cn, "psubscribe", mapKeys(c.patterns)) | |||||
if err != nil && firstErr == nil { | |||||
firstErr = err | |||||
} | |||||
} | |||||
return firstErr | |||||
} | |||||
func mapKeys(m map[string]struct{}) []string { | |||||
s := make([]string, len(m)) | |||||
i := 0 | |||||
for k := range m { | |||||
s[i] = k | |||||
i++ | |||||
} | |||||
return s | |||||
} | |||||
func (c *PubSub) _subscribe( | |||||
cn *pool.Conn, redisCmd string, channels []string, | |||||
) error { | |||||
args := make([]interface{}, 0, 1+len(channels)) | |||||
args = append(args, redisCmd) | |||||
for _, channel := range channels { | |||||
args = append(args, channel) | |||||
} | |||||
cmd := NewSliceCmd(args...) | |||||
return c.writeCmd(cn, cmd) | |||||
} | |||||
func (c *PubSub) releaseConn(cn *pool.Conn, err error, allowTimeout bool) { | |||||
c.mu.Lock() | |||||
c._releaseConn(cn, err, allowTimeout) | |||||
c.mu.Unlock() | |||||
} | |||||
func (c *PubSub) _releaseConn(cn *pool.Conn, err error, allowTimeout bool) { | |||||
if c.cn != cn { | |||||
return | |||||
} | |||||
if internal.IsBadConn(err, allowTimeout) { | |||||
c._reconnect(err) | |||||
} | |||||
} | |||||
func (c *PubSub) _reconnect(reason error) { | |||||
_ = c._closeTheCn(reason) | |||||
_, _ = c._conn(nil) | |||||
} | |||||
func (c *PubSub) _closeTheCn(reason error) error { | |||||
if c.cn == nil { | |||||
return nil | |||||
} | |||||
if !c.closed { | |||||
internal.Logf("redis: discarding bad PubSub connection: %s", reason) | |||||
} | |||||
err := c.closeConn(c.cn) | |||||
c.cn = nil | |||||
return err | |||||
} | |||||
func (c *PubSub) Close() error { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
if c.closed { | |||||
return pool.ErrClosed | |||||
} | |||||
c.closed = true | |||||
close(c.exit) | |||||
err := c._closeTheCn(pool.ErrClosed) | |||||
return err | |||||
} | |||||
// Subscribe the client to the specified channels. It returns | |||||
// empty subscription if there are no channels. | |||||
func (c *PubSub) Subscribe(channels ...string) error { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
err := c.subscribe("subscribe", channels...) | |||||
if c.channels == nil { | |||||
c.channels = make(map[string]struct{}) | |||||
} | |||||
for _, s := range channels { | |||||
c.channels[s] = struct{}{} | |||||
} | |||||
return err | |||||
} | |||||
// PSubscribe the client to the given patterns. It returns | |||||
// empty subscription if there are no patterns. | |||||
func (c *PubSub) PSubscribe(patterns ...string) error { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
err := c.subscribe("psubscribe", patterns...) | |||||
if c.patterns == nil { | |||||
c.patterns = make(map[string]struct{}) | |||||
} | |||||
for _, s := range patterns { | |||||
c.patterns[s] = struct{}{} | |||||
} | |||||
return err | |||||
} | |||||
// Unsubscribe the client from the given channels, or from all of | |||||
// them if none is given. | |||||
func (c *PubSub) Unsubscribe(channels ...string) error { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
for _, channel := range channels { | |||||
delete(c.channels, channel) | |||||
} | |||||
err := c.subscribe("unsubscribe", channels...) | |||||
return err | |||||
} | |||||
// PUnsubscribe the client from the given patterns, or from all of | |||||
// them if none is given. | |||||
func (c *PubSub) PUnsubscribe(patterns ...string) error { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
for _, pattern := range patterns { | |||||
delete(c.patterns, pattern) | |||||
} | |||||
err := c.subscribe("punsubscribe", patterns...) | |||||
return err | |||||
} | |||||
func (c *PubSub) subscribe(redisCmd string, channels ...string) error { | |||||
cn, err := c._conn(channels) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = c._subscribe(cn, redisCmd, channels) | |||||
c._releaseConn(cn, err, false) | |||||
return err | |||||
} | |||||
func (c *PubSub) Ping(payload ...string) error { | |||||
args := []interface{}{"ping"} | |||||
if len(payload) == 1 { | |||||
args = append(args, payload[0]) | |||||
} | |||||
cmd := NewCmd(args...) | |||||
cn, err := c.conn() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = c.writeCmd(cn, cmd) | |||||
c.releaseConn(cn, err, false) | |||||
return err | |||||
} | |||||
// Subscription received after a successful subscription to channel. | |||||
type Subscription struct { | |||||
// Can be "subscribe", "unsubscribe", "psubscribe" or "punsubscribe". | |||||
Kind string | |||||
// Channel name we have subscribed to. | |||||
Channel string | |||||
// Number of channels we are currently subscribed to. | |||||
Count int | |||||
} | |||||
func (m *Subscription) String() string { | |||||
return fmt.Sprintf("%s: %s", m.Kind, m.Channel) | |||||
} | |||||
// Message received as result of a PUBLISH command issued by another client. | |||||
type Message struct { | |||||
Channel string | |||||
Pattern string | |||||
Payload string | |||||
} | |||||
func (m *Message) String() string { | |||||
return fmt.Sprintf("Message<%s: %s>", m.Channel, m.Payload) | |||||
} | |||||
// Pong received as result of a PING command issued by another client. | |||||
type Pong struct { | |||||
Payload string | |||||
} | |||||
func (p *Pong) String() string { | |||||
if p.Payload != "" { | |||||
return fmt.Sprintf("Pong<%s>", p.Payload) | |||||
} | |||||
return "Pong" | |||||
} | |||||
func (c *PubSub) newMessage(reply interface{}) (interface{}, error) { | |||||
switch reply := reply.(type) { | |||||
case string: | |||||
return &Pong{ | |||||
Payload: reply, | |||||
}, nil | |||||
case []interface{}: | |||||
switch kind := reply[0].(string); kind { | |||||
case "subscribe", "unsubscribe", "psubscribe", "punsubscribe": | |||||
return &Subscription{ | |||||
Kind: kind, | |||||
Channel: reply[1].(string), | |||||
Count: int(reply[2].(int64)), | |||||
}, nil | |||||
case "message": | |||||
return &Message{ | |||||
Channel: reply[1].(string), | |||||
Payload: reply[2].(string), | |||||
}, nil | |||||
case "pmessage": | |||||
return &Message{ | |||||
Pattern: reply[1].(string), | |||||
Channel: reply[2].(string), | |||||
Payload: reply[3].(string), | |||||
}, nil | |||||
case "pong": | |||||
return &Pong{ | |||||
Payload: reply[1].(string), | |||||
}, nil | |||||
default: | |||||
return nil, fmt.Errorf("redis: unsupported pubsub message: %q", kind) | |||||
} | |||||
default: | |||||
return nil, fmt.Errorf("redis: unsupported pubsub message: %#v", reply) | |||||
} | |||||
} | |||||
// ReceiveTimeout acts like Receive but returns an error if message | |||||
// is not received in time. This is low-level API and in most cases | |||||
// Channel should be used instead. | |||||
func (c *PubSub) ReceiveTimeout(timeout time.Duration) (interface{}, error) { | |||||
if c.cmd == nil { | |||||
c.cmd = NewCmd() | |||||
} | |||||
cn, err := c.conn() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
err = cn.WithReader(timeout, func(rd *proto.Reader) error { | |||||
return c.cmd.readReply(rd) | |||||
}) | |||||
c.releaseConn(cn, err, timeout > 0) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return c.newMessage(c.cmd.Val()) | |||||
} | |||||
// Receive returns a message as a Subscription, Message, Pong or error. | |||||
// See PubSub example for details. This is low-level API and in most cases | |||||
// Channel should be used instead. | |||||
func (c *PubSub) Receive() (interface{}, error) { | |||||
return c.ReceiveTimeout(0) | |||||
} | |||||
// ReceiveMessage returns a Message or error ignoring Subscription and Pong | |||||
// messages. This is low-level API and in most cases Channel should be used | |||||
// instead. | |||||
func (c *PubSub) ReceiveMessage() (*Message, error) { | |||||
for { | |||||
msg, err := c.Receive() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
switch msg := msg.(type) { | |||||
case *Subscription: | |||||
// Ignore. | |||||
case *Pong: | |||||
// Ignore. | |||||
case *Message: | |||||
return msg, nil | |||||
default: | |||||
err := fmt.Errorf("redis: unknown message: %T", msg) | |||||
return nil, err | |||||
} | |||||
} | |||||
} | |||||
// Channel returns a Go channel for concurrently receiving messages. | |||||
// It periodically sends Ping messages to test connection health. | |||||
// The channel is closed with PubSub. Receive* APIs can not be used | |||||
// after channel is created. | |||||
func (c *PubSub) Channel() <-chan *Message { | |||||
c.chOnce.Do(c.initChannel) | |||||
return c.ch | |||||
} | |||||
func (c *PubSub) initChannel() { | |||||
c.ch = make(chan *Message, 100) | |||||
c.ping = make(chan struct{}, 10) | |||||
go func() { | |||||
var errCount int | |||||
for { | |||||
msg, err := c.Receive() | |||||
if err != nil { | |||||
if err == pool.ErrClosed { | |||||
close(c.ch) | |||||
return | |||||
} | |||||
if errCount > 0 { | |||||
time.Sleep(c.retryBackoff(errCount)) | |||||
} | |||||
errCount++ | |||||
continue | |||||
} | |||||
errCount = 0 | |||||
// Any message is as good as a ping. | |||||
select { | |||||
case c.ping <- struct{}{}: | |||||
default: | |||||
} | |||||
switch msg := msg.(type) { | |||||
case *Subscription: | |||||
// Ignore. | |||||
case *Pong: | |||||
// Ignore. | |||||
case *Message: | |||||
c.ch <- msg | |||||
default: | |||||
internal.Logf("redis: unknown message: %T", msg) | |||||
} | |||||
} | |||||
}() | |||||
go func() { | |||||
const timeout = 5 * time.Second | |||||
timer := time.NewTimer(timeout) | |||||
timer.Stop() | |||||
healthy := true | |||||
for { | |||||
timer.Reset(timeout) | |||||
select { | |||||
case <-c.ping: | |||||
healthy = true | |||||
if !timer.Stop() { | |||||
<-timer.C | |||||
} | |||||
case <-timer.C: | |||||
pingErr := c.Ping() | |||||
if healthy { | |||||
healthy = false | |||||
} else { | |||||
if pingErr == nil { | |||||
pingErr = errPingTimeout | |||||
} | |||||
c.mu.Lock() | |||||
c._reconnect(pingErr) | |||||
c.mu.Unlock() | |||||
} | |||||
case <-c.exit: | |||||
return | |||||
} | |||||
} | |||||
}() | |||||
} | |||||
func (c *PubSub) retryBackoff(attempt int) time.Duration { | |||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff) | |||||
} |
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 | |||||
} |
package redis | |||||
import "time" | |||||
// NewCmdResult returns a Cmd initialised with val and err for testing | |||||
func NewCmdResult(val interface{}, err error) *Cmd { | |||||
var cmd Cmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewSliceResult returns a SliceCmd initialised with val and err for testing | |||||
func NewSliceResult(val []interface{}, err error) *SliceCmd { | |||||
var cmd SliceCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewStatusResult returns a StatusCmd initialised with val and err for testing | |||||
func NewStatusResult(val string, err error) *StatusCmd { | |||||
var cmd StatusCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewIntResult returns an IntCmd initialised with val and err for testing | |||||
func NewIntResult(val int64, err error) *IntCmd { | |||||
var cmd IntCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewDurationResult returns a DurationCmd initialised with val and err for testing | |||||
func NewDurationResult(val time.Duration, err error) *DurationCmd { | |||||
var cmd DurationCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewBoolResult returns a BoolCmd initialised with val and err for testing | |||||
func NewBoolResult(val bool, err error) *BoolCmd { | |||||
var cmd BoolCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewStringResult returns a StringCmd initialised with val and err for testing | |||||
func NewStringResult(val string, err error) *StringCmd { | |||||
var cmd StringCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewFloatResult returns a FloatCmd initialised with val and err for testing | |||||
func NewFloatResult(val float64, err error) *FloatCmd { | |||||
var cmd FloatCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewStringSliceResult returns a StringSliceCmd initialised with val and err for testing | |||||
func NewStringSliceResult(val []string, err error) *StringSliceCmd { | |||||
var cmd StringSliceCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewBoolSliceResult returns a BoolSliceCmd initialised with val and err for testing | |||||
func NewBoolSliceResult(val []bool, err error) *BoolSliceCmd { | |||||
var cmd BoolSliceCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewStringStringMapResult returns a StringStringMapCmd initialised with val and err for testing | |||||
func NewStringStringMapResult(val map[string]string, err error) *StringStringMapCmd { | |||||
var cmd StringStringMapCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewStringIntMapCmdResult returns a StringIntMapCmd initialised with val and err for testing | |||||
func NewStringIntMapCmdResult(val map[string]int64, err error) *StringIntMapCmd { | |||||
var cmd StringIntMapCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewZSliceCmdResult returns a ZSliceCmd initialised with val and err for testing | |||||
func NewZSliceCmdResult(val []Z, err error) *ZSliceCmd { | |||||
var cmd ZSliceCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewScanCmdResult returns a ScanCmd initialised with val and err for testing | |||||
func NewScanCmdResult(keys []string, cursor uint64, err error) *ScanCmd { | |||||
var cmd ScanCmd | |||||
cmd.page = keys | |||||
cmd.cursor = cursor | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewClusterSlotsCmdResult returns a ClusterSlotsCmd initialised with val and err for testing | |||||
func NewClusterSlotsCmdResult(val []ClusterSlot, err error) *ClusterSlotsCmd { | |||||
var cmd ClusterSlotsCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewGeoLocationCmdResult returns a GeoLocationCmd initialised with val and err for testing | |||||
func NewGeoLocationCmdResult(val []GeoLocation, err error) *GeoLocationCmd { | |||||
var cmd GeoLocationCmd | |||||
cmd.locations = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} | |||||
// NewCommandsInfoCmdResult returns a CommandsInfoCmd initialised with val and err for testing | |||||
func NewCommandsInfoCmdResult(val map[string]*CommandInfo, err error) *CommandsInfoCmd { | |||||
var cmd CommandsInfoCmd | |||||
cmd.val = val | |||||
cmd.setErr(err) | |||||
return &cmd | |||||
} |
package redis | |||||
import ( | |||||
"context" | |||||
"errors" | |||||
"fmt" | |||||
"math/rand" | |||||
"strconv" | |||||
"sync" | |||||
"sync/atomic" | |||||
"time" | |||||
"github.com/go-redis/redis/internal" | |||||
"github.com/go-redis/redis/internal/consistenthash" | |||||
"github.com/go-redis/redis/internal/hashtag" | |||||
"github.com/go-redis/redis/internal/pool" | |||||
) | |||||
// Hash is type of hash function used in consistent hash. | |||||
type Hash consistenthash.Hash | |||||
var errRingShardsDown = errors.New("redis: all ring shards are down") | |||||
// RingOptions are used to configure a ring client and should be | |||||
// passed to NewRing. | |||||
type RingOptions struct { | |||||
// Map of name => host:port addresses of ring shards. | |||||
Addrs map[string]string | |||||
// Frequency of PING commands sent to check shards availability. | |||||
// Shard is considered down after 3 subsequent failed checks. | |||||
HeartbeatFrequency time.Duration | |||||
// Hash function used in consistent hash. | |||||
// Default is crc32.ChecksumIEEE. | |||||
Hash Hash | |||||
// Number of replicas in consistent hash. | |||||
// Default is 100 replicas. | |||||
// | |||||
// Higher number of replicas will provide less deviation, that is keys will be | |||||
// distributed to nodes more evenly. | |||||
// | |||||
// Following is deviation for common nreplicas: | |||||
// -------------------------------------------------------- | |||||
// | nreplicas | standard error | 99% confidence interval | | |||||
// | 10 | 0.3152 | (0.37, 1.98) | | |||||
// | 100 | 0.0997 | (0.76, 1.28) | | |||||
// | 1000 | 0.0316 | (0.92, 1.09) | | |||||
// -------------------------------------------------------- | |||||
// | |||||
// See https://arxiv.org/abs/1406.2294 for reference | |||||
HashReplicas int | |||||
// Following options are copied from Options struct. | |||||
OnConnect func(*Conn) error | |||||
DB int | |||||
Password string | |||||
MaxRetries int | |||||
MinRetryBackoff time.Duration | |||||
MaxRetryBackoff time.Duration | |||||
DialTimeout time.Duration | |||||
ReadTimeout time.Duration | |||||
WriteTimeout time.Duration | |||||
PoolSize int | |||||
MinIdleConns int | |||||
MaxConnAge time.Duration | |||||
PoolTimeout time.Duration | |||||
IdleTimeout time.Duration | |||||
IdleCheckFrequency time.Duration | |||||
} | |||||
func (opt *RingOptions) init() { | |||||
if opt.HeartbeatFrequency == 0 { | |||||
opt.HeartbeatFrequency = 500 * time.Millisecond | |||||
} | |||||
if opt.HashReplicas == 0 { | |||||
opt.HashReplicas = 100 | |||||
} | |||||
switch opt.MinRetryBackoff { | |||||
case -1: | |||||
opt.MinRetryBackoff = 0 | |||||
case 0: | |||||
opt.MinRetryBackoff = 8 * time.Millisecond | |||||
} | |||||
switch opt.MaxRetryBackoff { | |||||
case -1: | |||||
opt.MaxRetryBackoff = 0 | |||||
case 0: | |||||
opt.MaxRetryBackoff = 512 * time.Millisecond | |||||
} | |||||
} | |||||
func (opt *RingOptions) clientOptions() *Options { | |||||
return &Options{ | |||||
OnConnect: opt.OnConnect, | |||||
DB: opt.DB, | |||||
Password: opt.Password, | |||||
DialTimeout: opt.DialTimeout, | |||||
ReadTimeout: opt.ReadTimeout, | |||||
WriteTimeout: opt.WriteTimeout, | |||||
PoolSize: opt.PoolSize, | |||||
MinIdleConns: opt.MinIdleConns, | |||||
MaxConnAge: opt.MaxConnAge, | |||||
PoolTimeout: opt.PoolTimeout, | |||||
IdleTimeout: opt.IdleTimeout, | |||||
IdleCheckFrequency: opt.IdleCheckFrequency, | |||||
} | |||||
} | |||||
//------------------------------------------------------------------------------ | |||||
type ringShard struct { | |||||
Client *Client | |||||
down int32 | |||||
} | |||||
func (shard *ringShard) String() string { | |||||
var state string | |||||
if shard.IsUp() { | |||||
state = "up" | |||||
} else { | |||||
state = "down" | |||||
} | |||||
return fmt.Sprintf("%s is %s", shard.Client, state) | |||||
} | |||||
func (shard *ringShard) IsDown() bool { | |||||
const threshold = 3 | |||||
return atomic.LoadInt32(&shard.down) >= threshold | |||||
} | |||||
func (shard *ringShard) IsUp() bool { | |||||
return !shard.IsDown() | |||||
} | |||||
// Vote votes to set shard state and returns true if state was changed. | |||||
func (shard *ringShard) Vote(up bool) bool { | |||||
if up { | |||||
changed := shard.IsDown() | |||||
atomic.StoreInt32(&shard.down, 0) | |||||
return changed | |||||
} | |||||
if shard.IsDown() { | |||||
return false | |||||
} | |||||
atomic.AddInt32(&shard.down, 1) | |||||
return shard.IsDown() | |||||
} | |||||
//------------------------------------------------------------------------------ | |||||
type ringShards struct { | |||||
opt *RingOptions | |||||
mu sync.RWMutex | |||||
hash *consistenthash.Map | |||||
shards map[string]*ringShard // read only | |||||
list []*ringShard // read only | |||||
len int | |||||
closed bool | |||||
} | |||||
func newRingShards(opt *RingOptions) *ringShards { | |||||
return &ringShards{ | |||||
opt: opt, | |||||
hash: newConsistentHash(opt), | |||||
shards: make(map[string]*ringShard), | |||||
} | |||||
} | |||||
func (c *ringShards) Add(name string, cl *Client) { | |||||
shard := &ringShard{Client: cl} | |||||
c.hash.Add(name) | |||||
c.shards[name] = shard | |||||
c.list = append(c.list, shard) | |||||
} | |||||
func (c *ringShards) List() []*ringShard { | |||||
c.mu.RLock() | |||||
list := c.list | |||||
c.mu.RUnlock() | |||||
return list | |||||
} | |||||
func (c *ringShards) Hash(key string) string { | |||||
c.mu.RLock() | |||||
hash := c.hash.Get(key) | |||||
c.mu.RUnlock() | |||||
return hash | |||||
} | |||||
func (c *ringShards) GetByKey(key string) (*ringShard, error) { | |||||
key = hashtag.Key(key) | |||||
c.mu.RLock() | |||||
if c.closed { | |||||
c.mu.RUnlock() | |||||
return nil, pool.ErrClosed | |||||
} | |||||
hash := c.hash.Get(key) | |||||
if hash == "" { | |||||
c.mu.RUnlock() | |||||
return nil, errRingShardsDown | |||||
} | |||||
shard := c.shards[hash] | |||||
c.mu.RUnlock() | |||||
return shard, nil | |||||
} | |||||
func (c *ringShards) GetByHash(name string) (*ringShard, error) { | |||||
if name == "" { | |||||
return c.Random() | |||||
} | |||||
c.mu.RLock() | |||||
shard := c.shards[name] | |||||
c.mu.RUnlock() | |||||
return shard, nil | |||||
} | |||||
func (c *ringShards) Random() (*ringShard, error) { | |||||
return c.GetByKey(strconv.Itoa(rand.Int())) | |||||
} | |||||
// heartbeat monitors state of each shard in the ring. | |||||
func (c *ringShards) Heartbeat(frequency time.Duration) { | |||||
ticker := time.NewTicker(frequency) | |||||
defer ticker.Stop() | |||||
for range ticker.C { | |||||
var rebalance bool | |||||
c.mu.RLock() | |||||
if c.closed { | |||||
c.mu.RUnlock() | |||||
break | |||||
} | |||||
shards := c.list | |||||
c.mu.RUnlock() | |||||
for _, shard := range shards { | |||||
err := shard.Client.Ping().Err() | |||||
if shard.Vote(err == nil || err == pool.ErrPoolTimeout) { | |||||
internal.Logf("ring shard state changed: %s", shard) | |||||
rebalance = true | |||||
} | |||||
} | |||||
if rebalance { | |||||
c.rebalance() | |||||
} | |||||
} | |||||
} | |||||
// rebalance removes dead shards from the Ring. | |||||
func (c *ringShards) rebalance() { | |||||
hash := newConsistentHash(c.opt) | |||||
var shardsNum int | |||||
for name, shard := range c.shards { | |||||
if shard.IsUp() { | |||||
hash.Add(name) | |||||
shardsNum++ | |||||
} | |||||
} | |||||
c.mu.Lock() | |||||
c.hash = hash | |||||
c.len = shardsNum | |||||
c.mu.Unlock() | |||||
} | |||||
func (c *ringShards) Len() int { | |||||
c.mu.RLock() | |||||
l := c.len | |||||
c.mu.RUnlock() | |||||
return l | |||||
} | |||||
func (c *ringShards) Close() error { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
if c.closed { | |||||
return nil | |||||
} | |||||
c.closed = true | |||||
var firstErr error | |||||
for _, shard := range c.shards { | |||||
if err := shard.Client.Close(); err != nil && firstErr == nil { | |||||
firstErr = err | |||||
} | |||||
} | |||||
c.hash = nil | |||||
c.shards = nil | |||||
c.list = nil | |||||
return firstErr | |||||
} | |||||
//------------------------------------------------------------------------------ | |||||
// Ring is a Redis client that uses consistent hashing to distribute | |||||
// keys across multiple Redis servers (shards). It's safe for | |||||
// concurrent use by multiple goroutines. | |||||
// | |||||
// Ring monitors the state of each shard and removes dead shards from | |||||
// the ring. When a shard comes online it is added back to the ring. This | |||||
// gives you maximum availability and partition tolerance, but no | |||||
// consistency between different shards or even clients. Each client | |||||
// uses shards that are available to the client and does not do any | |||||
// coordination when shard state is changed. | |||||
// | |||||
// Ring should be used when you need multiple Redis servers for caching | |||||
// and can tolerate losing data when one of the servers dies. | |||||
// Otherwise you should use Redis Cluster. | |||||
type Ring struct { | |||||
cmdable | |||||
ctx context.Context | |||||
opt *RingOptions | |||||
shards *ringShards | |||||
cmdsInfoCache *cmdsInfoCache | |||||
process func(Cmder) error | |||||
processPipeline func([]Cmder) error | |||||
} | |||||
func NewRing(opt *RingOptions) *Ring { | |||||
opt.init() | |||||
ring := &Ring{ | |||||
opt: opt, | |||||
shards: newRingShards(opt), | |||||
} | |||||
ring.cmdsInfoCache = newCmdsInfoCache(ring.cmdsInfo) | |||||
ring.process = ring.defaultProcess | |||||
ring.processPipeline = ring.defaultProcessPipeline | |||||
ring.cmdable.setProcessor(ring.Process) | |||||
for name, addr := range opt.Addrs { | |||||
clopt := opt.clientOptions() | |||||
clopt.Addr = addr | |||||
ring.shards.Add(name, NewClient(clopt)) | |||||
} | |||||
go ring.shards.Heartbeat(opt.HeartbeatFrequency) | |||||
return ring | |||||
} | |||||
func (c *Ring) Context() context.Context { | |||||
if c.ctx != nil { | |||||
return c.ctx | |||||
} | |||||
return context.Background() | |||||
} | |||||
func (c *Ring) WithContext(ctx context.Context) *Ring { | |||||
if ctx == nil { | |||||
panic("nil context") | |||||
} | |||||
c2 := c.copy() | |||||
c2.ctx = ctx | |||||
return c2 | |||||
} | |||||
func (c *Ring) copy() *Ring { | |||||
cp := *c | |||||
return &cp | |||||
} | |||||
// Options returns read-only Options that were used to create the client. | |||||
func (c *Ring) Options() *RingOptions { | |||||
return c.opt | |||||
} | |||||
func (c *Ring) retryBackoff(attempt int) time.Duration { | |||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff) | |||||
} | |||||
// PoolStats returns accumulated connection pool stats. | |||||
func (c *Ring) PoolStats() *PoolStats { | |||||
shards := c.shards.List() | |||||
var acc PoolStats | |||||
for _, shard := range shards { | |||||
s := shard.Client.connPool.Stats() | |||||
acc.Hits += s.Hits | |||||
acc.Misses += s.Misses | |||||
acc.Timeouts += s.Timeouts | |||||
acc.TotalConns += s.TotalConns | |||||
acc.IdleConns += s.IdleConns | |||||
} | |||||
return &acc | |||||
} | |||||
// Len returns the current number of shards in the ring. | |||||
func (c *Ring) Len() int { | |||||
return c.shards.Len() | |||||
} | |||||
// Subscribe subscribes the client to the specified channels. | |||||
func (c *Ring) Subscribe(channels ...string) *PubSub { | |||||
if len(channels) == 0 { | |||||
panic("at least one channel is required") | |||||
} | |||||
shard, err := c.shards.GetByKey(channels[0]) | |||||
if err != nil { | |||||
// TODO: return PubSub with sticky error | |||||
panic(err) | |||||
} | |||||
return shard.Client.Subscribe(channels...) | |||||
} | |||||
// PSubscribe subscribes the client to the given patterns. | |||||
func (c *Ring) PSubscribe(channels ...string) *PubSub { | |||||
if len(channels) == 0 { | |||||
panic("at least one channel is required") | |||||
} | |||||
shard, err := c.shards.GetByKey(channels[0]) | |||||
if err != nil { | |||||
// TODO: return PubSub with sticky error | |||||
panic(err) | |||||
} | |||||
return shard.Client.PSubscribe(channels...) | |||||
} | |||||
// ForEachShard concurrently calls the fn on each live shard in the ring. | |||||
// It returns the first error if any. | |||||
func (c *Ring) ForEachShard(fn func(client *Client) error) error { | |||||
shards := c.shards.List() | |||||
var wg sync.WaitGroup | |||||
errCh := make(chan error, 1) | |||||
for _, shard := range shards { | |||||
if shard.IsDown() { | |||||
continue | |||||
} | |||||
wg.Add(1) | |||||
go func(shard *ringShard) { | |||||
defer wg.Done() | |||||
err := fn(shard.Client) | |||||
if err != nil { | |||||
select { | |||||
case errCh <- err: | |||||
default: | |||||
} | |||||
} | |||||
}(shard) | |||||
} | |||||
wg.Wait() | |||||
select { | |||||
case err := <-errCh: | |||||
return err | |||||
default: | |||||
return nil | |||||
} | |||||
} | |||||
func (c *Ring) cmdsInfo() (map[string]*CommandInfo, error) { | |||||
shards := c.shards.List() | |||||
firstErr := errRingShardsDown | |||||
for _, shard := range shards { | |||||
cmdsInfo, err := shard.Client.Command().Result() | |||||
if err == nil { | |||||
return cmdsInfo, nil | |||||
} | |||||
if firstErr == nil { | |||||
firstErr = err | |||||
} | |||||
} | |||||
return nil, firstErr | |||||
} | |||||
func (c *Ring) 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 (c *Ring) cmdShard(cmd Cmder) (*ringShard, error) { | |||||
cmdInfo := c.cmdInfo(cmd.Name()) | |||||
pos := cmdFirstKeyPos(cmd, cmdInfo) | |||||
if pos == 0 { | |||||
return c.shards.Random() | |||||
} | |||||
firstKey := cmd.stringArg(pos) | |||||
return c.shards.GetByKey(firstKey) | |||||
} | |||||
// Do creates a Cmd from the args and processes the cmd. | |||||
func (c *Ring) Do(args ...interface{}) *Cmd { | |||||
cmd := NewCmd(args...) | |||||
c.Process(cmd) | |||||
return cmd | |||||
} | |||||
func (c *Ring) WrapProcess( | |||||
fn func(oldProcess func(cmd Cmder) error) func(cmd Cmder) error, | |||||
) { | |||||
c.process = fn(c.process) | |||||
} | |||||
func (c *Ring) Process(cmd Cmder) error { | |||||
return c.process(cmd) | |||||
} | |||||
func (c *Ring) defaultProcess(cmd Cmder) error { | |||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ { | |||||
if attempt > 0 { | |||||
time.Sleep(c.retryBackoff(attempt)) | |||||
} | |||||
shard, err := c.cmdShard(cmd) | |||||
if err != nil { | |||||
cmd.setErr(err) | |||||
return err | |||||
} | |||||
err = shard.Client.Process(cmd) | |||||
if err == nil { | |||||
return nil | |||||
} | |||||
if !internal.IsRetryableError(err, cmd.readTimeout() == nil) { | |||||
return err | |||||
} | |||||
} | |||||
return cmd.Err() | |||||
} | |||||
func (c *Ring) Pipeline() Pipeliner { | |||||
pipe := Pipeline{ | |||||
exec: c.processPipeline, | |||||
} | |||||
pipe.cmdable.setProcessor(pipe.Process) | |||||
return &pipe | |||||
} | |||||
func (c *Ring) Pipelined(fn func(Pipeliner) error) ([]Cmder, error) { | |||||
return c.Pipeline().Pipelined(fn) | |||||
} | |||||
func (c *Ring) WrapProcessPipeline( | |||||
fn func(oldProcess func([]Cmder) error) func([]Cmder) error, | |||||
) { | |||||
c.processPipeline = fn(c.processPipeline) | |||||
} | |||||
func (c *Ring) defaultProcessPipeline(cmds []Cmder) error { | |||||
cmdsMap := make(map[string][]Cmder) | |||||
for _, cmd := range cmds { | |||||
cmdInfo := c.cmdInfo(cmd.Name()) | |||||
hash := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo)) | |||||
if hash != "" { | |||||
hash = c.shards.Hash(hashtag.Key(hash)) | |||||
} | |||||
cmdsMap[hash] = append(cmdsMap[hash], cmd) | |||||
} | |||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ { | |||||
if attempt > 0 { | |||||
time.Sleep(c.retryBackoff(attempt)) | |||||
} | |||||
var mu sync.Mutex | |||||
var failedCmdsMap map[string][]Cmder | |||||
var wg sync.WaitGroup | |||||
for hash, cmds := range cmdsMap { | |||||
wg.Add(1) | |||||
go func(hash string, cmds []Cmder) { | |||||
defer wg.Done() | |||||
shard, err := c.shards.GetByHash(hash) | |||||
if err != nil { | |||||
setCmdsErr(cmds, err) | |||||
return | |||||
} | |||||
cn, err := shard.Client.getConn() | |||||
if err != nil { | |||||
setCmdsErr(cmds, err) | |||||
return | |||||
} | |||||
canRetry, err := shard.Client.pipelineProcessCmds(cn, cmds) | |||||
shard.Client.releaseConnStrict(cn, err) | |||||
if canRetry && internal.IsRetryableError(err, true) { | |||||
mu.Lock() | |||||
if failedCmdsMap == nil { | |||||
failedCmdsMap = make(map[string][]Cmder) | |||||
} | |||||
failedCmdsMap[hash] = cmds | |||||
mu.Unlock() | |||||
} | |||||
}(hash, cmds) | |||||
} | |||||
wg.Wait() | |||||
if len(failedCmdsMap) == 0 { | |||||
break | |||||
} | |||||
cmdsMap = failedCmdsMap | |||||
} | |||||
return cmdsFirstErr(cmds) | |||||
} | |||||
func (c *Ring) TxPipeline() Pipeliner { | |||||
panic("not implemented") | |||||
} | |||||
func (c *Ring) TxPipelined(fn func(Pipeliner) error) ([]Cmder, error) { | |||||
panic("not implemented") | |||||
} | |||||
// Close closes the ring client, releasing any open resources. | |||||
// | |||||
// It is rare to Close a Ring, as the Ring is meant to be long-lived | |||||
// and shared between many goroutines. | |||||
func (c *Ring) Close() error { | |||||
return c.shards.Close() | |||||
} | |||||
func newConsistentHash(opt *RingOptions) *consistenthash.Map { | |||||
return consistenthash.New(opt.HashReplicas, consistenthash.Hash(opt.Hash)) | |||||
} |
package redis | |||||
import ( | |||||
"crypto/sha1" | |||||
"encoding/hex" | |||||
"io" | |||||
"strings" | |||||
) | |||||
type scripter interface { | |||||
Eval(script string, keys []string, args ...interface{}) *Cmd | |||||
EvalSha(sha1 string, keys []string, args ...interface{}) *Cmd | |||||
ScriptExists(hashes ...string) *BoolSliceCmd | |||||
ScriptLoad(script string) *StringCmd | |||||
} | |||||
var _ scripter = (*Client)(nil) | |||||
var _ scripter = (*Ring)(nil) | |||||
var _ scripter = (*ClusterClient)(nil) | |||||
type Script struct { | |||||
src, hash string | |||||
} | |||||
func NewScript(src string) *Script { | |||||
h := sha1.New() | |||||
io.WriteString(h, src) | |||||
return &Script{ | |||||
src: src, | |||||
hash: hex.EncodeToString(h.Sum(nil)), | |||||
} | |||||
} | |||||
func (s *Script) Hash() string { | |||||
return s.hash | |||||
} | |||||
func (s *Script) Load(c scripter) *StringCmd { | |||||
return c.ScriptLoad(s.src) | |||||
} | |||||
func (s *Script) Exists(c scripter) *BoolSliceCmd { | |||||
return c.ScriptExists(s.hash) | |||||
} | |||||
func (s *Script) Eval(c scripter, keys []string, args ...interface{}) *Cmd { | |||||
return c.Eval(s.src, keys, args...) | |||||
} | |||||
func (s *Script) EvalSha(c scripter, keys []string, args ...interface{}) *Cmd { | |||||
return c.EvalSha(s.hash, keys, args...) | |||||
} | |||||
// Run optimistically uses EVALSHA to run the script. If script does not exist | |||||
// it is retried using EVAL. | |||||
func (s *Script) Run(c scripter, keys []string, args ...interface{}) *Cmd { | |||||
r := s.EvalSha(c, keys, args...) | |||||
if err := r.Err(); err != nil && strings.HasPrefix(err.Error(), "NOSCRIPT ") { | |||||
return s.Eval(c, keys, args...) | |||||
} | |||||
return r | |||||
} |
package redis | |||||
import ( | |||||
"crypto/tls" | |||||
"errors" | |||||
"net" | |||||
"strings" | |||||
"sync" | |||||
"time" | |||||
"github.com/go-redis/redis/internal" | |||||
"github.com/go-redis/redis/internal/pool" | |||||
) | |||||
//------------------------------------------------------------------------------ | |||||
// FailoverOptions are used to configure a failover client and should | |||||
// be passed to NewFailoverClient. | |||||
type FailoverOptions struct { | |||||
// The master name. | |||||
MasterName string | |||||
// A seed list of host:port addresses of sentinel nodes. | |||||
SentinelAddrs []string | |||||
// Following options are copied from Options struct. | |||||
OnConnect func(*Conn) error | |||||
Password string | |||||
DB int | |||||
MaxRetries int | |||||
MinRetryBackoff time.Duration | |||||
MaxRetryBackoff time.Duration | |||||
DialTimeout time.Duration | |||||
ReadTimeout time.Duration | |||||
WriteTimeout time.Duration | |||||
PoolSize int | |||||
MinIdleConns int | |||||
MaxConnAge time.Duration | |||||
PoolTimeout time.Duration | |||||
IdleTimeout time.Duration | |||||
IdleCheckFrequency time.Duration | |||||
TLSConfig *tls.Config | |||||
} | |||||
func (opt *FailoverOptions) options() *Options { | |||||
return &Options{ | |||||
Addr: "FailoverClient", | |||||
OnConnect: opt.OnConnect, | |||||
DB: opt.DB, | |||||
Password: opt.Password, | |||||
MaxRetries: opt.MaxRetries, | |||||
DialTimeout: opt.DialTimeout, | |||||
ReadTimeout: opt.ReadTimeout, | |||||
WriteTimeout: opt.WriteTimeout, | |||||
PoolSize: opt.PoolSize, | |||||
PoolTimeout: opt.PoolTimeout, | |||||
IdleTimeout: opt.IdleTimeout, | |||||
IdleCheckFrequency: opt.IdleCheckFrequency, | |||||
TLSConfig: opt.TLSConfig, | |||||
} | |||||
} | |||||
// NewFailoverClient returns a Redis client that uses Redis Sentinel | |||||
// for automatic failover. It's safe for concurrent use by multiple | |||||
// goroutines. | |||||
func NewFailoverClient(failoverOpt *FailoverOptions) *Client { | |||||
opt := failoverOpt.options() | |||||
opt.init() | |||||
failover := &sentinelFailover{ | |||||
masterName: failoverOpt.MasterName, | |||||
sentinelAddrs: failoverOpt.SentinelAddrs, | |||||
opt: opt, | |||||
} | |||||
c := Client{ | |||||
baseClient: baseClient{ | |||||
opt: opt, | |||||
connPool: failover.Pool(), | |||||
onClose: func() error { | |||||
return failover.Close() | |||||
}, | |||||
}, | |||||
} | |||||
c.baseClient.init() | |||||
c.cmdable.setProcessor(c.Process) | |||||
return &c | |||||
} | |||||
//------------------------------------------------------------------------------ | |||||
type SentinelClient struct { | |||||
baseClient | |||||
} | |||||
func NewSentinelClient(opt *Options) *SentinelClient { | |||||
opt.init() | |||||
c := &SentinelClient{ | |||||
baseClient: baseClient{ | |||||
opt: opt, | |||||
connPool: newConnPool(opt), | |||||
}, | |||||
} | |||||
c.baseClient.init() | |||||
return c | |||||
} | |||||
func (c *SentinelClient) 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. | |||||
func (c *SentinelClient) 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 *SentinelClient) PSubscribe(channels ...string) *PubSub { | |||||
pubsub := c.pubSub() | |||||
if len(channels) > 0 { | |||||
_ = pubsub.PSubscribe(channels...) | |||||
} | |||||
return pubsub | |||||
} | |||||
func (c *SentinelClient) GetMasterAddrByName(name string) *StringSliceCmd { | |||||
cmd := NewStringSliceCmd("sentinel", "get-master-addr-by-name", name) | |||||
c.Process(cmd) | |||||
return cmd | |||||
} | |||||
func (c *SentinelClient) Sentinels(name string) *SliceCmd { | |||||
cmd := NewSliceCmd("sentinel", "sentinels", name) | |||||
c.Process(cmd) | |||||
return cmd | |||||
} | |||||
// Failover forces a failover as if the master was not reachable, and without | |||||
// asking for agreement to other Sentinels. | |||||
func (c *SentinelClient) Failover(name string) *StatusCmd { | |||||
cmd := NewStatusCmd("sentinel", "failover", name) | |||||
c.Process(cmd) | |||||
return cmd | |||||
} | |||||
// Reset resets all the masters with matching name. The pattern argument is a | |||||
// glob-style pattern. The reset process clears any previous state in a master | |||||
// (including a failover in progress), and removes every slave and sentinel | |||||
// already discovered and associated with the master. | |||||
func (c *SentinelClient) Reset(pattern string) *IntCmd { | |||||
cmd := NewIntCmd("sentinel", "reset", pattern) | |||||
c.Process(cmd) | |||||
return cmd | |||||
} | |||||
type sentinelFailover struct { | |||||
sentinelAddrs []string | |||||
opt *Options | |||||
pool *pool.ConnPool | |||||
poolOnce sync.Once | |||||
mu sync.RWMutex | |||||
masterName string | |||||
_masterAddr string | |||||
sentinel *SentinelClient | |||||
pubsub *PubSub | |||||
} | |||||
func (c *sentinelFailover) Close() error { | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
if c.sentinel != nil { | |||||
return c.closeSentinel() | |||||
} | |||||
return nil | |||||
} | |||||
func (c *sentinelFailover) Pool() *pool.ConnPool { | |||||
c.poolOnce.Do(func() { | |||||
c.opt.Dialer = c.dial | |||||
c.pool = newConnPool(c.opt) | |||||
}) | |||||
return c.pool | |||||
} | |||||
func (c *sentinelFailover) dial() (net.Conn, error) { | |||||
addr, err := c.MasterAddr() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return net.DialTimeout("tcp", addr, c.opt.DialTimeout) | |||||
} | |||||
func (c *sentinelFailover) MasterAddr() (string, error) { | |||||
addr, err := c.masterAddr() | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
c.switchMaster(addr) | |||||
return addr, nil | |||||
} | |||||
func (c *sentinelFailover) masterAddr() (string, error) { | |||||
addr := c.getMasterAddr() | |||||
if addr != "" { | |||||
return addr, nil | |||||
} | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
for i, sentinelAddr := range c.sentinelAddrs { | |||||
sentinel := NewSentinelClient(&Options{ | |||||
Addr: sentinelAddr, | |||||
MaxRetries: c.opt.MaxRetries, | |||||
DialTimeout: c.opt.DialTimeout, | |||||
ReadTimeout: c.opt.ReadTimeout, | |||||
WriteTimeout: c.opt.WriteTimeout, | |||||
PoolSize: c.opt.PoolSize, | |||||
PoolTimeout: c.opt.PoolTimeout, | |||||
IdleTimeout: c.opt.IdleTimeout, | |||||
IdleCheckFrequency: c.opt.IdleCheckFrequency, | |||||
TLSConfig: c.opt.TLSConfig, | |||||
}) | |||||
masterAddr, err := sentinel.GetMasterAddrByName(c.masterName).Result() | |||||
if err != nil { | |||||
internal.Logf("sentinel: GetMasterAddrByName master=%q failed: %s", | |||||
c.masterName, err) | |||||
_ = sentinel.Close() | |||||
continue | |||||
} | |||||
// Push working sentinel to the top. | |||||
c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0] | |||||
c.setSentinel(sentinel) | |||||
addr := net.JoinHostPort(masterAddr[0], masterAddr[1]) | |||||
return addr, nil | |||||
} | |||||
return "", errors.New("redis: all sentinels are unreachable") | |||||
} | |||||
func (c *sentinelFailover) getMasterAddr() string { | |||||
c.mu.RLock() | |||||
sentinel := c.sentinel | |||||
c.mu.RUnlock() | |||||
if sentinel == nil { | |||||
return "" | |||||
} | |||||
addr, err := sentinel.GetMasterAddrByName(c.masterName).Result() | |||||
if err != nil { | |||||
internal.Logf("sentinel: GetMasterAddrByName name=%q failed: %s", | |||||
c.masterName, err) | |||||
c.mu.Lock() | |||||
if c.sentinel == sentinel { | |||||
c.closeSentinel() | |||||
} | |||||
c.mu.Unlock() | |||||
return "" | |||||
} | |||||
return net.JoinHostPort(addr[0], addr[1]) | |||||
} | |||||
func (c *sentinelFailover) switchMaster(addr string) { | |||||
c.mu.RLock() | |||||
masterAddr := c._masterAddr | |||||
c.mu.RUnlock() | |||||
if masterAddr == addr { | |||||
return | |||||
} | |||||
c.mu.Lock() | |||||
defer c.mu.Unlock() | |||||
internal.Logf("sentinel: new master=%q addr=%q", | |||||
c.masterName, addr) | |||||
_ = c.Pool().Filter(func(cn *pool.Conn) bool { | |||||
return cn.RemoteAddr().String() != addr | |||||
}) | |||||
c._masterAddr = addr | |||||
} | |||||
func (c *sentinelFailover) setSentinel(sentinel *SentinelClient) { | |||||
c.discoverSentinels(sentinel) | |||||
c.sentinel = sentinel | |||||
c.pubsub = sentinel.Subscribe("+switch-master") | |||||
go c.listen(c.pubsub) | |||||
} | |||||
func (c *sentinelFailover) closeSentinel() error { | |||||
var firstErr error | |||||
err := c.pubsub.Close() | |||||
if err != nil && firstErr == err { | |||||
firstErr = err | |||||
} | |||||
c.pubsub = nil | |||||
err = c.sentinel.Close() | |||||
if err != nil && firstErr == err { | |||||
firstErr = err | |||||
} | |||||
c.sentinel = nil | |||||
return firstErr | |||||
} | |||||
func (c *sentinelFailover) discoverSentinels(sentinel *SentinelClient) { | |||||
sentinels, err := sentinel.Sentinels(c.masterName).Result() | |||||
if err != nil { | |||||
internal.Logf("sentinel: Sentinels master=%q failed: %s", c.masterName, err) | |||||
return | |||||
} | |||||
for _, sentinel := range sentinels { | |||||
vals := sentinel.([]interface{}) | |||||
for i := 0; i < len(vals); i += 2 { | |||||
key := vals[i].(string) | |||||
if key == "name" { | |||||
sentinelAddr := vals[i+1].(string) | |||||
if !contains(c.sentinelAddrs, sentinelAddr) { | |||||
internal.Logf("sentinel: discovered new sentinel=%q for master=%q", | |||||
sentinelAddr, c.masterName) | |||||
c.sentinelAddrs = append(c.sentinelAddrs, sentinelAddr) | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
func (c *sentinelFailover) listen(pubsub *PubSub) { | |||||
ch := pubsub.Channel() | |||||
for { | |||||
msg, ok := <-ch | |||||
if !ok { | |||||
break | |||||
} | |||||
switch msg.Channel { | |||||
case "+switch-master": | |||||
parts := strings.Split(msg.Payload, " ") | |||||
if parts[0] != c.masterName { | |||||
internal.Logf("sentinel: ignore addr for master=%q", parts[0]) | |||||
continue | |||||
} | |||||
addr := net.JoinHostPort(parts[3], parts[4]) | |||||
c.switchMaster(addr) | |||||
} | |||||
} | |||||
} | |||||
func contains(slice []string, str string) bool { | |||||
for _, s := range slice { | |||||
if s == str { | |||||
return true | |||||
} | |||||
} | |||||
return false | |||||
} |
package redis | |||||
import ( | |||||
"github.com/go-redis/redis/internal/pool" | |||||
"github.com/go-redis/redis/internal/proto" | |||||
) | |||||
// TxFailedErr transaction redis failed. | |||||
const TxFailedErr = proto.RedisError("redis: transaction failed") | |||||
// Tx implements Redis transactions as described in | |||||
// http://redis.io/topics/transactions. It's NOT safe for concurrent use | |||||
// by multiple goroutines, because Exec resets list of watched keys. | |||||
// If you don't need WATCH it is better to use Pipeline. | |||||
type Tx struct { | |||||
statefulCmdable | |||||
baseClient | |||||
} | |||||
func (c *Client) newTx() *Tx { | |||||
tx := Tx{ | |||||
baseClient: baseClient{ | |||||
opt: c.opt, | |||||
connPool: pool.NewStickyConnPool(c.connPool.(*pool.ConnPool), true), | |||||
}, | |||||
} | |||||
tx.baseClient.init() | |||||
tx.statefulCmdable.setProcessor(tx.Process) | |||||
return &tx | |||||
} | |||||
// Watch prepares a transaction and marks the keys to be watched | |||||
// for conditional execution if there are any keys. | |||||
// | |||||
// The transaction is automatically closed when fn exits. | |||||
func (c *Client) Watch(fn func(*Tx) error, keys ...string) error { | |||||
tx := c.newTx() | |||||
if len(keys) > 0 { | |||||
if err := tx.Watch(keys...).Err(); err != nil { | |||||
_ = tx.Close() | |||||
return err | |||||
} | |||||
} | |||||
err := fn(tx) | |||||
_ = tx.Close() | |||||
return err | |||||
} | |||||
// Close closes the transaction, releasing any open resources. | |||||
func (c *Tx) Close() error { | |||||
_ = c.Unwatch().Err() | |||||
return c.baseClient.Close() | |||||
} | |||||
// Watch marks the keys to be watched for conditional execution | |||||
// of a transaction. | |||||
func (c *Tx) Watch(keys ...string) *StatusCmd { | |||||
args := make([]interface{}, 1+len(keys)) | |||||
args[0] = "watch" | |||||
for i, key := range keys { | |||||
args[1+i] = key | |||||
} | |||||
cmd := NewStatusCmd(args...) | |||||
c.Process(cmd) | |||||
return cmd | |||||
} | |||||
// Unwatch flushes all the previously watched keys for a transaction. | |||||
func (c *Tx) Unwatch(keys ...string) *StatusCmd { | |||||
args := make([]interface{}, 1+len(keys)) | |||||
args[0] = "unwatch" | |||||
for i, key := range keys { | |||||
args[1+i] = key | |||||
} | |||||
cmd := NewStatusCmd(args...) | |||||
c.Process(cmd) | |||||
return cmd | |||||
} | |||||
// Pipeline creates a new pipeline. It is more convenient to use Pipelined. | |||||
func (c *Tx) Pipeline() Pipeliner { | |||||
pipe := Pipeline{ | |||||
exec: c.processTxPipeline, | |||||
} | |||||
pipe.statefulCmdable.setProcessor(pipe.Process) | |||||
return &pipe | |||||
} | |||||
// Pipelined executes commands queued in the fn in a transaction. | |||||
// | |||||
// When using WATCH, EXEC will execute commands only if the watched keys | |||||
// were not modified, allowing for a check-and-set mechanism. | |||||
// | |||||
// Exec always returns list of commands. If transaction fails | |||||
// TxFailedErr is returned. Otherwise Exec returns an error of the first | |||||
// failed command or nil. | |||||
func (c *Tx) Pipelined(fn func(Pipeliner) error) ([]Cmder, error) { | |||||
return c.Pipeline().Pipelined(fn) | |||||
} | |||||
// TxPipelined is an alias for Pipelined. | |||||
func (c *Tx) TxPipelined(fn func(Pipeliner) error) ([]Cmder, error) { | |||||
return c.Pipelined(fn) | |||||
} | |||||
// TxPipeline is an alias for Pipeline. | |||||
func (c *Tx) TxPipeline() Pipeliner { | |||||
return c.Pipeline() | |||||
} |
package redis | |||||
import ( | |||||
"crypto/tls" | |||||
"time" | |||||
) | |||||
// UniversalOptions information is required by UniversalClient to establish | |||||
// connections. | |||||
type UniversalOptions struct { | |||||
// Either a single address or a seed list of host:port addresses | |||||
// of cluster/sentinel nodes. | |||||
Addrs []string | |||||
// Database to be selected after connecting to the server. | |||||
// Only single-node and failover clients. | |||||
DB int | |||||
// Common options. | |||||
OnConnect func(*Conn) error | |||||
Password string | |||||
MaxRetries int | |||||
MinRetryBackoff time.Duration | |||||
MaxRetryBackoff time.Duration | |||||
DialTimeout time.Duration | |||||
ReadTimeout time.Duration | |||||
WriteTimeout time.Duration | |||||
PoolSize int | |||||
MinIdleConns int | |||||
MaxConnAge time.Duration | |||||
PoolTimeout time.Duration | |||||
IdleTimeout time.Duration | |||||
IdleCheckFrequency time.Duration | |||||
TLSConfig *tls.Config | |||||
// Only cluster clients. | |||||
MaxRedirects int | |||||
ReadOnly bool | |||||
RouteByLatency bool | |||||
RouteRandomly bool | |||||
// The sentinel master name. | |||||
// Only failover clients. | |||||
MasterName string | |||||
} | |||||
func (o *UniversalOptions) cluster() *ClusterOptions { | |||||
if len(o.Addrs) == 0 { | |||||
o.Addrs = []string{"127.0.0.1:6379"} | |||||
} | |||||
return &ClusterOptions{ | |||||
Addrs: o.Addrs, | |||||
OnConnect: o.OnConnect, | |||||
Password: o.Password, | |||||
MaxRedirects: o.MaxRedirects, | |||||
ReadOnly: o.ReadOnly, | |||||
RouteByLatency: o.RouteByLatency, | |||||
RouteRandomly: o.RouteRandomly, | |||||
MaxRetries: o.MaxRetries, | |||||
MinRetryBackoff: o.MinRetryBackoff, | |||||
MaxRetryBackoff: o.MaxRetryBackoff, | |||||
DialTimeout: o.DialTimeout, | |||||
ReadTimeout: o.ReadTimeout, | |||||
WriteTimeout: o.WriteTimeout, | |||||
PoolSize: o.PoolSize, | |||||
MinIdleConns: o.MinIdleConns, | |||||
MaxConnAge: o.MaxConnAge, | |||||
PoolTimeout: o.PoolTimeout, | |||||
IdleTimeout: o.IdleTimeout, | |||||
IdleCheckFrequency: o.IdleCheckFrequency, | |||||
TLSConfig: o.TLSConfig, | |||||
} | |||||
} | |||||
func (o *UniversalOptions) failover() *FailoverOptions { | |||||
if len(o.Addrs) == 0 { | |||||
o.Addrs = []string{"127.0.0.1:26379"} | |||||
} | |||||
return &FailoverOptions{ | |||||
SentinelAddrs: o.Addrs, | |||||
MasterName: o.MasterName, | |||||
OnConnect: o.OnConnect, | |||||
DB: o.DB, | |||||
Password: o.Password, | |||||
MaxRetries: o.MaxRetries, | |||||
MinRetryBackoff: o.MinRetryBackoff, | |||||
MaxRetryBackoff: o.MaxRetryBackoff, | |||||
DialTimeout: o.DialTimeout, | |||||
ReadTimeout: o.ReadTimeout, | |||||
WriteTimeout: o.WriteTimeout, | |||||
PoolSize: o.PoolSize, | |||||
MinIdleConns: o.MinIdleConns, | |||||
MaxConnAge: o.MaxConnAge, | |||||
PoolTimeout: o.PoolTimeout, | |||||
IdleTimeout: o.IdleTimeout, | |||||
IdleCheckFrequency: o.IdleCheckFrequency, | |||||
TLSConfig: o.TLSConfig, | |||||
} | |||||
} | |||||
func (o *UniversalOptions) simple() *Options { | |||||
addr := "127.0.0.1:6379" | |||||
if len(o.Addrs) > 0 { | |||||
addr = o.Addrs[0] | |||||
} | |||||
return &Options{ | |||||
Addr: addr, | |||||
OnConnect: o.OnConnect, | |||||
DB: o.DB, | |||||
Password: o.Password, | |||||
MaxRetries: o.MaxRetries, | |||||
MinRetryBackoff: o.MinRetryBackoff, | |||||
MaxRetryBackoff: o.MaxRetryBackoff, | |||||
DialTimeout: o.DialTimeout, | |||||
ReadTimeout: o.ReadTimeout, | |||||
WriteTimeout: o.WriteTimeout, | |||||
PoolSize: o.PoolSize, | |||||
MinIdleConns: o.MinIdleConns, | |||||
MaxConnAge: o.MaxConnAge, | |||||
PoolTimeout: o.PoolTimeout, | |||||
IdleTimeout: o.IdleTimeout, | |||||
IdleCheckFrequency: o.IdleCheckFrequency, | |||||
TLSConfig: o.TLSConfig, | |||||
} | |||||
} | |||||
// -------------------------------------------------------------------- | |||||
// UniversalClient is an abstract client which - based on the provided options - | |||||
// can connect to either clusters, or sentinel-backed failover instances or simple | |||||
// single-instance servers. This can be useful for testing cluster-specific | |||||
// applications locally. | |||||
type UniversalClient interface { | |||||
Cmdable | |||||
Watch(fn func(*Tx) error, keys ...string) error | |||||
Process(cmd Cmder) error | |||||
WrapProcess(fn func(oldProcess func(cmd Cmder) error) func(cmd Cmder) error) | |||||
Subscribe(channels ...string) *PubSub | |||||
PSubscribe(channels ...string) *PubSub | |||||
Close() error | |||||
} | |||||
var _ UniversalClient = (*Client)(nil) | |||||
var _ UniversalClient = (*ClusterClient)(nil) | |||||
// NewUniversalClient returns a new multi client. The type of client returned depends | |||||
// on the following three conditions: | |||||
// | |||||
// 1. if a MasterName is passed a sentinel-backed FailoverClient will be returned | |||||
// 2. if the number of Addrs is two or more, a ClusterClient will be returned | |||||
// 3. otherwise, a single-node redis Client will be returned. | |||||
func NewUniversalClient(opts *UniversalOptions) UniversalClient { | |||||
if opts.MasterName != "" { | |||||
return NewFailoverClient(opts.failover()) | |||||
} else if len(opts.Addrs) > 1 { | |||||
return NewClusterClient(opts.cluster()) | |||||
} | |||||
return NewClient(opts.simple()) | |||||
} |
github.com/go-macaron/session/redis | github.com/go-macaron/session/redis | ||||
# github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90 | # github.com/go-macaron/toolbox v0.0.0-20180818072302-a77f45a7ce90 | ||||
github.com/go-macaron/toolbox | github.com/go-macaron/toolbox | ||||
# github.com/go-redis/redis v6.15.2+incompatible | |||||
github.com/go-redis/redis | |||||
github.com/go-redis/redis/internal | |||||
github.com/go-redis/redis/internal/consistenthash | |||||
github.com/go-redis/redis/internal/hashtag | |||||
github.com/go-redis/redis/internal/pool | |||||
github.com/go-redis/redis/internal/proto | |||||
github.com/go-redis/redis/internal/util | |||||
# github.com/go-sql-driver/mysql v1.4.0 => github.com/go-sql-driver/mysql v0.0.0-20181218123637-c45f530f8e7f | # github.com/go-sql-driver/mysql v1.4.0 => github.com/go-sql-driver/mysql v0.0.0-20181218123637-c45f530f8e7f | ||||
github.com/go-sql-driver/mysql | github.com/go-sql-driver/mysql | ||||
# github.com/go-xorm/builder v0.3.3 | # github.com/go-xorm/builder v0.3.3 |