├── .dockerignore ├── .github └── workflows │ └── validate.yml ├── .gitignore ├── LICENSE ├── Makefile ├── SECURITY.md ├── cmd ├── bench │ └── bench.go └── redplex │ └── redplex.go ├── dialer.go ├── go.mod ├── go.sum ├── prom.go ├── protocol.go ├── protocol_test.go ├── pubsub.go ├── pubsub_test.go ├── readme.md ├── server.go └── server_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /redplex 2 | /redplex.exe 3 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-go@v2 10 | with: 11 | go-version: '^1.16.3' 12 | - uses: supercharge/redis-github-action@1.2.0 13 | with: 14 | redis-version: 6 15 | - run: go test -v -race ./ 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | #### go #### 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | # Output of the go coverage tool, specifically when used with LiteIDE 29 | *.out 30 | 31 | /.idea 32 | /vendor/*/ 33 | /redplex 34 | /bench 35 | *.profile 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_SRC = $(wildcard *.go) 2 | 3 | redplex: $(GO_SRC) 4 | @printf " → Compiling %s \n" $@ 5 | @go build ./cmd/$@ 6 | @printf " ✔ Compiled %s \n" $@ 7 | 8 | lint: $(GO_SRC) 9 | @go vet ./ && printf " ✔ Vet passed \n" 10 | @golint ./ && printf " ✔ Lint passed \n" 11 | 12 | check: $(GO_SRC) lint 13 | @go test -v -race ./ 14 | @printf " ✔ Tests passed \n" 15 | 16 | .PHONY: lint check 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /cmd/bench/bench.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "runtime/pprof" 7 | 8 | "time" 9 | 10 | "fmt" 11 | 12 | "sync" 13 | "sync/atomic" 14 | 15 | "github.com/microsoft/redplex" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | var ( 20 | benchData = []byte("*3\r\n$7\r\nmessage\r\n$7\r\nchannel\r\n$345\r\n" + `{"channel":314,"id":"fd269030-aacd-11e7-b70f-fd0fddd98285","user_name":"Jeff","user_id":1355,"user_roles":["Pro","User"],"user_level":94,"user_avatar":"https://uploads.beam.pro/avatar/qryjcpn1-1355.jpg","message":{"message":[{"type":"text","data":"Finally. We're back on the computer.","text":"Finally. We're back on the computer."}],"meta":{}}}` + "\r\n") 21 | subscribe = redplex.NewRequest("subscribe", 1).Bulk([]byte(`channel`)).Bytes() 22 | remoteAddress = "127.0.0.1:3100" 23 | redplexAddress = "127.0.0.1:3101" 24 | benchedBytes = 1024 * 1024 * 10 25 | ) 26 | 27 | func main() { 28 | os.Remove(remoteAddress) 29 | os.Remove(redplexAddress) 30 | logrus.SetLevel(logrus.DebugLevel) 31 | 32 | benchListener, err := net.Listen("tcp", remoteAddress) 33 | if err != nil { 34 | logrus.WithError(err).Fatal("redplex/bench: could not listen on the requested address") 35 | os.Exit(1) 36 | } 37 | redplexListener, err := net.Listen("tcp", redplexAddress) 38 | if err != nil { 39 | logrus.WithError(err).Fatal("redplex/bench: could not listen on the requested address") 40 | os.Exit(1) 41 | } 42 | 43 | start := make(chan struct{}) 44 | go serveBenchRemote(benchListener, start) 45 | go redplex.NewServer(redplexListener, redplex.NewPubsub(redplex.NewDirectDialer(remoteAddress, 0), time.Second)).Listen() 46 | 47 | f, _ := os.Create("cpu.profile") 48 | pprof.StartCPUProfile(f) 49 | runBenchmark(start) 50 | pprof.StopCPUProfile() 51 | } 52 | 53 | func runBenchmark(start chan<- struct{}) { 54 | 55 | var ( 56 | wg sync.WaitGroup 57 | totalBytes int64 58 | ) 59 | 60 | for i := 0; i < 15; i++ { 61 | wg.Add(1) 62 | go func() { 63 | cnx, err := net.Dial("tcp", redplexAddress) 64 | if err != nil { 65 | logrus.WithError(err).Fatal("redplex/bench: error dialing to server") 66 | } 67 | 68 | if _, err := cnx.Write(subscribe); err != nil { 69 | logrus.WithError(err).Fatal("redplex/bench: error subscribing") 70 | } 71 | 72 | wg.Done() 73 | wg.Wait() 74 | 75 | buf := make([]byte, 32*1024) 76 | for { 77 | n, err := cnx.Read(buf) 78 | atomic.AddInt64(&totalBytes, int64(n)) 79 | if err != nil { 80 | logrus.WithError(err).Fatal("redplex/bench: error in reader") 81 | } 82 | } 83 | }() 84 | } 85 | 86 | wg.Wait() 87 | start <- struct{}{} 88 | time.Sleep(time.Second) // give it a second to ramp up 89 | atomic.StoreInt64(&totalBytes, 0) 90 | 91 | go func() { 92 | last := int64(0) 93 | for { 94 | time.Sleep(time.Second) 95 | next := atomic.LoadInt64(&totalBytes) 96 | fmt.Println("delta", next-last) 97 | last = next 98 | } 99 | }() 100 | 101 | started := time.Now() 102 | time.Sleep(15 * time.Second) 103 | delta := time.Now().Sub(started) 104 | seconds := float64(delta) / float64(time.Second) 105 | gigabits := float64(totalBytes) / 1024 / 1024 * 8 106 | fmt.Printf("Read %d bytes in %.2fs (%.0f Mbps)\n", totalBytes, seconds, gigabits/seconds) 107 | } 108 | 109 | func serveBenchRemote(l net.Listener, start <-chan struct{}) { 110 | cnx, err := l.Accept() 111 | if err != nil { 112 | logrus.WithError(err).Fatal("redplex/bench: error accepting connection") 113 | os.Exit(1) 114 | } 115 | 116 | <-start 117 | 118 | toWrite := []byte{} 119 | for i := 0; i < 1000; i++ { 120 | toWrite = append(toWrite, benchData...) 121 | } 122 | 123 | for { 124 | cnx.Write(toWrite) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cmd/redplex/redplex.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | _ "net/http/pprof" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/microsoft/redplex" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | "github.com/sirupsen/logrus" 14 | kingpin "gopkg.in/alecthomas/kingpin.v2" 15 | ) 16 | 17 | var ( 18 | address = kingpin.Flag("listen", "Address to listen on").Short('l').Default("127.0.0.1:3000").String() 19 | network = kingpin.Flag("network", "Network to listen on").Short('n').Default("tcp").String() 20 | remote = kingpin.Flag("remote", "Remote address of the Redis server").Default("127.0.0.1:6379").String() 21 | remoteEnv = kingpin.Flag("remote-env", "Environment variable for --remote").Default("").String() 22 | passwordEnv = kingpin.Flag("password-env", "Environment variable for redis password").Default("REDIS_PASSWORD").String() 23 | sentinelEnv = kingpin.Flag("sentinel-env", "Environment variable for sentinel password").Default("SENTINEL_PASSWORD").String() 24 | remoteNetwork = kingpin.Flag("remote-network", "Remote network to dial through (usually tcp or tcp6)").Default("tcp").String() 25 | useTLS = kingpin.Flag("use-tls", "Use TLS to connect to redis").Default("false").Bool() 26 | sentinels = kingpin.Flag("sentinels", "A list of Redis sentinel addresses").Strings() 27 | sentinelMaster = kingpin.Flag("sentinel-name", "The name of the sentinel master").String() 28 | logLevel = kingpin.Flag("log-level", "Log level (one of debug, info, warn, error").Default("info").String() 29 | dialTimeout = kingpin.Flag("dial-timeout", "Timeout connecting to Redis").Default("10s").Duration() 30 | writeTimeout = kingpin.Flag("write-timeout", "Timeout during write operations").Default("2s").Duration() 31 | pprofServer = kingpin.Flag("pprof-server", "Address to bind a pprof server on. Not bound if empty.").String() 32 | ) 33 | 34 | func main() { 35 | kingpin.UsageTemplate(kingpin.DefaultUsageTemplate) 36 | 37 | kingpin.Parse() 38 | level, err := logrus.ParseLevel(*logLevel) 39 | if err != nil { 40 | logrus.WithError(err).Fatal("redplex/main: error parsing log level") 41 | os.Exit(1) 42 | } 43 | 44 | listener, err := net.Listen(*network, *address) 45 | if err != nil { 46 | logrus.WithError(err).Fatal("redplex/main: could not listen on the requested address") 47 | os.Exit(1) 48 | } 49 | 50 | logrus.SetLevel(level) 51 | 52 | password := os.Getenv(*passwordEnv) 53 | sentinelPassword := os.Getenv(*sentinelEnv) 54 | 55 | var dialer redplex.Dialer 56 | if *sentinelMaster != "" { 57 | dialer = redplex.NewSentinelDialer(*remoteNetwork, *sentinels, *sentinelMaster, password, sentinelPassword, *useTLS, *dialTimeout) 58 | } else { 59 | useRemote := *remote 60 | useRemoveEnv := *remoteEnv 61 | if useRemoveEnv != "" { 62 | useRemote = os.Getenv(useRemoveEnv) 63 | } 64 | dialer = redplex.NewDirectDialer(*remoteNetwork, useRemote, password, *useTLS, *dialTimeout) 65 | } 66 | 67 | closed := make(chan struct{}) 68 | go func() { 69 | awaitInterrupt() 70 | close(closed) 71 | listener.Close() 72 | }() 73 | 74 | go startPprof() 75 | 76 | logrus.Debugf("redplex/main: listening on %s://%s", *network, *address) 77 | if err := redplex.NewServer(listener, redplex.NewPubsub(dialer, *writeTimeout)).Listen(); err != nil { 78 | select { 79 | case <-closed: 80 | os.Exit(0) 81 | default: 82 | logrus.WithError(err).Fatal("redplex/main: error accepting incoming connections") 83 | } 84 | } 85 | } 86 | 87 | func startPprof() { 88 | if *pprofServer == "" { 89 | return 90 | } 91 | 92 | http.Handle("/stats", promhttp.Handler()) 93 | 94 | if err := http.ListenAndServe(*pprofServer, nil); err != nil { 95 | logrus.WithError(err).Error("redplex/main: could not start pprof server") 96 | } 97 | } 98 | 99 | func awaitInterrupt() { 100 | interrupt := make(chan os.Signal, 1) 101 | signal.Notify(interrupt, syscall.SIGINT) 102 | <-interrupt 103 | } 104 | -------------------------------------------------------------------------------- /dialer.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "time" 7 | 8 | "bufio" 9 | "fmt" 10 | 11 | "github.com/cenkalti/backoff" 12 | "github.com/garyburd/redigo/redis" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // The Dialer is a type that can create a TCP connection to the Redis server. 17 | type Dialer interface { 18 | // Dial attempts to create a connection to the server. 19 | Dial() (net.Conn, error) 20 | } 21 | 22 | // defaultTimeout is used in the DirectDialer 23 | // if a non-zero timeout is not provided. 24 | const defaultTimeout = time.Second * 5 25 | 26 | // DirectDialer creates a direct connection to a single Redis server or IP. 27 | type DirectDialer struct { 28 | network string 29 | address string 30 | password string 31 | useTLS bool 32 | timeout time.Duration 33 | } 34 | 35 | // NewDirectDialer creates a DirectDialer that dials to the given address. 36 | // If the timeout is 0, a default value of 5 seconds will be used. 37 | func NewDirectDialer(network string, address string, password string, useTLS bool, timeout time.Duration) DirectDialer { 38 | if timeout == 0 { 39 | timeout = defaultTimeout 40 | } 41 | 42 | return DirectDialer{network, address, password, useTLS, timeout} 43 | } 44 | 45 | // Dial implements Dialer.Dial 46 | func (d DirectDialer) Dial() (cnx net.Conn, err error) { 47 | dialer := &net.Dialer{ 48 | Timeout: d.timeout, 49 | } 50 | 51 | if d.useTLS { 52 | cnx, err = tls.DialWithDialer(dialer, d.network, d.address, nil) 53 | } else { 54 | cnx, err = dialer.Dial(d.network, d.address) 55 | } 56 | 57 | if err != nil || d.password == "" { 58 | return 59 | } 60 | 61 | line := fmt.Sprintf("*2\r\n$4\r\nAUTH\n\n$%d\r\n%s\r\n", len(d.password), d.password) 62 | _, err = cnx.Write([]byte(line)) 63 | if err != nil { 64 | return 65 | } 66 | 67 | reader := bufio.NewReader(cnx) 68 | 69 | line, err = reader.ReadString('\n') 70 | if err != nil { 71 | return 72 | } 73 | 74 | if line[0] != '+' { 75 | err = fmt.Errorf("Error authenticating to redis: %s", line) 76 | } 77 | 78 | return 79 | } 80 | 81 | // The SentinelDialer dials into the Redis cluster master as defined by 82 | // the assortment of sentinel servers. 83 | type SentinelDialer struct { 84 | network string 85 | sentinels []string 86 | masterName string 87 | password string 88 | sentinelPassword string 89 | useTLS bool 90 | timeout time.Duration 91 | closer chan<- struct{} 92 | } 93 | 94 | // NewSentinelDialer creates a SentinelDialer. 95 | func NewSentinelDialer(network string, sentinels []string, masterName string, password string, sentinelPassword string, useTLS bool, timeout time.Duration) *SentinelDialer { 96 | if timeout == 0 { 97 | timeout = defaultTimeout 98 | } 99 | 100 | return &SentinelDialer{ 101 | network: network, 102 | sentinels: sentinels, 103 | masterName: masterName, 104 | password: password, 105 | sentinelPassword: sentinelPassword, 106 | useTLS: useTLS, 107 | timeout: timeout, 108 | closer: make(chan struct{}), 109 | } 110 | } 111 | 112 | var _ Dialer = &SentinelDialer{} 113 | 114 | // Dial implements Dialer.Dial. It looks up and connects to the Redis master 115 | // dictated by the sentinels. The connection will be closed if we detect 116 | // that the master changes. 117 | func (s *SentinelDialer) Dial() (net.Conn, error) { 118 | close(s.closer) 119 | closer := make(chan struct{}) 120 | s.closer = closer 121 | 122 | master, err := s.getMaster() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | cnx, err := DirectDialer{s.network, master, s.password, s.useTLS, s.timeout}.Dial() 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | logrus.Debugf("redplex/dialer: created connection to %s as the sentinel master", master) 133 | 134 | go s.monitorMaster(cnx, master, closer) 135 | 136 | return cnx, nil 137 | } 138 | 139 | // monitorMaster subscribes to the Sentinel cluster and watches for master 140 | // changes. When a change happens, it closes the old connection. Often a 141 | // failover-triggering event will result in the connection being terminated 142 | // anyway, but this is not always the case--the Sentinels can decide to trigger 143 | // a failover if load is too high on one server, for instance, even though the 144 | // server is generally healthy and the connection will remain alive. 145 | func (s *SentinelDialer) monitorMaster(cnx net.Conn, currentMaster string, closer <-chan struct{}) { 146 | backoff := backoff.NewExponentialBackOff() 147 | backoff.MaxInterval = time.Second * 10 148 | 149 | defer func() { 150 | cnx.Close() 151 | logrus.Infof("redplex/dialer: failed over from %s as it is no longer the cluster master", currentMaster) 152 | }() 153 | 154 | for { 155 | err := s.watchForReelection(backoff, currentMaster, closer) 156 | if err == nil { 157 | return 158 | } 159 | 160 | logrus.WithError(err).Info("redplex/dialer: error connecting to Redis sentinels") 161 | select { 162 | case <-time.After(backoff.NextBackOff()): 163 | continue 164 | case <-closer: 165 | return 166 | } 167 | } 168 | } 169 | 170 | // watchForReelection returns a channel which is closed to when the sentinel 171 | // master changes. Blocks until the subscription is set up. Returns a connection 172 | // which should be closed when we're no longer interested in it. Resets the 173 | // backoff once a connection is established successfully. 174 | func (s *SentinelDialer) watchForReelection(backoff backoff.BackOff, currentMaster string, closer <-chan struct{}) (err error) { 175 | for { 176 | // First off, subscribe to master changes. 177 | psc, err := s.subscribeToElections() 178 | if err != nil { 179 | return err 180 | } 181 | 182 | // Validate the master is current after we set up the pubsub conn 183 | // to avoid races. 184 | if master, err := s.getMaster(); err != nil || master != currentMaster { 185 | psc.Close() 186 | return err 187 | } 188 | 189 | recvd := make(chan struct{}) 190 | go func() { 191 | select { 192 | case <-closer: 193 | psc.Close() 194 | case <-recvd: 195 | } 196 | }() 197 | 198 | // Wait until we get a message on the pubsub channel. Once we do and 199 | // if it's a subscription message, return. 200 | backoff.Reset() 201 | _, didReelect := psc.Receive().(redis.Message) 202 | close(recvd) 203 | psc.Close() 204 | 205 | if didReelect { 206 | return nil 207 | } 208 | 209 | // Otherwise, loop through if we didn't close intentionally.. Occasional 210 | // crashes and timeouts are expected. If we can't connect to anyone 211 | // else, the next loop iteration will return an error. 212 | select { 213 | case <-closer: 214 | return nil 215 | default: 216 | } 217 | } 218 | } 219 | 220 | // getMaster returns the current cluster master. 221 | func (s *SentinelDialer) getMaster() (string, error) { 222 | result, err := s.doUntilSuccess(func(conn redis.Conn) (interface{}, error) { 223 | res, err := redis.Strings(conn.Do("SENTINEL", "get-master-addr-by-name", s.masterName)) 224 | if err != nil { 225 | return "", err 226 | } 227 | 228 | conn.Close() 229 | return net.JoinHostPort(res[0], res[1]), nil 230 | }) 231 | 232 | if err != nil { 233 | return "", err 234 | } 235 | 236 | return result.(string), nil 237 | } 238 | 239 | // subscribeToElections returns a pubsub channel subscribed on a Redis sentinel 240 | // to cluster master changes. 241 | func (s *SentinelDialer) subscribeToElections() (redis.PubSubConn, error) { 242 | result, err := s.doUntilSuccess(func(conn redis.Conn) (interface{}, error) { 243 | psc := redis.PubSubConn{Conn: conn} 244 | if err := psc.Subscribe("+switch-master"); err != nil { 245 | return psc, err 246 | } 247 | 248 | msg := psc.Receive() 249 | if _, ok := msg.(redis.Subscription); !ok { 250 | return psc, fmt.Errorf("redplex/dialer: expected subscription ack from sentinel, got: %+v", msg) 251 | } 252 | 253 | return psc, nil 254 | }) 255 | 256 | if err != nil { 257 | return redis.PubSubConn{}, err 258 | } 259 | 260 | return result.(redis.PubSubConn), nil 261 | } 262 | 263 | // doUntilSuccess runs the querying function against sentinels until one 264 | // returns with a success. If no sentinels respond successfully, the last 265 | // returned error is bubbled. The connection used in a successful reply will 266 | // remain open. 267 | func (s *SentinelDialer) doUntilSuccess(fn func(conn redis.Conn) (interface{}, error)) (result interface{}, err error) { 268 | var cnx redis.Conn 269 | 270 | for _, addr := range s.sentinels { 271 | options := []redis.DialOption{ 272 | redis.DialConnectTimeout(s.timeout), 273 | redis.DialUseTLS(s.useTLS), 274 | } 275 | 276 | if s.sentinelPassword != "" { 277 | options = append(options, redis.DialPassword(s.sentinelPassword)) 278 | } 279 | cnx, err = redis.Dial(s.network, addr, options...) 280 | if err != nil { 281 | continue 282 | } 283 | 284 | result, err = fn(cnx) 285 | if err != nil { 286 | cnx.Close() 287 | continue 288 | } 289 | 290 | return result, nil 291 | } 292 | 293 | return nil, err 294 | } 295 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/microsoft/redplex 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/cenkalti/backoff v2.2.1+incompatible 7 | github.com/garyburd/redigo v1.6.2 8 | github.com/prometheus/client_golang v1.10.0 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/stretchr/testify v1.4.0 11 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 5 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 6 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 7 | github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= 8 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 9 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 10 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 11 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 14 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= 15 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 16 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 17 | github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 18 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 19 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 20 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 21 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 22 | github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= 23 | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 24 | github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= 25 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 26 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 27 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 28 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 29 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 30 | github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= 31 | github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= 32 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 33 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 34 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 35 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 36 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= 37 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 38 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= 39 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 40 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 41 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 42 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 43 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 44 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 45 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 47 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 49 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 50 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 51 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 52 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 53 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 54 | github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 55 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 56 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 57 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 58 | github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= 59 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 60 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 61 | github.com/garyburd/redigo v1.6.2 h1:yE/pwKCrbLpLpQICzYTeZ7JsTA/C53wFTJHaEtRqniM= 62 | github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 63 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 64 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 65 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 66 | github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 67 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 68 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 69 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 70 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 71 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 72 | github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 73 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 74 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 75 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 76 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 77 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 78 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 80 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 81 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 82 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 83 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 84 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 85 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 86 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 87 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 88 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 89 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 90 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 91 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 92 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 93 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 94 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 95 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 96 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 97 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 99 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 100 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 101 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 102 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 103 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 104 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 105 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 106 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 107 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 108 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 109 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 110 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 111 | github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= 112 | github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 113 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 114 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 115 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 116 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 117 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 118 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 119 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 120 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 121 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 122 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 123 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 124 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 125 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 126 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 127 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 128 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 129 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 130 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 131 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 132 | github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= 133 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 134 | github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 135 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 136 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 137 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 138 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 139 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 140 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 141 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 142 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 143 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 144 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 145 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 146 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 147 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 148 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 149 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 150 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 151 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 152 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 153 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 154 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 155 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= 156 | github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= 157 | github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= 158 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 159 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 160 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 161 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 162 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 163 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 164 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 165 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 166 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 167 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 168 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 169 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 170 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 171 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 172 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 173 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 174 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 175 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 176 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 177 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 178 | github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= 179 | github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= 180 | github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= 181 | github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 182 | github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 183 | github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 184 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 185 | github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= 186 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 187 | github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 188 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 189 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 190 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 191 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 192 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= 193 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 194 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 195 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 196 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= 197 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 198 | github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 199 | github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 200 | github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= 201 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 202 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 203 | github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= 204 | github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 205 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 206 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 207 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 208 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 209 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 210 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 211 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 212 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 213 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 214 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 215 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 216 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 217 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 218 | github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg= 219 | github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= 220 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 221 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 222 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 223 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 224 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 225 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 226 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 227 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 228 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 229 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 230 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 231 | github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y= 232 | github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= 233 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 234 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 235 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 236 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 237 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 238 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 239 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 240 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 241 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 242 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 243 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 244 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 245 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= 246 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 247 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 248 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 249 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 250 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 251 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 252 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 253 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 254 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 255 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 256 | github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 257 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 258 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 259 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 260 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 261 | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 262 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 263 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 264 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 265 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 266 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 267 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 268 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 269 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 270 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 271 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 272 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 273 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 274 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= 275 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 276 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 277 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 278 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 279 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 280 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 281 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 282 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 283 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 284 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 285 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 286 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 287 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 288 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 289 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 290 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 291 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 292 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 293 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 294 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 295 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 296 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 297 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 298 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 299 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 300 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 301 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 302 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 303 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 304 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 305 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 306 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 307 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 308 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 309 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 310 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 311 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 312 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 313 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 314 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 315 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 316 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 317 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 318 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 319 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 320 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 321 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 322 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 323 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 325 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 326 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 327 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 328 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 329 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 330 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 331 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 332 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 333 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 334 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 335 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= 348 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 350 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 351 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 352 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 353 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 354 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 355 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 356 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 357 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 358 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 359 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 360 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 361 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 362 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 363 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 364 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 365 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 366 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 367 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 368 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 369 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 370 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 371 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 372 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 373 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 374 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 375 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 376 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 377 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 378 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 379 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 380 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 381 | google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= 382 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 383 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 384 | google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 385 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 386 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 387 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 388 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 389 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 390 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 391 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 392 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 393 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 394 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 395 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 396 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 397 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 398 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 399 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 400 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 401 | gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 402 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 403 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 404 | gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= 405 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 406 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 407 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 408 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 409 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 410 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 411 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 412 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 413 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 414 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 415 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 416 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 417 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 418 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 419 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 420 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 421 | -------------------------------------------------------------------------------- /prom.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | clientsCount = prometheus.NewGauge(prometheus.GaugeOpts{ 7 | Name: "redplex_clients", 8 | Help: "Number of subscribers currently connected through Redplex", 9 | }) 10 | serverReconnects = prometheus.NewGauge(prometheus.GaugeOpts{ 11 | Name: "redplex_reconnects", 12 | Help: "Number of times this server has reconnected to the remote Redis instance", 13 | }) 14 | throughputMessages = prometheus.NewGauge(prometheus.GaugeOpts{ 15 | Name: "redplex_messages_count", 16 | Help: "Number messages this instance has sent to subscribers.", 17 | }) 18 | throughputBytes = prometheus.NewGauge(prometheus.GaugeOpts{ 19 | Name: "redplex_bytes_count", 20 | Help: "Number bytes this instance has sent to subscribers.", 21 | }) 22 | ) 23 | 24 | func init() { 25 | prometheus.MustRegister(clientsCount) 26 | prometheus.MustRegister(serverReconnects) 27 | prometheus.MustRegister(throughputMessages) 28 | prometheus.MustRegister(throughputBytes) 29 | } 30 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | // MessageError is the prefix for Redis line errors in the protocol. 14 | MessageError = '-' 15 | // MessageStatus is the prefix for Redis line statues in the protocol. 16 | MessageStatus = '+' 17 | // MessageInt is the prefix for Redis line integers in the protocol. 18 | // It's followed by the plain text number 19 | MessageInt = ':' 20 | // MessageBulk is the prefix for Redis bulk messages. It's followed by the 21 | // bulk message size, and CRLF, and then the full bulk message bytes. 22 | MessageBulk = '$' 23 | // MessageMutli is the prefix for Redis "multi" messages (arrays). 24 | // It's followed by the array length, and CRLF, and then the next N messages 25 | // as elements of the array/ 26 | MessageMutli = '*' 27 | ) 28 | 29 | var ( 30 | // messageDelimiter is the CRLF separator between Redis messages. 31 | messageDelimiter = []byte("\r\n") 32 | // messagePrefix is the prefix for pubsub messages on the Redis protocol. 33 | messagePrefix = []byte("*3\r\n$7\r\nmessage\r\n") 34 | // pmessagePrefix is the prefix for pattern pubsub messages on the protocol. 35 | pmessagePrefix = []byte("*4\r\n$8\r\npmessage\r\n") 36 | // ErrWrongMessage is returned in Parse commands if the command 37 | // is not a pubsub command. 38 | ErrWrongMessage = errors.New("redplex/protocol: unexpected message type") 39 | 40 | commandSubscribe = `subscribe` 41 | commandPSubscribe = `psubscribe` 42 | commandUnsubscribe = `unsubscribe` 43 | commandPUnsubscribe = `punsubscribe` 44 | commandQuit = `quit` 45 | ) 46 | 47 | // ReadNextFull copies the next full command from the reader to the buffer. 48 | func ReadNextFull(writeTo *bytes.Buffer, r *bufio.Reader) error { 49 | line, err := r.ReadSlice('\n') 50 | if err != nil { 51 | return err 52 | } 53 | writeTo.Write(line) 54 | line = line[:len(line)-2] 55 | switch line[0] { 56 | 57 | case MessageError: 58 | return nil 59 | case MessageStatus: 60 | return nil 61 | case MessageInt: 62 | return nil 63 | 64 | case MessageBulk: 65 | l, err := strconv.ParseInt(string(line[1:]), 10, 64) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if l < 0 { 71 | return nil 72 | } 73 | 74 | _, err = writeTo.ReadFrom(io.LimitReader(r, l+2)) 75 | return err 76 | 77 | case MessageMutli: 78 | l, err := strconv.Atoi(string(line[1:])) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | if l < 0 { 84 | return nil 85 | } 86 | for i := 0; i < l; i++ { 87 | if err := ReadNextFull(writeTo, r); err != nil { 88 | return err 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | return errors.New("redplex/protocol: received illegal data from redis") 95 | } 96 | 97 | // PublishCommand is returned from ParsePublishCommand. 98 | type PublishCommand struct { 99 | IsPattern bool 100 | ChannelOrPattern []byte 101 | } 102 | 103 | // ParseBulkMessage expects that the byte slice starts with the length 104 | // delimiter, and returns the contained message. Does not include the 105 | // trailing delimiter. 106 | func ParseBulkMessage(line []byte) ([]byte, error) { 107 | if line[0] != MessageBulk { 108 | return nil, ErrWrongMessage 109 | } 110 | 111 | delimiter := bytes.IndexByte(line, '\n') 112 | 113 | n, err := strconv.ParseInt(string(line[1:delimiter-1]), 10, 64) 114 | if err != nil { 115 | return nil, err 116 | } 117 | if n <= 0 { 118 | return nil, nil 119 | } 120 | 121 | if len(line) <= delimiter+1+int(n) { 122 | return nil, ErrWrongMessage 123 | } 124 | 125 | return line[delimiter+1 : delimiter+1+int(n)], nil 126 | } 127 | 128 | // ParsePublishCommand parses the given pubsub command efficiently. Returns a 129 | // NotPubsubError if the command isn't a pubsub command. 130 | func ParsePublishCommand(b []byte) (cmd PublishCommand, err error) { 131 | switch { 132 | case bytes.HasPrefix(b, messagePrefix): 133 | name, err := ParseBulkMessage(b[len(messagePrefix):]) 134 | if err != nil { 135 | return cmd, err 136 | } 137 | 138 | return PublishCommand{IsPattern: false, ChannelOrPattern: name}, nil 139 | case bytes.HasPrefix(b, pmessagePrefix): 140 | name, err := ParseBulkMessage(b[len(pmessagePrefix):]) 141 | if err != nil { 142 | return cmd, err 143 | } 144 | return PublishCommand{IsPattern: true, ChannelOrPattern: name}, nil 145 | default: 146 | return cmd, ErrWrongMessage 147 | } 148 | } 149 | 150 | // Request is a byte slice with utility methods for building up Redis commands. 151 | type Request []byte 152 | 153 | // NewRequest creates a new request to send to the Redis server. 154 | func NewRequest(name string, argCount int) *Request { 155 | b := []byte{MessageMutli} 156 | b = append(b, []byte(strconv.Itoa(argCount+1))...) 157 | b = append(b, messageDelimiter...) 158 | r := Request(b) 159 | return (&r).Bulk([]byte(name)) 160 | } 161 | 162 | // Bulk adds a new bulk argument value to the request. 163 | func (r *Request) Bulk(arg []byte) *Request { 164 | data := *r 165 | data = append(data, MessageBulk) 166 | data = append(data, []byte(strconv.Itoa(len(arg)))...) 167 | data = append(data, messageDelimiter...) 168 | data = append(data, arg...) 169 | data = append(data, messageDelimiter...) 170 | 171 | *r = data 172 | return r 173 | } 174 | 175 | // Int adds a new integer argument value to the request. 176 | func (r *Request) Int(n int) *Request { 177 | data := *r 178 | data = append(data, MessageInt) 179 | data = append(data, []byte(strconv.Itoa(n))...) 180 | data = append(data, messageDelimiter...) 181 | 182 | *r = data 183 | return r 184 | } 185 | 186 | // Bytes returns the request bytes. 187 | func (r *Request) Bytes() []byte { return *r } 188 | 189 | // ParseRequest parses a method and arguments from the reader. 190 | func ParseRequest(r *bufio.Reader) (method string, args [][]byte, err error) { 191 | line, err := r.ReadSlice('\n') 192 | if err != nil { 193 | return "", nil, err 194 | } 195 | 196 | n, err := strconv.Atoi(string(line[1 : len(line)-2])) 197 | if err != nil { 198 | return "", nil, err 199 | } 200 | 201 | if n < 0 { 202 | return "", nil, nil 203 | } 204 | 205 | buffer := bytes.NewBuffer(nil) 206 | for i := 0; i < n; i++ { 207 | if err := ReadNextFull(buffer, r); err != nil { 208 | return "", nil, err 209 | } 210 | 211 | msg, err := ParseBulkMessage(buffer.Bytes()) 212 | if err != nil { 213 | return "", nil, err 214 | } 215 | 216 | if method == "" { 217 | method = strings.ToLower(string(msg)) 218 | } else { 219 | args = append(args, copyBytes(msg)) 220 | } 221 | 222 | buffer.Reset() 223 | } 224 | 225 | return method, args, nil 226 | } 227 | 228 | func copyBytes(b []byte) (dup []byte) { 229 | dup = make([]byte, len(b)) 230 | copy(dup, b) 231 | return dup 232 | } 233 | 234 | // SubscribeResponse returns an appropriate response to the given subscribe 235 | // or unsubscribe command. 236 | func SubscribeResponse(command string, channel []byte) []byte { 237 | return NewRequest(command, 2).Bulk(channel).Int(1).Bytes() 238 | } 239 | -------------------------------------------------------------------------------- /protocol_test.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestReadNextFull(t *testing.T) { 13 | tt := [][]string{ 14 | {"$7", "he\r\nllo"}, 15 | {"$0", ""}, 16 | {"$-1"}, 17 | {":1"}, 18 | {"+OK"}, 19 | {"-hello world!"}, 20 | {"*0"}, 21 | {"*-1"}, 22 | {"*2", "$5", "hello", ":2"}, 23 | } 24 | 25 | for _, tcase := range tt { 26 | output := bytes.NewBuffer(nil) 27 | expected := []byte(strings.Join(tcase, "\r\n") + "\r\n") 28 | reader := bytes.NewReader(expected) 29 | bufferedReader := bufio.NewReader(reader) 30 | 31 | require.Nil(t, ReadNextFull(output, bufferedReader), "unexpected error in %+v", tcase) 32 | require.Equal(t, output.Bytes(), expected, "expected parsed %+v to be equal", tcase) 33 | require.Zero(t, reader.Len()+bufferedReader.Buffered(), "should have consumed all of %+v", tcase) 34 | } 35 | } 36 | 37 | func TestParseBulkMessage(t *testing.T) { 38 | tt := []struct { 39 | Message string 40 | Expected string 41 | Error error 42 | }{ 43 | {"$-1\r\n", "", nil}, 44 | {"$0\r\n", "", nil}, 45 | {"$5\r\nhello\r\n", "hello", nil}, 46 | {"$5\r\nhe", "", ErrWrongMessage}, 47 | {":1\r\nasdf\r\n", "", ErrWrongMessage}, 48 | } 49 | 50 | for _, tcase := range tt { 51 | actual, err := ParseBulkMessage([]byte(tcase.Message)) 52 | 53 | if tcase.Error != nil { 54 | require.Equal(t, tcase.Error, err, "unexpected error parsing %s", tcase.Message) 55 | } else if len(tcase.Expected) == 0 { 56 | require.Nil(t, actual, "expected empty byte slice parsing %s", tcase.Message) 57 | } else { 58 | require.Equal(t, []byte(tcase.Expected), actual, "unexpected result parsing %s", tcase.Message) 59 | } 60 | } 61 | } 62 | 63 | func TestParsePublishCommand(t *testing.T) { 64 | tt := []struct { 65 | Message string 66 | Expected PublishCommand 67 | Error error 68 | }{ 69 | {"$-1\r\n", PublishCommand{}, ErrWrongMessage}, 70 | {"*3\r\n$7\r\nmessage\r\n$6\r\nsecond\r\n$5\r\nHello\r\n", PublishCommand{false, []byte("second")}, nil}, 71 | {"*4\r\n$8\r\npmessage\r\n$6\r\nsecond\r\n$2\r\ns*\r\n$5\r\nHello\r\n", PublishCommand{true, []byte("second")}, nil}, 72 | } 73 | 74 | for _, tcase := range tt { 75 | actual, err := ParsePublishCommand([]byte(tcase.Message)) 76 | 77 | if err != nil { 78 | require.Equal(t, tcase.Error, err, "unexpected error parsing %s", tcase.Message) 79 | } else { 80 | require.Equal(t, tcase.Expected, actual, "unexpected result parsing %s", tcase.Message) 81 | } 82 | } 83 | } 84 | 85 | func TestNewRequest(t *testing.T) { 86 | require.Equal(t, 87 | NewRequest("PING", 0).Bytes(), 88 | []byte("*1\r\n$4\r\nPING\r\n"), 89 | ) 90 | require.Equal(t, 91 | NewRequest("SUBSCRIBE", 1).Bulk([]byte("channel-name")).Bytes(), 92 | []byte("*2\r\n$9\r\nSUBSCRIBE\r\n$12\r\nchannel-name\r\n"), 93 | ) 94 | } 95 | 96 | func TestParseRequest(t *testing.T) { 97 | tt := [][]string{ 98 | {"ping"}, 99 | {"subscribe", "foo"}, 100 | {"subscribe", "foo", "bar"}, 101 | } 102 | 103 | for _, tcase := range tt { 104 | var args [][]byte 105 | cmd := NewRequest(tcase[0], len(tcase)-1) 106 | for _, arg := range tcase[1:] { 107 | args = append(args, []byte(arg)) 108 | cmd.Bulk([]byte(arg)) 109 | } 110 | 111 | method, actualArgs, err := ParseRequest(bufio.NewReader(bytes.NewReader(cmd.Bytes()))) 112 | require.Nil(t, err, "unexpected error parsing %+v", tcase) 113 | require.Equal(t, tcase[0], method) 114 | require.Equal(t, args, actualArgs) 115 | } 116 | } 117 | 118 | func TestSubscribeResponse(t *testing.T) { 119 | require.Equal(t, 120 | SubscribeResponse("subscribe", []byte("first")), 121 | []byte("*3\r\n$9\r\nsubscribe\r\n$5\r\nfirst\r\n:1\r\n"), 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /pubsub.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/cenkalti/backoff" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Writable is an interface passed into Pubsub. It's called when we want to 15 | // publish data. 16 | type Writable interface { 17 | Write(b []byte) 18 | } 19 | 20 | // The Listener wraps a function that's called when a pubsub message it sent. 21 | type Listener struct { 22 | IsPattern bool 23 | Channel string 24 | Conn Writable 25 | } 26 | 27 | // listenerMap is a map of patterns or channels to Listeners. 28 | type listenerMap map[string][]Writable 29 | 30 | // broadcast pushes the byte slice asynchronously to the list of listeners. 31 | // Blocks until all listeners have been called. 32 | func (l listenerMap) broadcast(pattern []byte, b []byte) { 33 | listeners := l[string(pattern)] 34 | 35 | count := float64(len(listeners)) 36 | throughputMessages.Add(count) 37 | throughputBytes.Add(count * float64(len(b))) 38 | 39 | var wg sync.WaitGroup 40 | wg.Add(len(listeners)) 41 | for _, l := range listeners { 42 | go func(l Writable) { l.Write(b); wg.Done() }(l) 43 | } 44 | wg.Wait() 45 | } 46 | 47 | // add inserts the listener into the pattern's set of listeners. 48 | func (l listenerMap) add(channel string, listener Writable) (shouldSubscribe bool) { 49 | list := l[channel] 50 | shouldSubscribe = len(list) == 0 51 | l[channel] = append(list, listener) 52 | return shouldSubscribe 53 | } 54 | 55 | // remove pulls the listener out of the map. 56 | func (l listenerMap) remove(channel string, listener Writable) (shouldUnsubscribe bool) { 57 | list := l[channel] 58 | changed := false 59 | for i, other := range list { 60 | if other == listener { 61 | changed = true 62 | list[i] = list[len(list)-1] 63 | list[len(list)-1] = nil 64 | list = list[:len(list)-1] 65 | break 66 | } 67 | } 68 | 69 | if !changed { 70 | return false 71 | } 72 | 73 | if len(list) == 0 { 74 | delete(l, channel) 75 | return true 76 | } 77 | 78 | l[channel] = list 79 | return false 80 | } 81 | 82 | // removeAll removes all channels the listener is connected to. 83 | func (l listenerMap) removeAll(conn Writable) (toUnsub [][]byte) { 84 | for channel, list := range l { 85 | for i := 0; i < len(list); i++ { 86 | if list[i] == conn { 87 | list[i] = list[len(list)-1] 88 | list[len(list)-1] = nil 89 | list = list[:len(list)-1] 90 | i-- 91 | continue 92 | } 93 | } 94 | 95 | if len(list) == 0 { 96 | delete(l, channel) 97 | toUnsub = append(toUnsub, []byte(channel)) 98 | } else { 99 | l[channel] = list 100 | } 101 | } 102 | 103 | return toUnsub 104 | } 105 | 106 | // Pubsub manages the connection of redplex to the remote pubsub server. 107 | type Pubsub struct { 108 | dialer Dialer 109 | closer chan struct{} 110 | writeTimeout time.Duration 111 | 112 | mu sync.Mutex 113 | connection net.Conn 114 | patterns listenerMap 115 | channels listenerMap 116 | } 117 | 118 | // NewPubsub creates a new Pubsub instance. 119 | func NewPubsub(dialer Dialer, writeTimeout time.Duration) *Pubsub { 120 | return &Pubsub{ 121 | dialer: dialer, 122 | writeTimeout: writeTimeout, 123 | patterns: listenerMap{}, 124 | channels: listenerMap{}, 125 | closer: make(chan struct{}), 126 | } 127 | } 128 | 129 | // Start creates a pubsub listener to proxy connection data. 130 | func (p *Pubsub) Start() { 131 | backoff := backoff.NewExponentialBackOff() 132 | backoff.MaxInterval = time.Second * 10 133 | 134 | for { 135 | cnx, err := p.dialer.Dial() 136 | if err != nil { 137 | logrus.WithError(err).Info("redplex/pubsub: error dialing to pubsub master") 138 | select { 139 | case <-time.After(backoff.NextBackOff()): 140 | serverReconnects.Inc() 141 | continue 142 | case <-p.closer: 143 | return 144 | } 145 | } 146 | 147 | backoff.Reset() 148 | err = p.read(cnx) 149 | 150 | select { 151 | case <-p.closer: 152 | return 153 | default: 154 | logrus.WithError(err).Info("redplex/pubsub: lost connection to pubsub server") 155 | } 156 | } 157 | } 158 | 159 | // Close frees resources associated with the pubsub server. 160 | func (p *Pubsub) Close() { 161 | close(p.closer) 162 | p.mu.Lock() 163 | if p.connection != nil { 164 | p.connection.Close() 165 | } 166 | p.mu.Unlock() 167 | } 168 | 169 | // Subscribe adds the listener to the channel. 170 | func (p *Pubsub) Subscribe(listener Listener) { 171 | p.mu.Lock() 172 | if listener.IsPattern { 173 | if p.patterns.add(listener.Channel, listener.Conn) { 174 | p.command(NewRequest(commandPSubscribe, 1).Bulk([]byte(listener.Channel))) 175 | } 176 | } else { 177 | if p.channels.add(listener.Channel, listener.Conn) { 178 | p.command(NewRequest(commandSubscribe, 1).Bulk([]byte(listener.Channel))) 179 | } 180 | } 181 | p.mu.Unlock() 182 | } 183 | 184 | // Unsubscribe removes the listener from the channel. 185 | func (p *Pubsub) Unsubscribe(listener Listener) { 186 | p.mu.Lock() 187 | if listener.IsPattern { 188 | if p.patterns.remove(listener.Channel, listener.Conn) { 189 | p.command(NewRequest(commandPUnsubscribe, 1).Bulk([]byte(listener.Channel))) 190 | } 191 | } else { 192 | if p.channels.remove(listener.Channel, listener.Conn) { 193 | p.command(NewRequest(commandUnsubscribe, 1).Bulk([]byte(listener.Channel))) 194 | } 195 | } 196 | p.mu.Unlock() 197 | } 198 | 199 | // UnsubscribeAll removes all channels the writer is subscribed to. 200 | func (p *Pubsub) UnsubscribeAll(c Writable) { 201 | p.mu.Lock() 202 | 203 | var ( 204 | toUnsub = p.patterns.removeAll(c) 205 | command []byte 206 | ) 207 | 208 | if len(toUnsub) > 0 { 209 | r := NewRequest(commandPUnsubscribe, len(toUnsub)) 210 | for _, p := range toUnsub { 211 | r.Bulk(p) 212 | } 213 | 214 | command = append(command, r.Bytes()...) 215 | } 216 | 217 | toUnsub = p.channels.removeAll(c) 218 | if len(toUnsub) > 0 { 219 | r := NewRequest(commandUnsubscribe, len(toUnsub)) 220 | for _, p := range toUnsub { 221 | r.Bulk(p) 222 | } 223 | 224 | command = append(command, r.Bytes()...) 225 | } 226 | 227 | if p.connection != nil && len(command) > 0 { 228 | p.connection.SetWriteDeadline(time.Now().Add(p.writeTimeout)) 229 | go p.connection.Write(command) 230 | } 231 | 232 | p.mu.Unlock() 233 | } 234 | 235 | // command sends the request to the pubsub server asynchronously. 236 | func (p *Pubsub) command(r *Request) { 237 | if p.connection != nil { 238 | p.connection.SetWriteDeadline(time.Now().Add(p.writeTimeout)) 239 | go p.connection.Write(r.Bytes()) 240 | } 241 | } 242 | 243 | // command sends the request to the pubsub server and blocks until it sends. 244 | func (p *Pubsub) commandSync(r *Request) { 245 | if p.connection != nil { 246 | p.connection.SetWriteDeadline(time.Now().Add(p.writeTimeout)) 247 | p.connection.Write(r.Bytes()) 248 | } 249 | } 250 | 251 | func (p *Pubsub) resubscribe(cnx net.Conn) { 252 | p.mu.Lock() 253 | p.connection = cnx 254 | 255 | if len(p.channels) > 0 { 256 | cmd := NewRequest(commandSubscribe, len(p.channels)) 257 | for channel := range p.channels { 258 | cmd.Bulk([]byte(channel)) 259 | } 260 | p.commandSync(cmd) 261 | } 262 | 263 | if len(p.patterns) > 0 { 264 | cmd := NewRequest(commandPSubscribe, len(p.patterns)) 265 | for pattern := range p.patterns { 266 | cmd.Bulk([]byte(pattern)) 267 | } 268 | p.commandSync(cmd) 269 | } 270 | 271 | p.mu.Unlock() 272 | } 273 | 274 | // read grabs commands from the connection, reading them until the 275 | // connection terminates. 276 | func (p *Pubsub) read(cnx net.Conn) error { 277 | var ( 278 | reader = bufio.NewReader(cnx) 279 | buffer = bytes.NewBuffer(nil) 280 | ) 281 | 282 | p.resubscribe(cnx) 283 | 284 | // The only thing that 285 | for { 286 | buffer.Reset() 287 | if err := ReadNextFull(buffer, reader); err != nil { 288 | p.mu.Lock() 289 | p.connection = nil 290 | p.mu.Unlock() 291 | return err 292 | } 293 | 294 | bytes := copyBytes(buffer.Bytes()) 295 | parsed, err := ParsePublishCommand(bytes) 296 | if err != nil { 297 | continue // expected, we can get replies from subscriptions, which we'll ignore 298 | } 299 | 300 | p.mu.Lock() 301 | if parsed.IsPattern { 302 | p.patterns.broadcast(parsed.ChannelOrPattern, bytes) 303 | } else { 304 | p.channels.broadcast(parsed.ChannelOrPattern, bytes) 305 | } 306 | p.mu.Unlock() 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /pubsub_test.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/mock" 11 | "github.com/stretchr/testify/require" 12 | "github.com/stretchr/testify/suite" 13 | ) 14 | 15 | type mockWritable struct{ mock.Mock } 16 | 17 | func (m *mockWritable) Write(b []byte) { m.Called(b) } 18 | 19 | func makeWritableMocks() []*mockWritable { 20 | var mocks []*mockWritable 21 | for i := 0; i < 10; i++ { 22 | mocks = append(mocks, &mockWritable{}) 23 | } 24 | 25 | return mocks 26 | } 27 | 28 | func assertMocks(t *testing.T, mocks []*mockWritable) { 29 | for _, mock := range mocks { 30 | mock.AssertExpectations(t) 31 | } 32 | } 33 | 34 | func TestListenersMap(t *testing.T) { 35 | mocks := makeWritableMocks() 36 | l := listenerMap{} 37 | 38 | t.Run("AddsSubscribers", func(t *testing.T) { 39 | require.True(t, l.add("foo", mocks[0])) 40 | require.False(t, l.add("foo", mocks[1])) 41 | require.True(t, l.add("bar", mocks[2])) 42 | }) 43 | 44 | t.Run("Broadcasts", func(t *testing.T) { 45 | mocks[0].On("Write", []byte{1, 2, 3}).Return() 46 | mocks[1].On("Write", []byte{1, 2, 3}).Return() 47 | l.broadcast([]byte("foo"), []byte{1, 2, 3}) 48 | assertMocks(t, mocks) 49 | }) 50 | 51 | t.Run("Unsubscribes", func(t *testing.T) { 52 | require.Equal(t, 2, len(l)) 53 | require.True(t, l.remove("bar", mocks[2])) 54 | require.Equal(t, 1, len(l)) 55 | require.False(t, l.remove("foo", mocks[0])) 56 | require.True(t, l.remove("foo", mocks[1])) 57 | require.Equal(t, 0, len(l)) 58 | }) 59 | 60 | t.Run("RemoveAll", func(t *testing.T) { 61 | require.True(t, l.add("foo", mocks[0])) 62 | require.False(t, l.add("foo", mocks[1])) 63 | require.True(t, l.add("bar", mocks[0])) 64 | 65 | require.Equal(t, 66 | [][]byte{[]byte("bar")}, 67 | l.removeAll(mocks[0]), 68 | ) 69 | 70 | require.Equal(t, 1, len(l)) 71 | }) 72 | } 73 | 74 | type PubsubSuite struct { 75 | suite.Suite 76 | server net.Listener 77 | connections <-chan net.Conn 78 | pubsubWg sync.WaitGroup 79 | pubsub *Pubsub 80 | } 81 | 82 | func TestPubsubSuite(t *testing.T) { 83 | suite.Run(t, new(PubsubSuite)) 84 | } 85 | 86 | func (p *PubsubSuite) SetupSuite() { 87 | server, err := net.Listen("tcp", "127.0.0.1:0") 88 | require.Nil(p.T(), err) 89 | connections := make(chan net.Conn) 90 | 91 | p.connections = connections 92 | p.server = server 93 | 94 | go func() { 95 | for { 96 | cnx, err := server.Accept() 97 | if err != nil { 98 | return 99 | } 100 | 101 | connections <- cnx 102 | } 103 | }() 104 | } 105 | 106 | func (p *PubsubSuite) SetupTest() { 107 | p.pubsub = NewPubsub( 108 | NewDirectDialer("tcp", p.server.Addr().String(), "", false, 0), 109 | time.Second, 110 | ) 111 | 112 | p.pubsubWg.Add(1) 113 | go func() { p.pubsub.Start(); p.pubsubWg.Done() }() 114 | } 115 | 116 | func (p *PubsubSuite) TearDownTest() { 117 | p.pubsub.Close() 118 | p.pubsubWg.Wait() 119 | } 120 | 121 | func (p *PubsubSuite) setupSubscription() (cnx net.Conn, mw *mockWritable) { 122 | cnx = <-p.connections 123 | mw = &mockWritable{} 124 | p.pubsub.Subscribe(Listener{IsPattern: false, Channel: "foo", Conn: mw}) 125 | assertReads(p.T(), cnx, "*2\r\n$9\r\nsubscribe\r\n$3\r\nfoo\r\n") 126 | p.pubsub.Subscribe(Listener{IsPattern: true, Channel: "ba*", Conn: mw}) 127 | assertReads(p.T(), cnx, "*2\r\n$10\r\npsubscribe\r\n$3\r\nba*\r\n") 128 | return cnx, mw 129 | } 130 | 131 | func (p *PubsubSuite) TestMakeSimpleSubscription() { 132 | cnx, mw := p.setupSubscription() 133 | p.assertReceivesMessage(cnx, mw, "*3\r\n$7\r\nmessage\r\n$3\r\nfoo\r\n$5\r\nheyo!\r\n") 134 | p.assertReceivesMessage(cnx, mw, "*4\r\n$8\r\npmessage\r\n$3\r\nba*\r\n$3\r\nbar\r\n$5\r\nheyo!\r\n") 135 | mw.AssertExpectations(p.T()) 136 | } 137 | 138 | func (p *PubsubSuite) TestUnsubscribesAll() { 139 | cnx, mw := p.setupSubscription() 140 | p.pubsub.UnsubscribeAll(mw) 141 | assertReads(p.T(), cnx, "*2\r\n$12\r\npunsubscribe\r\n$3\r\nba*\r\n") 142 | assertReads(p.T(), cnx, "*2\r\n$11\r\nunsubscribe\r\n$3\r\nfoo\r\n") 143 | } 144 | 145 | func (p *PubsubSuite) TestUnsubscribesChannel() { 146 | cnx, mw := p.setupSubscription() 147 | p.pubsub.Unsubscribe(Listener{IsPattern: false, Channel: "foo", Conn: mw}) 148 | assertReads(p.T(), cnx, "*2\r\n$11\r\nunsubscribe\r\n$3\r\nfoo\r\n") 149 | p.pubsub.Unsubscribe(Listener{IsPattern: true, Channel: "ba*", Conn: mw}) 150 | assertReads(p.T(), cnx, "*2\r\n$12\r\npunsubscribe\r\n$3\r\nba*\r\n") 151 | } 152 | 153 | func (p *PubsubSuite) TestResubscribesOnFailure() { 154 | cnx, mw := p.setupSubscription() 155 | cnx.Close() 156 | 157 | newCnx := <-p.connections 158 | assertReads(p.T(), newCnx, "*2\r\n$9\r\nsubscribe\r\n$3\r\nfoo\r\n") 159 | assertReads(p.T(), newCnx, "*2\r\n$10\r\npsubscribe\r\n$3\r\nba*\r\n") 160 | 161 | p.assertReceivesMessage(newCnx, mw, "*3\r\n$7\r\nmessage\r\n$3\r\nfoo\r\n$5\r\nheyo!\r\n") 162 | } 163 | 164 | func (p *PubsubSuite) TestIgnoresExtraneousMessages() { 165 | cnx, mw := p.setupSubscription() 166 | cnx.Write([]byte("+OK\r\n")) 167 | p.assertReceivesMessage(cnx, mw, "*3\r\n$7\r\nmessage\r\n$3\r\nfoo\r\n$5\r\nheyo!\r\n") 168 | } 169 | 170 | func (p *PubsubSuite) assertReceivesMessage(cnx net.Conn, mw *mockWritable, message string) { 171 | ok := make(chan struct{}) 172 | mw.On("Write", []byte(message)).Run(func(_ mock.Arguments) { ok <- struct{}{} }).Return() 173 | cnx.Write([]byte(message)) 174 | select { 175 | case <-ok: 176 | case <-time.After(time.Second): 177 | p.Fail("Expected to read sub message, but didn't") 178 | } 179 | mw.AssertExpectations(p.T()) 180 | } 181 | 182 | func assertReads(t *testing.T, cnx net.Conn, message string) { 183 | cnx.SetReadDeadline(time.Now().Add(time.Second * 5)) 184 | actual := make([]byte, len(message)) 185 | _, err := io.ReadFull(cnx, actual) 186 | require.Nil(t, err, "error reading expected message %q", message) 187 | require.Equal(t, message, string(actual)) 188 | } 189 | 190 | func (p *PubsubSuite) TearDownSuite() { 191 | p.server.Close() 192 | } 193 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # redplex 2 | 3 | Project status: redplex is actively maintained and was running in production at [Mixer](https://en.wikipedia.org/wiki/Mixer_(service)) from 2017 to 2021. 4 | 5 | redplex is a tool to multiplex Redis pubsub. It implements the Redis protocol and is a drop-in replacement for existing Redis pubsub servers, simply boot redplex and change your port number. This is a useful tool in situations where you have very many readers for pubsub events, as Redis pubsub throughput is inversely proportional to the number of subscribers for the event. 6 | 7 | > Note: some Redis clients have health checks that call commands like INFO on boot. You'll want to turn these off, as redplex does not implement commands expect for SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, and EXIT. 8 | 9 | ### Usage 10 | 11 | ``` 12 | ➜ redplex git:(master) ./redplex --help 13 | usage: redplex [] 14 | 15 | Flags: 16 | --help Show context-sensitive help (also try --help-long and --help-man). 17 | -l, --listen="127.0.0.1:3000" Address to listen on 18 | -n, --network="tcp" Network to listen on 19 | --remote="127.0.0.1:6379" Remote address of the Redis server 20 | --remote-network="tcp" Remote network to dial through (usually tcp or tcp6) 21 | --sentinels=SENTINELS ... A list of Redis sentinel addresses 22 | --sentinel-name=SENTINEL-NAME 23 | The name of the sentinel master 24 | --log-level="info" Log level (one of debug, info, warn, error 25 | --dial-timeout=10s Timeout connecting to Redis 26 | --write-timeout=2s Timeout during write operations 27 | --pprof-server=PPROF-SERVER 28 | Address to bind a pprof server on. Not bound if empty. 29 | ``` 30 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "sync" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // toSendQueueLimit is the number of pubsub messages we can 13 | // buffer on the outgoing connection. 14 | const toSendQueueLimit = 128 15 | 16 | // Server is the redplex server which accepts connections and talks to the 17 | // underlying Pubsub implementation. 18 | type Server struct { 19 | l net.Listener 20 | pubsub *Pubsub 21 | } 22 | 23 | // NewServer creates a new Redplex server. It listens for connections from 24 | // the listener and uses the Dialer to proxy to the remote server. 25 | func NewServer(l net.Listener, pubsub *Pubsub) *Server { 26 | return &Server{ 27 | l: l, 28 | pubsub: pubsub, 29 | } 30 | } 31 | 32 | // Listen accepts and serves incoming connections. 33 | func (s *Server) Listen() error { 34 | go s.pubsub.Start() 35 | 36 | for { 37 | cnx, err := s.l.Accept() 38 | if err != nil { 39 | return err 40 | } 41 | go (&connection{ 42 | cnx: cnx, 43 | pubsub: s.pubsub, 44 | toSendCond: *sync.NewCond(&sync.Mutex{}), 45 | toSend: make([][]byte, 0, toSendQueueLimit), 46 | }).Start() 47 | } 48 | } 49 | 50 | // Close frees resources associated with the server. 51 | func (s *Server) Close() { 52 | s.l.Close() 53 | s.pubsub.Close() 54 | } 55 | 56 | type connection struct { 57 | cnx net.Conn 58 | pubsub *Pubsub 59 | listeners []*Listener 60 | 61 | toSendCond sync.Cond 62 | isClosed bool 63 | toSend [][]byte 64 | } 65 | 66 | // Start reads data from the connection until we're no longer able to. 67 | func (s *connection) Start() { 68 | reader := bufio.NewReader(s.cnx) 69 | defer func() { 70 | s.toSendCond.L.Lock() 71 | s.isClosed = true 72 | s.toSendCond.Broadcast() 73 | s.toSendCond.L.Unlock() 74 | s.pubsub.UnsubscribeAll(s) 75 | clientsCount.Dec() 76 | }() 77 | 78 | clientsCount.Inc() 79 | logrus.Debug("redplex/server: accepted connection") 80 | go s.loopWrite() 81 | 82 | for { 83 | method, args, err := ParseRequest(reader) 84 | if err != nil { 85 | logrus.WithError(err).Debug("redplex/server: error reading command, terminating client connection") 86 | return 87 | } 88 | 89 | switch method { 90 | case commandSubscribe: 91 | for _, channel := range args { 92 | s.pubsub.Subscribe(Listener{false, string(channel), s}) 93 | } 94 | case commandPSubscribe: 95 | for _, channel := range args { 96 | s.pubsub.Subscribe(Listener{true, string(channel), s}) 97 | } 98 | case commandUnsubscribe: 99 | for _, channel := range args { 100 | s.pubsub.Unsubscribe(Listener{false, string(channel), s}) 101 | } 102 | case commandPUnsubscribe: 103 | for _, channel := range args { 104 | s.pubsub.Unsubscribe(Listener{true, string(channel), s}) 105 | } 106 | case commandQuit: 107 | logrus.Debug("redplex/server: terminating connection at client's request") 108 | return 109 | default: 110 | s.cnx.Write([]byte(fmt.Sprintf("-ERR unknown command '%s'\r\n", method))) 111 | continue 112 | } 113 | 114 | for _, channel := range args { 115 | s.cnx.Write(SubscribeResponse(method, channel)) 116 | } 117 | } 118 | } 119 | 120 | func (s *connection) loopWrite() { 121 | buffers := net.Buffers{} 122 | for { 123 | s.toSendCond.L.Lock() 124 | for len(s.toSend) == 0 && !s.isClosed { 125 | s.toSendCond.Wait() 126 | } 127 | if s.isClosed { 128 | s.toSendCond.L.Unlock() 129 | return 130 | } 131 | 132 | buffers = append(buffers, s.toSend...) 133 | s.toSend = s.toSend[:0] 134 | s.toSendCond.L.Unlock() 135 | buffers.WriteTo(s.cnx) 136 | buffers = buffers[:0] 137 | } 138 | } 139 | 140 | // Write implements Writable.Write. 141 | func (s *connection) Write(b []byte) { 142 | s.toSendCond.L.Lock() 143 | if len(s.toSend) < cap(s.toSend) && !s.isClosed { 144 | s.toSend = append(s.toSend, b) 145 | s.toSendCond.Broadcast() 146 | } 147 | s.toSendCond.L.Unlock() 148 | } 149 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package redplex 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/garyburd/redigo/redis" 9 | "github.com/stretchr/testify/require" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | const redisAddress = "127.0.0.1:6379" 14 | 15 | type EndToEndServerSuite struct { 16 | suite.Suite 17 | server *Server 18 | redplexConn redis.Conn 19 | directConn redis.Conn 20 | } 21 | 22 | func TestEndToEndServerSuite(t *testing.T) { 23 | suite.Run(t, new(EndToEndServerSuite)) 24 | } 25 | 26 | func (e *EndToEndServerSuite) SetupSuite() { 27 | listener, err := net.Listen("tcp", "127.0.0.1:0") 28 | require.Nil(e.T(), err) 29 | 30 | e.server = NewServer(listener, NewPubsub( 31 | NewDirectDialer("tcp", redisAddress, "", false, 0), 32 | time.Second*5, 33 | )) 34 | go e.server.Listen() 35 | 36 | directConn, err := redis.Dial("tcp", redisAddress) 37 | require.Nil(e.T(), err) 38 | e.directConn = directConn 39 | 40 | redplexConn, err := redis.Dial("tcp", listener.Addr().String()) 41 | require.Nil(e.T(), err) 42 | e.redplexConn = redplexConn 43 | } 44 | 45 | func (e *EndToEndServerSuite) TearDownSuite() { 46 | e.server.Close() 47 | e.redplexConn.Close() 48 | e.directConn.Close() 49 | } 50 | 51 | func (e *EndToEndServerSuite) TestSubscribesAndGetsMessages() { 52 | psc := redis.PubSubConn{Conn: e.redplexConn} 53 | require.Nil(e.T(), psc.Subscribe("foo")) 54 | require.Equal(e.T(), redis.Subscription{Kind: "subscribe", Channel: "foo", Count: 1}, psc.Receive()) 55 | require.Nil(e.T(), psc.PSubscribe("ba*")) 56 | require.Equal(e.T(), redis.Subscription{Kind: "psubscribe", Channel: "ba*", Count: 1}, psc.Receive()) 57 | 58 | e.retryUntilReturns( 59 | func() { 60 | _, err := e.directConn.Do("PUBLISH", "foo", "bar") 61 | require.Nil(e.T(), err) 62 | }, 63 | func() { 64 | require.Equal(e.T(), redis.Message{Channel: "foo", Data: []byte("bar")}, psc.Receive()) 65 | }, 66 | ) 67 | 68 | e.retryUntilReturns( 69 | func() { 70 | _, err := e.directConn.Do("PUBLISH", "bar", "heyo!") 71 | require.Nil(e.T(), err) 72 | }, 73 | func() { 74 | require.Equal(e.T(), redis.PMessage{Pattern: "ba*", Channel: "bar", Data: []byte("heyo!")}, psc.Receive()) 75 | }, 76 | ) 77 | } 78 | 79 | func (e *EndToEndServerSuite) retryUntilReturns(retried func(), awaitedFn func()) { 80 | ok := make(chan struct{}) 81 | go func() { 82 | for { 83 | retried() 84 | select { 85 | case <-time.After(time.Millisecond * 500): 86 | case <-ok: 87 | return 88 | } 89 | } 90 | }() 91 | 92 | awaitedFn() 93 | ok <- struct{}{} 94 | } 95 | --------------------------------------------------------------------------------