├── .gitignore ├── .travis.yml ├── BENCHMARKS.md ├── CODEOWNERS ├── CONTRIBUTING-ARCHIVED.md ├── DISABLED_COMMANDS.md ├── LICENSE.TXT ├── Makefile ├── PRODUCTION_BENCHMARKS.md ├── README.md ├── client.go ├── client_test.go ├── connection ├── connection.go ├── connection_pool.go ├── connection_pool_test.go ├── connection_test.go ├── doc.go └── hash_ring.go ├── doc.go ├── doc └── config.md ├── example ├── config-mux.json ├── config.json └── redis.conf ├── func_client_test.go ├── graphite └── graphite.go ├── graphs ├── direct_upper.png ├── direct_upper90.png ├── rmux_upper.png └── rmux_upper90.png ├── integration_test.go ├── log ├── log.go ├── log_dev.go └── log_prod.go ├── main ├── config.go ├── config_test.go └── main.go ├── protocol ├── command.go ├── doc.go ├── errors.go ├── inline_command.go ├── inline_command_test.go ├── multibulk_command.go ├── multibulk_command_test.go ├── protocol.go ├── protocol_test.go ├── read_writer.go ├── scan.go ├── scan_test.go ├── scanner.go ├── simple_command.go ├── simple_command_test.go ├── string_command.go ├── string_command_test.go └── test_test.go ├── server.go ├── server_test.go └── writer ├── writer.go └── writer_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | ### Go template 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | 30 | 31 | .idea 32 | *.iml 33 | 34 | build 35 | 36 | .DS_Store 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4.2 5 | -------------------------------------------------------------------------------- /BENCHMARKS.md: -------------------------------------------------------------------------------- 1 | # Benchmarks # 2 | 3 | Benchmarks with keep-alive off show a unix-socket rmux connection being ~10x as fast as a direct tcp connection, under heavy load: 4 | ``` 5 | $ redis-benchmark -q -n 1000 -c 50 -r 50 -k 0 6 | WARNING: keepalive disabled, you probably need 'echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse' for Linux and 'sudo sysctl -w net.inet.tcp.msl=1000' for Mac OS X in order to use a lot of clients/requests 7 | PING_INLINE: 7633.59 requests per second 8 | PING_BULK: 5025.13 requests per second 9 | SET: 4032.26 requests per second 10 | GET: 2770.08 requests per second 11 | INCR: 2652.52 requests per second 12 | LPUSH: 2906.98 requests per second 13 | LPOP: 2409.64 requests per second 14 | SADD: 1381.22 requests per second 15 | SPOP: 1126.13 requests per second 16 | LPUSH (needed to benchmark LRANGE): 2645.50 requests per second 17 | LRANGE_100 (first 100 elements): 2808.99 requests per second 18 | LRANGE_300 (first 300 elements): 1510.57 requests per second 19 | LRANGE_500 (first 450 elements): 1515.15 requests per second 20 | LRANGE_600 (first 600 elements): 1483.68 requests per second 21 | MSET (10 keys): 2801.12 requests per second 22 | ``` 23 | versus 24 | ``` 25 | redis-benchmark -q -n 10000 -c 50 -r 50 -k 0 -s /tmp/rmux.sock 26 | WARNING: keepalive disabled, you probably need 'echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse' for Linux and 'sudo sysctl -w net.inet.tcp.msl=1000' for Mac OS X in order to use a lot of clients/requests 27 | PING_INLINE: 28169.02 requests per second 28 | PING_BULK: 23364.49 requests per second 29 | SET: 24875.62 requests per second 30 | GET: 25062.66 requests per second 31 | INCR: 23696.68 requests per second 32 | LPUSH: 26178.01 requests per second 33 | LPOP: 27247.96 requests per second 34 | SADD: 28328.61 requests per second 35 | SPOP: 25906.73 requests per second 36 | LPUSH (needed to benchmark LRANGE): 24813.90 requests per second 37 | LRANGE_100 (first 100 elements): 14970.06 requests per second 38 | LRANGE_300 (first 300 elements): 8857.40 requests per second 39 | LRANGE_500 (first 450 elements): 6570.30 requests per second 40 | LRANGE_600 (first 600 elements): 4990.02 requests per second 41 | MSET (10 keys): 26178.01 requests per second 42 | ``` 43 | ==== 44 | Benchmarks with keep-alive on show a unix-socket rmux connection being ~70% as fast as a direct tcp connection, under heavy load 45 | ``` 46 | $ redis-benchmark -q -n 1000 -c 50 -r 50 47 | PING_INLINE: 100000.00 requests per second 48 | PING_BULK: 111111.12 requests per second 49 | SET: 90909.09 requests per second 50 | GET: 111111.12 requests per second 51 | INCR: 100000.00 requests per second 52 | LPUSH: 90909.09 requests per second 53 | LPOP: 111111.12 requests per second 54 | SADD: 111111.12 requests per second 55 | SPOP: 90909.09 requests per second 56 | LPUSH (needed to benchmark LRANGE): 111111.12 requests per second 57 | LRANGE_100 (first 100 elements): 31250.00 requests per second 58 | LRANGE_300 (first 300 elements): 12987.01 requests per second 59 | LRANGE_500 (first 450 elements): 8928.57 requests per second 60 | LRANGE_600 (first 600 elements): 6849.31 requests per second 61 | MSET (10 keys): 58823.53 requests per second 62 | ``` 63 | versus 64 | ``` 65 | $ redis-benchmark -q -n 1000 -c 50 -r 50 -s /tmp/rmux.sock 66 | PING_INLINE: 156250.00 requests per second 67 | PING_BULK: 158730.16 requests per second 68 | SET: 68965.52 requests per second 69 | GET: 69930.07 requests per second 70 | INCR: 70422.53 requests per second 71 | LPUSH: 71942.45 requests per second 72 | LPOP: 71428.57 requests per second 73 | SADD: 70422.53 requests per second 74 | SPOP: 75757.58 requests per second 75 | LPUSH (needed to benchmark LRANGE): 72992.70 requests per second 76 | LRANGE_100 (first 100 elements): 24390.24 requests per second 77 | LRANGE_300 (first 300 elements): 11709.60 requests per second 78 | LRANGE_500 (first 450 elements): 8382.23 requests per second 79 | LRANGE_600 (first 600 elements): 6269.59 requests per second 80 | MSET (10 keys): 114942.53 requests per second 81 | ``` 82 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /CONTRIBUTING-ARCHIVED.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED 2 | 3 | This project is `Archived` and is no longer actively maintained; 4 | We are not accepting contributions or Pull Requests. 5 | 6 | -------------------------------------------------------------------------------- /DISABLED_COMMANDS.md: -------------------------------------------------------------------------------- 1 | ### Disabled commands ### 2 | 3 | The following redis commands are disabled, because they should generally be run on the actual redis server that you want information from: 4 | ``` 5 | multi 6 | watch 7 | exec 8 | unwatch 9 | bgrewriteaof 10 | bgsave 11 | client 12 | config 13 | dbsize 14 | debug 15 | flushall 16 | flushdb 17 | lastsave 18 | move 19 | monitor 20 | migrate 21 | object 22 | randomkey 23 | save 24 | shutdown 25 | slaveof 26 | slowlog 27 | sync 28 | time 29 | ``` 30 | 31 | The following redis commands are disabled if multiplexing is enabled, because they have the potential to operate on multiple keys: 32 | ``` 33 | discard 34 | eval 35 | bitop 36 | brpoplpush 37 | keys 38 | mget 39 | mset 40 | msetnx 41 | rename 42 | renamenx 43 | rpoplpush 44 | sdiff 45 | sdiffstore 46 | sinter 47 | sinterstore 48 | sinter 49 | smove 50 | sunion 51 | sunionstore 52 | zinterstore 53 | zunionstore 54 | ``` 55 | 56 | PubSub support is currently experimental, and only publish and subscribe are supported. 57 | Disabled: 58 | ``` 59 | psubscribe 60 | pubsub 61 | punsubscribe 62 | unsubscribe 63 | ``` 64 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Salesforce.com, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 17 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 22 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO=go 2 | #GO=/code/go15/bin/go 3 | REDISSERV=redis-server 4 | REDISCLI=redis-cli 5 | INTSOCK=/tmp/redis-test.sock 6 | VER=0.3.3.2 7 | BUILDFLAGS=-ldflags "-X github.com/salesforce/rmux.version=$(VER)" 8 | 9 | all: clean test build-dev build 10 | 11 | clean: 12 | rm -f ./build/* 13 | 14 | test: 15 | $(GO) test ./... 16 | 17 | test-dev: 18 | $(GO) test -v -tags 'dev' ./... 19 | 20 | test-integration: 21 | $(REDISSERV) $(PWD)/example/redis.conf 22 | sleep 1 23 | $(REDISCLI) -s $(INTSOCK) flushall 24 | $(GO) test -tags 'integration' ./... 25 | kill -TERM $$(cat /tmp/redis-test-instance-for-rmux.pid) 26 | 27 | test-integration-dev: 28 | $(REDISSERV) $(PWD)/example/redis.conf 29 | sleep 1 30 | $(REDISCLI) -s $(INTSOCK) flushall 31 | $(GO) test -tags 'integration dev' ./... 32 | kill -TERM $$(cat /tmp/redis-test-instance-for-rmux.pid) 33 | 34 | fmt: 35 | $(GO) fmt ./... 36 | 37 | mkbuild: 38 | mkdir -p ./build 39 | 40 | build: mkbuild 41 | $(GO) build $(BUILDFLAGS) -o build/rmux ./main 42 | 43 | build-all: mkbuild build build-linux-amd64 build-linux-386 build-darwin 44 | 45 | build-linux-amd64: mkbuild 46 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO) build $(BUILDFLAGS) -o build/rmux.amd64 ./main 47 | 48 | build-linux-386: mkbuild 49 | GOOS=linux GOARCH=386 CGO_ENABLED=0 $(GO) build $(BUILDFLAGS) -o build/rmux.386 ./main 50 | 51 | build-darwin: mkbuild 52 | GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 $(GO) build $(BUILDFLAGS) -o build/rmux.osx ./main 53 | 54 | build-dev: mkbuild 55 | $(GO) build $(BUILDFLAGS) -tags 'dev' -o build/rmux-dev ./main 56 | 57 | build-all-dev: mkbuild build-dev 58 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO) build $(BUILDFLAGS) -tags 'dev' -o build/rmux-linux-amd64-dev ./main 59 | GOOS=linux GOARCH=386 CGO_ENABLED=0 $(GO) build $(BUILDFLAGS) -tags 'dev' -o build/rmux-linux-386-dev ./main 60 | 61 | run-example: build 62 | ./build/rmux -config=./example/config.json -graphite=localhost:8125 -timing 63 | 64 | run-example-dev: build-dev 65 | ./build/rmux-dev -config=./example/config.json 66 | 67 | run-profile: build 68 | ./build/rmux -config=./example/config.json -cpuProfile=./build/profile.prof 69 | 70 | run-example-mux: build 71 | ./build/rmux -config=./example/config-mux.json 72 | 73 | .PHONY: clean test test-dev mkbuild build build-all build-dev build-all-dev fmt run-example run-example-dev run-profile test-integration test-integration-dev 74 | -------------------------------------------------------------------------------- /PRODUCTION_BENCHMARKS.md: -------------------------------------------------------------------------------- 1 | # Production Graphs # 2 | Rmux has been in production at Pardot since October. We have not had any issues with it in this time. It slightly improved, and smoothed out our upper_90th percentile connection and response times 3 | - Before: 4 | - ![Direct connection, upper 90th](graphs/direct_upper90.png) 5 | - After: 6 | - ![Rmux connection, upper 90th](graphs/rmux_upper90.png) 7 | ==== 8 | Our Upper connection times saw a more drastic increase: 9 | - Before: 10 | - ![Direct connection, upper 90th](graphs/direct_upper.png) 11 | - After: 12 | - ![Rmux connection, upper 90th](graphs/rmux_upper.png) 13 | 14 | Graphs will be added for pubsub side-effects, once we have finished rolling it out. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis CI](https://travis-ci.org/salesforce/rmux.svg?branch=master)](https://travis-ci.org/salesforce/rmux) 2 | # Rmux # 3 | 4 | Rmux is a Redis connection pooler and multiplexer, written in Go. Rmux is meant to be used for LAMP stacks, or other short-lived process applications, with high request volume. It should be run as a client, on every server that connects to redis--to reduce the total inbound connection count to the redis servers, while handle consistent multiplexing. 5 | 6 | ## Motivation ## 7 | 8 | At Pardot, we use redis (among other things) for our cache layer. Early on, we saw occasional latency spikes. After tuning our redis servers' net.ipv4.tcp.. settings , everything settled down--but as we grew, we began to see issues pop up again. 9 | 10 | While our Memory usage remained remarkably low, we saw occasional CPU spikes during peak access times. Adding more redis boxes, with key-based hashing in our application, surprisingly did not help. Pardot application severs run on a LAMP stack, which means that each request has to create its own connection to Redis. Since each application request hits multiple cache keys, destination redis boxes were receiving the same number of connections, but less commands. 11 | 12 | Since the issue seemed to be purely connection rates, and not command count, we started looking for a connection pooler. After finding none that were designed for redis, we built our own. Along the way, we built in key-based multiplexing, with a failover strategy in place. 13 | 14 | With rmux, our application servers all connect to a local unix socket, instead of the target destination redis port. Rmux then parses the incomming request, reads the first key, and hashes it to find which server to execute the command on. If a server is down, the command will instead be sent to a backup-hashed server. Since rmux understands the redis protocol, it also handles connection pooling//recycling for you, and handles server id management for the connections. 15 | 16 | When rmux hit production, we saw immediate gains in our 90th-percentile and upper-bound response times. 17 | 18 | ## Installing ## 19 | 20 | - Install [Go](http://golang.org/doc/install) 21 | - go get -u github.com/salesforce/rmux 22 | - go build -o /usr/local/bin/rmux github.com/salesforce/rmux/main 23 | 24 | 25 | ## Usage ## 26 | 27 | ``` 28 | Usage of rmux: 29 | -host="localhost": The host to listen for incoming connections on 30 | -localReadTimeout=0: Timeout to set locally (read) 31 | -localTimeout=0: Timeout to set locally (read+write) 32 | -localWriteTimeout=0: Timeout to set locally (write) 33 | -maxProcesses=0: The number of processes to use. If this is not defined, go's default is used. 34 | -poolSize=50: The size of the connection pools to use 35 | -port="6379": The port to listen for incoming connections on 36 | -remoteConnectTimeout=0: Timeout to set for remote redises (connect) 37 | -remoteReadTimeout=0: Timeout to set for remote redises (read) 38 | -remoteTimeout=0: Timeout to set for remote redises (connect+read+write) 39 | -remoteWriteTimeout=0: Timeout to set for remote redises (write) 40 | -socket="": The socket to listen for incoming connections on. If this is provided, host and port are ignored 41 | -tcpConnections="localhost:6380 localhost:6381": TCP connections (destination redis servers) to multiplex over 42 | -unixConnections="": Unix connections (destination redis servers) to multiplex over 43 | -config="": Path to configuration file 44 | ``` 45 | 46 | For more details about rmux configuration see [Configuration](doc/config.md) 47 | 48 | Localhost example: 49 | ``` 50 | redis-server --port 6379 & 51 | redis-server --port 6380 & 52 | redis-server --port 6381 & 53 | redis-server --port 6382 & 54 | rmux -socket=/tmp/rmux.sock -tcpConnections="localhost:6379 localhost:6380 localhost:6381 localhost:6382" & 55 | redis-cli -s /tmp/rmux.sock 56 | ``` 57 | 58 | - In the above example, all key-based commands will hash over ports 6379->6382 on localhost 59 | - If the server that a key hashes to is down, a backup server is automatically used (hashed based over the servers that are currently up) 60 | - All servers running production code should be running the same version (and destination flags) of rmux, and should be connecting over the rmux socket 61 | - Select will always return +OK, even if the server id is invalid 62 | - Ping will always return +PONG 63 | - Quit will always return +OK 64 | - Info will return an abbreviated response: 65 | 66 | ``` 67 | rmux_version: 1.0 68 | go_version: go1.1.2 69 | process_id: 48885 70 | connected_clients: 0 71 | active_endpoints: 4 72 | total_endpoints: 4 73 | role: master 74 | ``` 75 | 76 | Production equivalent: 77 | ``` 78 | rmux -socket=/tmp/rmux.sock -tcpConnections="redis1:6379 redis1:6380 redis2:6379 redis2:6380" 79 | ``` 80 | 81 | ### Disabled commands ### 82 | 83 | Redis commands that should only be run directly on a redis server are disabled. Commands that operate on more than one key (or have the potential to) are disabled if multiplexing is enabled. 84 | 85 | PubSub support is currently experimental, and only publish and subscribe are supported. 86 | Disabled: 87 | ``` 88 | psubscribe 89 | pubsub 90 | punsubscribe 91 | unsubscribe 92 | ``` 93 | 94 | [Full list of disabled commands](DISABLED_COMMANDS.md) 95 | 96 | ### Benchmarks ### 97 | 98 | Benchmarks with keep-alive off (simulating a lamp stack) show rmux being ~10x as fast as a direct connection, under heavy load. 99 | 100 | Benchmarks with keep-alive on (simulating how a java server would operate) show rmux being ~70% as fast as a direct connection. 101 | 102 | [Benchmark results here](BENCHMARKS.md) 103 | 104 | Rmux is currently used in production by Pardot. We have seen a reduction in our upper and 90th percentile connection and command times. The 90th percentile times are slightly improved, and the upper times are drastically improved. 105 | 106 | [Production graphite data is here](PRODUCTION_BENCHMARKS.md) 107 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package rmux 27 | 28 | import ( 29 | "bytes" 30 | "errors" 31 | "github.com/salesforce/rmux/connection" 32 | . "github.com/salesforce/rmux/log" 33 | "github.com/salesforce/rmux/protocol" 34 | . "github.com/salesforce/rmux/writer" 35 | "io" 36 | "net" 37 | "time" 38 | "github.com/salesforce/rmux/graphite" 39 | ) 40 | 41 | type readItem struct { 42 | command protocol.Command 43 | err error 44 | } 45 | 46 | //Represents a redis client that is connected to our rmux server 47 | type Client struct { 48 | //The underlying ReadWriter for this connection 49 | Writer *FlexibleWriter 50 | //Whether or not this client needs to consider multiplexing 51 | Multiplexing bool 52 | Connection net.Conn 53 | //The Database that our client thinks we're connected to 54 | DatabaseId int 55 | //Whether or not this client connection is active or not 56 | //Upon QUIT command, this gets toggled off 57 | Active bool 58 | ReadChannel chan readItem 59 | HashRing *connection.HashRing 60 | queued []protocol.Command 61 | Scanner *protocol.RespScanner 62 | } 63 | 64 | var ( 65 | ERR_QUIT = errors.New("Client asked to quit") 66 | ERR_CONNECTION_DOWN = errors.New(string(CONNECTION_DOWN_RESPONSE)) 67 | ERR_TIMEOUT = errors.New("Proxy timeout") 68 | ) 69 | 70 | //Initializes a new client, for the given established net connection, with the specified read/write timeouts 71 | func NewClient(connection net.Conn, readTimeout, writeTimeout time.Duration, isMuliplexing bool, hashRing *connection.HashRing) (newClient *Client) { 72 | newClient = &Client{} 73 | newClient.Connection = connection 74 | newClient.Writer = NewFlexibleWriter(connection) 75 | newClient.Active = true 76 | newClient.Multiplexing = isMuliplexing 77 | newClient.ReadChannel = make(chan readItem, 10000) 78 | newClient.queued = make([]protocol.Command, 0, 4) 79 | newClient.HashRing = hashRing 80 | newClient.DatabaseId = 0 81 | newClient.Scanner = protocol.NewRespScanner(connection) 82 | return 83 | } 84 | 85 | //Parses the given command 86 | func (this *Client) ParseCommand(command protocol.Command) ([]byte, error) { 87 | //block all unsafe commands 88 | if !protocol.IsSupportedFunction(command.GetCommand(), this.Multiplexing, command.GetArgCount() > 2) { 89 | return nil, protocol.ERR_COMMAND_UNSUPPORTED 90 | } 91 | 92 | if bytes.Equal(command.GetCommand(), protocol.PING_COMMAND) { 93 | return protocol.PONG_RESPONSE, nil 94 | } 95 | 96 | if bytes.Equal(command.GetCommand(), protocol.QUIT_COMMAND) { 97 | return nil, ERR_QUIT 98 | } 99 | 100 | if bytes.Equal(command.GetCommand(), protocol.SELECT_COMMAND) { 101 | databaseId, err := protocol.ParseInt(command.GetFirstArg()) 102 | if err != nil { 103 | return nil, protocol.ERR_BAD_ARGUMENTS 104 | } 105 | 106 | this.DatabaseId = databaseId 107 | return protocol.OK_RESPONSE, nil 108 | } 109 | 110 | return nil, nil 111 | } 112 | 113 | func (this *Client) WriteError(err error, flush bool) error { 114 | return protocol.WriteError([]byte(err.Error()), this.Writer, flush) 115 | } 116 | 117 | func (this *Client) FlushError(err error) error { 118 | return this.WriteError(err, true) 119 | } 120 | 121 | func (this *Client) WriteLine(line []byte) (err error) { 122 | return protocol.WriteLine(line, this.Writer, false) 123 | } 124 | 125 | func (this *Client) FlushLine(line []byte) (err error) { 126 | return protocol.WriteLine(line, this.Writer, true) 127 | } 128 | 129 | // Performs the query against the redis server and responds to the connected client with the response from redis. 130 | func (this *Client) FlushRedisAndRespond() error { 131 | var err error 132 | 133 | if !this.HasQueued() { 134 | return this.Writer.Flush() 135 | } 136 | 137 | var connectionPool *connection.ConnectionPool 138 | if !this.Multiplexing { 139 | connectionPool = this.HashRing.DefaultConnectionPool 140 | } else { 141 | if len(this.queued) != 1 { 142 | panic("Should not have multiple commands to flush when multiplexing") 143 | } 144 | connectionPool, err = this.HashRing.GetConnectionPool(this.queued[0]) 145 | if err != nil { 146 | Error("Failed to retrieve a connection pool from the hashring") 147 | this.ReadChannel <- readItem{nil, err} 148 | return err 149 | } 150 | } 151 | 152 | redisConn, err := connectionPool.GetConnection() 153 | if err != nil { 154 | Error("Failed to retrieve an active connection from the provided connection pool") 155 | this.ReadChannel <- readItem{nil, ERR_CONNECTION_DOWN} 156 | return ERR_CONNECTION_DOWN 157 | } 158 | defer connectionPool.RecycleRemoteConnection(redisConn) 159 | 160 | if redisConn.DatabaseId != this.DatabaseId { 161 | if err := redisConn.SelectDatabase(this.DatabaseId); err != nil { 162 | // Disconnect the current connection if selecting failed, will auto-reconnect this connection holder when queried later 163 | redisConn.Disconnect() 164 | return err 165 | } 166 | } 167 | 168 | numCommands := len(this.queued) 169 | 170 | startWrite := time.Now() 171 | 172 | for _, command := range this.queued { 173 | _, err := redisConn.Writer.Write(command.GetBuffer()) 174 | if err != nil { 175 | Error("Error when writing to server: %s. Disconnecting the connection.", err) 176 | redisConn.Disconnect() 177 | return err 178 | } 179 | } 180 | this.resetQueued() 181 | for redisConn.Writer.Buffered() > 0 { 182 | err := redisConn.Writer.Flush() 183 | if err != nil { 184 | Error("Error when flushing to server: %s. Disconnecting the connection.", err) 185 | redisConn.Disconnect() 186 | return err 187 | } 188 | } 189 | 190 | graphite.Timing("redis_write", time.Now().Sub(startWrite)) 191 | 192 | if err := protocol.CopyServerResponses(redisConn.Reader, this.Writer, numCommands); err != nil { 193 | Error("Error when copying redis responses to client: %s. Disconnecting the connection.", err) 194 | redisConn.Disconnect() 195 | this.ReadChannel <- readItem{nil, err} 196 | return err 197 | } 198 | 199 | this.Writer.Flush() 200 | 201 | return nil 202 | } 203 | 204 | func (this *Client) HasBufferedOutput() bool { 205 | return this.Writer.Buffered() > 0 206 | } 207 | 208 | // Read loop for this client - moves commands and channels to the worker loop 209 | func (this *Client) ReadLoop(rmux *RedisMultiplexer) { 210 | for rmux.active && this.Active && this.Scanner.Scan() { 211 | bytes := this.Scanner.Bytes() 212 | command, err := protocol.ParseCommand(bytes) 213 | this.ReadChannel <- readItem{command, err} 214 | } 215 | 216 | if err := this.Scanner.Err(); err != nil { 217 | this.ReadChannel <- readItem{nil, err} 218 | } else { 219 | this.ReadChannel <- readItem{nil, io.EOF} 220 | } 221 | } 222 | 223 | func (this *Client) resetQueued() { 224 | // We make a new one instead of using this.queued=this.queued[:0] so that the command arrays are eligible for GC 225 | this.queued = make([]protocol.Command, 0, 4) 226 | } 227 | 228 | func (this *Client) HasQueued() bool { 229 | return len(this.queued) > 0 230 | } 231 | 232 | func (this *Client) Queue(command protocol.Command) { 233 | this.queued = append(this.queued, command) 234 | } 235 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, salesforce.com, inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided 6 | * that the following conditions are met: 7 | * 8 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the 9 | * following disclaimer. 10 | * 11 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 12 | * the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or 15 | * promote products derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 18 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 19 | * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 21 | * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | * POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | package rmux 28 | 29 | import ( 30 | "bufio" 31 | "bytes" 32 | "github.com/salesforce/rmux/protocol" 33 | "github.com/salesforce/rmux/writer" 34 | "net" 35 | "testing" 36 | "time" 37 | ) 38 | 39 | func TestReadCommand(t *testing.T) { 40 | testData := []struct { 41 | input string 42 | command string 43 | argCount int 44 | arg1 string 45 | }{ 46 | {"+ping\r\n", "ping", 0, ""}, 47 | {"*1\r\n$4\r\nping\r\n", "ping", 0, ""}, 48 | {"*2\r\n$6\r\nselect\r\n$1\r\n1\r\n", "select", 1, "1"}, 49 | {"*2\r\n$6\r\nselect\r\n$1\r\na\r\n", "select", 1, "a"}, 50 | {"*5\r\n$3\r\nDEL\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n$4\r\nkey4\r\n", "del", 4, "key1"}, 51 | } 52 | 53 | listenSock, err := net.Listen("unix", "/tmp/rmuxTest1.sock") 54 | if err != nil { 55 | t.Fatal("Cannot listen on /tmp/rmuxTest1.sock: ", err) 56 | } 57 | defer listenSock.Close() 58 | testConnection, err := net.DialTimeout("unix", "/tmp/rmuxTest1.sock", 1*time.Second) 59 | if err != nil { 60 | t.Fatal("Could not dial in to our local rmux sock") 61 | } 62 | defer testConnection.Close() 63 | client := NewClient(testConnection, 1*time.Millisecond, 1*time.Millisecond, true, nil) 64 | 65 | for _, data := range testData { 66 | input := bytes.NewBuffer([]byte(data.input)) 67 | 68 | 69 | client.Writer = writer.NewFlexibleWriter(new(bytes.Buffer)) 70 | client.Scanner = protocol.NewRespScanner(input) 71 | 72 | commandScanned := client.Scanner.Scan() 73 | if commandScanned == false { 74 | t.Errorf("Error when scanning command from %q: %s", data.input, client.Scanner.Err()) 75 | continue 76 | } 77 | 78 | commandBytes := client.Scanner.Bytes() 79 | 80 | command, err := protocol.ParseCommand(commandBytes) 81 | if err != nil { 82 | t.Errorf("Error when parsing command from %q: %s", data.input, err) 83 | continue 84 | } 85 | 86 | if bytes.Compare(command.GetCommand(), []byte(data.command)) != 0 { 87 | t.Errorf("Should have parsed command %q, got %q", data.command, command.GetCommand()) 88 | } 89 | 90 | if command.GetArgCount() != data.argCount { 91 | t.Errorf("%q should have parsed into %d argument(s), got %d", data.input, data.argCount, command.GetArgCount()) 92 | } 93 | 94 | if bytes.Compare(command.GetFirstArg(), []byte(data.arg1)) != 0 { 95 | t.Errorf("Should have first arg %q, got %q", data.arg1, command.GetFirstArg()) 96 | } 97 | } 98 | } 99 | 100 | func TestParseCommand(test *testing.T) { 101 | testCases := []struct { 102 | input []byte 103 | immediateResponse []byte 104 | err error 105 | }{ 106 | //should accept simple format 107 | {[]byte("+PING\r\n"), protocol.PONG_RESPONSE, nil}, 108 | //should accept multibulk format 109 | {[]byte("*1\r\n$4\r\nping\r\n"), protocol.PONG_RESPONSE, nil}, 110 | //quit in proper format should respond appropriately 111 | {[]byte("*1\r\n$4\r\nquit\r\n"), nil, ERR_QUIT}, 112 | //select without database should err 113 | {[]byte("*1\r\n$6\r\nselect\r\n"), nil, protocol.ERR_BAD_ARGUMENTS}, 114 | //select in proper format should respond appropriately 115 | {[]byte("*2\r\n$6\r\nselect\r\n$1\r\n1\r\n"), protocol.OK_RESPONSE, nil}, 116 | //select in a bad format should err 117 | {[]byte("*2\r\n$6\r\nselect\r\n$1\r\na\r\n"), nil, protocol.ERR_BAD_ARGUMENTS}, 118 | //random command on our blacklist should respond appropriately 119 | {[]byte("*1\r\n$4\r\nauth\r\n"), nil, protocol.ERR_COMMAND_UNSUPPORTED}, 120 | //random command on our pubsub list should respond appropriately 121 | {[]byte("*1\r\n$6\r\npubsub\r\n"), nil, protocol.ERR_COMMAND_UNSUPPORTED}, 122 | //multi should fail 123 | {[]byte("*1\r\n$5\r\nmulti\r\n"), nil, protocol.ERR_COMMAND_UNSUPPORTED}, 124 | } 125 | 126 | listenSock, err := net.Listen("unix", "/tmp/rmuxTest1.sock") 127 | if err != nil { 128 | test.Fatalf("Cannot listen on /tmp/rmuxTest1.sock: %s", err) 129 | } 130 | defer listenSock.Close() 131 | 132 | testConnection, err := net.DialTimeout("unix", "/tmp/rmuxTest1.sock", 1*time.Second) 133 | if err != nil { 134 | test.Fatal("Could not dial in to our local rmux sock") 135 | } 136 | defer testConnection.Close() 137 | 138 | client := NewClient(testConnection, 1*time.Millisecond, 1*time.Millisecond, true, nil) 139 | 140 | for _, testCase := range testCases { 141 | w := new(bytes.Buffer) 142 | //Make a small buffer, just to confirm flushes 143 | client.Writer = writer.NewFlexibleWriter(w) 144 | client.Scanner = protocol.NewRespScanner(bufio.NewReader(bytes.NewBuffer(testCase.input))) 145 | 146 | if client.Scanner.Scan() == false { 147 | test.Errorf("Scan expected to succeed") 148 | continue 149 | } 150 | 151 | readCommand, err := protocol.ParseCommand(client.Scanner.Bytes()) 152 | if err != nil { 153 | test.Errorf("Errored while reading the command %q: %s", string(testCase.input), err) 154 | return 155 | } 156 | 157 | immediateResponse, err := client.ParseCommand(readCommand) 158 | 159 | if bytes.Compare(testCase.immediateResponse, immediateResponse) != 0 { 160 | test.Errorf("ParseCommand(%q) should have returned immediate response %q, but returned %q", string(testCase.input), testCase.immediateResponse, immediateResponse) 161 | } 162 | 163 | if testCase.err != err { 164 | test.Errorf("ParseCommand(%q) should have returned err %q, but returned %q", string(testCase.input), testCase.err, err) 165 | } 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /connection/connection.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package connection 27 | 28 | import ( 29 | "bufio" 30 | "bytes" 31 | "errors" 32 | "fmt" 33 | . "github.com/salesforce/rmux/log" 34 | "github.com/salesforce/rmux/protocol" 35 | . "github.com/salesforce/rmux/writer" 36 | "net" 37 | "time" 38 | "github.com/salesforce/rmux/graphite" 39 | ) 40 | 41 | //An outbound connection to a redis server 42 | //Maintains its own underlying TimedNetReadWriter, and keeps track of its DatabaseId for select() changes 43 | type Connection struct { 44 | connection net.Conn 45 | //The database that we are currently connected to 46 | DatabaseId int 47 | // The reader from the redis server 48 | Reader *bufio.Reader 49 | // The writer to the redis server 50 | Writer *FlexibleWriter 51 | 52 | protocol string 53 | endpoint string 54 | connectTimeout time.Duration 55 | readTimeout time.Duration 56 | writeTimeout time.Duration 57 | } 58 | 59 | //Initializes a new connection, of the given protocol and endpoint, with the given connection timeout 60 | //ex: "unix", "/tmp/myAwesomeSocket", 50*time.Millisecond 61 | func NewConnection(Protocol, Endpoint string, ConnectTimeout, ReadTimeout, WriteTimeout time.Duration) *Connection { 62 | c := &Connection{} 63 | c.protocol = Protocol 64 | c.endpoint = Endpoint 65 | c.connectTimeout = ConnectTimeout 66 | c.readTimeout = ReadTimeout 67 | c.writeTimeout = WriteTimeout 68 | return c 69 | } 70 | 71 | func (c *Connection) Disconnect() { 72 | if c.connection != nil { 73 | c.connection.Close() 74 | Info("Disconnected a connection") 75 | graphite.Increment("disconnect") 76 | } 77 | c.connection = nil 78 | c.DatabaseId = 0 79 | c.Reader = nil 80 | c.Writer = nil 81 | } 82 | 83 | func (c *Connection) ReconnectIfNecessary() (err error) { 84 | if c.IsConnected() { 85 | return nil 86 | } 87 | 88 | // If it's not connected, manually disconnect the connection for sanity's sake 89 | c.Disconnect() 90 | 91 | c.connection, err = net.DialTimeout(c.protocol, c.endpoint, c.connectTimeout) 92 | if err != nil { 93 | Error("NewConnection: Error received from dial: %s", err) 94 | c.connection = nil 95 | return err 96 | } 97 | 98 | netReadWriter := protocol.NewTimedNetReadWriter(c.connection, c.readTimeout, c.writeTimeout) 99 | c.DatabaseId = 0 100 | c.Writer = NewFlexibleWriter(netReadWriter) 101 | c.Reader = bufio.NewReader(netReadWriter) 102 | 103 | return nil 104 | } 105 | 106 | //Selects the given database, for the connection 107 | //If an error is returned, or if an invalid response is returned from the select, then this will return an error 108 | //If not, the connections internal database will be updated accordingly 109 | func (this *Connection) SelectDatabase(DatabaseId int) (err error) { 110 | if this.connection == nil { 111 | Error("SelectDatabase: Selecting on invalid connection") 112 | return errors.New("Selecting database on an invalid connection") 113 | } 114 | 115 | err = protocol.WriteLine([]byte(fmt.Sprintf("select %d", DatabaseId)), this.Writer, true) 116 | if err != nil { 117 | Error("SelectDatabase: Error received from protocol.FlushLine: %s", err) 118 | return err 119 | } 120 | 121 | if line, isPrefix, err := this.Reader.ReadLine(); err != nil || isPrefix || !bytes.Equal(line, protocol.OK_RESPONSE) { 122 | if err == nil { 123 | err = errors.New("unknown ReadLine error") 124 | } 125 | 126 | Error("SelectDatabase: Error while attempting to select database. Err:%q Response:%q isPrefix:%t", err, line, isPrefix) 127 | this.Disconnect() 128 | return errors.New("Invalid select response") 129 | } 130 | 131 | this.DatabaseId = DatabaseId 132 | return 133 | } 134 | 135 | //Checks if the current connection is up or not 136 | //If we do not get a response, or if we do not get a PONG reply, or if there is any error, returns false 137 | func (myConnection *Connection) CheckConnection() bool { 138 | if myConnection.connection == nil { 139 | return false 140 | } 141 | 142 | //start := time.Now() 143 | //defer func() { 144 | // graphite.Timing("check_connection", time.Now().Sub(start)) 145 | //}() 146 | 147 | startWrite := time.Now() 148 | err := protocol.WriteLine(protocol.SHORT_PING_COMMAND, myConnection.Writer, true) 149 | if err != nil { 150 | Error("CheckConnection: Could not write PING Err:%s Timing:%s", err, time.Now().Sub(startWrite)) 151 | myConnection.Disconnect() 152 | return false 153 | } 154 | 155 | startRead := time.Now() 156 | line, isPrefix, err := myConnection.Reader.ReadLine() 157 | 158 | if err == nil && !isPrefix && bytes.Equal(line, protocol.PONG_RESPONSE) { 159 | return true 160 | } else { 161 | if err != nil { 162 | Error("CheckConnection: Could not read PING. Error: %s Timing:%s", err, time.Now().Sub(startRead)) 163 | } else if isPrefix { 164 | Error("CheckConnection: ReadLine returned prefix: %q", line) 165 | } else { 166 | Error("CheckConnection: Expected PONG response. Got: %q", line) 167 | } 168 | myConnection.Disconnect() 169 | return false 170 | } 171 | } 172 | 173 | func (c *Connection) IsConnected() bool { 174 | if c.connection == nil { 175 | return false 176 | } 177 | 178 | // Adds a hundredth a milli... 179 | c.connection.SetReadDeadline(time.Now().Add(time.Microsecond * 10)) 180 | var b [4]byte 181 | n, err := c.connection.Read(b[:]) 182 | 183 | if err != nil { 184 | if err, ok := err.(net.Error); ok { 185 | if err.Timeout() { 186 | return true 187 | } 188 | } 189 | 190 | Info("There was an error when checking the connection (%s), will reconnect the connection", err) 191 | return false 192 | } 193 | 194 | if n != 0 { 195 | Warn("Got %d bytes back when we expected 0.", n) 196 | } 197 | 198 | return true 199 | } 200 | -------------------------------------------------------------------------------- /connection/connection_pool.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package connection 27 | 28 | import ( 29 | . "github.com/salesforce/rmux/log" 30 | "time" 31 | "sync/atomic" 32 | "github.com/salesforce/rmux/graphite" 33 | "strings" 34 | "sync" 35 | ) 36 | 37 | const ( 38 | //Default connect timeout, for connection pools. Can be adjusted on individual pools after initialization 39 | EXTERN_CONNECT_TIMEOUT = time.Millisecond * 500 40 | //Default read timeout, for connection pools. Can be adjusted on individual pools after initialization 41 | EXTERN_READ_TIMEOUT = time.Millisecond * 500 42 | //Default write timeout, for connection pools. Can be adjusted on individual pools after initialization 43 | EXTERN_WRITE_TIMEOUT = time.Millisecond * 500 44 | ) 45 | 46 | //A pool of connections to a single outbound redis server 47 | type ConnectionPool struct { 48 | //The protocol to use for our connections (unix/tcp/udp) 49 | Protocol string 50 | //The endpoint to connect to 51 | Endpoint string 52 | //And overridable connect timeout. Defaults to EXTERN_CONNECT_TIMEOUT 53 | ConnectTimeout time.Duration 54 | //An overridable read timeout. Defaults to EXTERN_READ_TIMEOUT 55 | ReadTimeout time.Duration 56 | //An overridable write timeout. Defaults to EXTERN_WRITE_TIMEOUT 57 | WriteTimeout time.Duration 58 | //channel of recycled connections, for re-use 59 | connectionPool chan *Connection 60 | // The connection used for diagnostics (like checking that the pool is up) 61 | diagnosticConnection *Connection 62 | diagnosticConnectionLock sync.Mutex 63 | // Number of active connections 64 | Count int32 65 | connectedLock sync.RWMutex 66 | // Whether or not the connction pool is up or down 67 | isConnected bool 68 | } 69 | 70 | //Initialize a new connection pool, for the given protocol/endpoint, with a given pool capacity 71 | //ex: "unix", "/tmp/myAwesomeSocket", 5 72 | func NewConnectionPool(Protocol, Endpoint string, poolCapacity int, connectTimeout time.Duration, 73 | readTimeout time.Duration, writeTimeout time.Duration) (newConnectionPool *ConnectionPool) { 74 | newConnectionPool = &ConnectionPool{} 75 | newConnectionPool.Protocol = Protocol 76 | newConnectionPool.Endpoint = Endpoint 77 | newConnectionPool.connectionPool = make(chan *Connection, poolCapacity) 78 | newConnectionPool.ConnectTimeout = connectTimeout 79 | newConnectionPool.ReadTimeout = readTimeout 80 | newConnectionPool.WriteTimeout = writeTimeout 81 | newConnectionPool.Count = 0 82 | 83 | // Fill the pool with as many handlers as it asks for 84 | for i := 0; i < poolCapacity; i++ { 85 | newConnectionPool.connectionPool <- newConnectionPool.CreateConnection() 86 | } 87 | 88 | newConnectionPool.diagnosticConnection = newConnectionPool.CreateConnection() 89 | 90 | return 91 | } 92 | 93 | //Gets a connection from the connection pool 94 | func (cp *ConnectionPool) GetConnection() (connection *Connection, err error) { 95 | select { 96 | case connection = <-cp.connectionPool: 97 | atomic.AddInt32(&cp.Count, 1) 98 | 99 | if err := connection.ReconnectIfNecessary(); err != nil { 100 | // Recycle the holder, return an error 101 | cp.RecycleRemoteConnection(connection) 102 | Error("Received a nil connection in pool.GetConnection: %s", err) 103 | graphite.Increment("reconnect_error"); 104 | return nil, err 105 | } 106 | 107 | return connection, nil 108 | // TODO: Maybe a while/timeout/graphiteping loop? 109 | } 110 | } 111 | 112 | // Creates a new Connection basead on the pool's configuration 113 | func (cp *ConnectionPool) CreateConnection() *Connection { 114 | return NewConnection( 115 | cp.Protocol, 116 | cp.Endpoint, 117 | cp.ConnectTimeout, 118 | cp.ReadTimeout, 119 | cp.WriteTimeout, 120 | ) 121 | } 122 | 123 | func (cp *ConnectionPool) getDiagnosticConnection() (connection *Connection, err error) { 124 | cp.diagnosticConnectionLock.Lock() 125 | 126 | if err := cp.diagnosticConnection.ReconnectIfNecessary(); err != nil { 127 | Error("The diangnostic connection is down for %s:%s : %s", cp.Protocol, cp.Endpoint, err) 128 | cp.diagnosticConnectionLock.Unlock() 129 | return nil, err 130 | } 131 | 132 | return cp.diagnosticConnection, nil 133 | } 134 | 135 | func (cp *ConnectionPool) releaseDiagnosticConnection() { 136 | cp.diagnosticConnectionLock.Unlock() 137 | } 138 | 139 | //Recycles a connection back into our connection pool 140 | //If the pool is full, throws it away 141 | func (myConnectionPool *ConnectionPool) RecycleRemoteConnection(remoteConnection *Connection) { 142 | myConnectionPool.connectionPool <- remoteConnection 143 | atomic.AddInt32(&myConnectionPool.Count, -1) 144 | } 145 | 146 | func (cp *ConnectionPool) SetIsConnected(isConnected bool) { 147 | cp.connectedLock.Lock() 148 | defer cp.connectedLock.Unlock() 149 | cp.isConnected = isConnected 150 | } 151 | 152 | func (cp *ConnectionPool) IsConnected() bool { 153 | cp.connectedLock.RLock() 154 | defer cp.connectedLock.RUnlock() 155 | return cp.isConnected 156 | } 157 | 158 | //Checks the state of connections in this connection pool 159 | //If a remote server has severe lag, mysteriously goes away, or stops responding all-together, returns false 160 | func (cp *ConnectionPool) CheckConnectionState() (isUp bool) { 161 | isUp = true 162 | defer func() { 163 | cp.SetIsConnected(isUp) 164 | }() 165 | 166 | connection, err := cp.getDiagnosticConnection() 167 | if err != nil { 168 | isUp = false 169 | return 170 | } 171 | defer cp.releaseDiagnosticConnection() 172 | 173 | //If we failed to bind, or if our PING fails, the pool is down 174 | if connection == nil || connection.connection == nil { 175 | isUp = false 176 | return 177 | } 178 | 179 | if !connection.CheckConnection() { 180 | connection.Disconnect() 181 | isUp = false 182 | return 183 | } 184 | 185 | return 186 | } 187 | 188 | func (cp *ConnectionPool) ReportGraphite() { 189 | endpoint := strings.Replace(cp.Endpoint, ".", "-", -1) 190 | endpoint = strings.Replace(cp.Endpoint, ":", "-", -1) 191 | 192 | graphite.Gauge("pools." + endpoint, int(cp.Count)) 193 | } 194 | -------------------------------------------------------------------------------- /connection/connection_pool_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package connection 27 | 28 | import ( 29 | "net" 30 | "testing" 31 | "time" 32 | "regexp" 33 | "os" 34 | "sync" 35 | "github.com/salesforce/rmux/protocol" 36 | "bytes" 37 | ) 38 | 39 | func TestRecycleConnection(test *testing.T) { 40 | testSocket := "/tmp/rmuxConnectionTest" 41 | listenSock, err := net.Listen("unix", testSocket) 42 | if err != nil { 43 | test.Fatal("Failed to listen on test socket ", testSocket) 44 | } 45 | defer listenSock.Close() 46 | 47 | //Setting the channel at size 2 makes this more interesting 48 | timeout := 500 * time.Millisecond 49 | connectionPool := NewConnectionPool("unix", testSocket, 2, timeout, timeout, timeout) 50 | 51 | connection, err := connectionPool.GetConnection() 52 | if err != nil { 53 | test.Errorf("Failed to get first connection: %s", err) 54 | return 55 | } 56 | 57 | connection2, err := connectionPool.GetConnection() 58 | if err != nil { 59 | test.Errorf("Failed to get second connection: %s", err) 60 | return 61 | } 62 | connectionPool.RecycleRemoteConnection(connection) 63 | if err != nil { 64 | test.Errorf("Should have been able to get a recycled connection: %s", err) 65 | return 66 | } 67 | connectionPool.RecycleRemoteConnection(connection2) 68 | 69 | listenSock.Close() 70 | connection, err = connectionPool.GetConnection() 71 | if err == nil { 72 | test.Errorf("Should not have been able to get the third connection") 73 | return 74 | } 75 | 76 | connection, err = connectionPool.GetConnection() 77 | if err == nil { 78 | test.Errorf("Should have failed to get the fourth connection: %s", err) 79 | return 80 | } 81 | } 82 | 83 | func _listenSocket(t *testing.T, socketPath string) net.Listener { 84 | listener, err := net.Listen("unix", socketPath) 85 | 86 | if err != nil { 87 | matched, _ := regexp.Match(`.*address already in use.*`, []byte(err.Error())) 88 | if matched { 89 | // try to unlink that socket file and rebind 90 | os.Remove(socketPath) 91 | 92 | listener, err = net.Listen("unix", socketPath) 93 | if err != nil { 94 | t.Fatalf("Failed to listen on test socket %s: %s", socketPath, err) 95 | } 96 | } else { 97 | t.Fatalf("Failed to listen on test socket %s: %s", socketPath, err) 98 | } 99 | } 100 | 101 | return listener 102 | } 103 | 104 | func TestCheckConnectionState(test *testing.T) { 105 | var wg sync.WaitGroup 106 | 107 | // Listen to on the socket 108 | testSocket := "/tmp/rmuxConnectionTest" 109 | listenSock := _listenSocket(test, testSocket) 110 | defer listenSock.Close() 111 | 112 | // Create the pool, have a size of zero so that no connections are made except for diagnostics 113 | timeout := 10 * time.Millisecond 114 | connectionPool := NewConnectionPool("unix", testSocket, 0, timeout, timeout, timeout) 115 | 116 | // get and release which will actually create the connection 117 | connectionPool.getDiagnosticConnection() 118 | connectionPool.releaseDiagnosticConnection() 119 | 120 | go func() { 121 | wg.Add(1) 122 | defer wg.Done() 123 | 124 | // Accept the connection 125 | fd, err := listenSock.Accept() 126 | if err != nil { 127 | test.Fatalf("Failed to accept connection: %s", err) 128 | } 129 | defer listenSock.Close() 130 | 131 | scanner := protocol.NewRespScanner(fd) 132 | 133 | // Read the first ping 134 | if !scanner.Scan() { 135 | test.Fatalf("Error scanning for bytes: %s", scanner.Err()) 136 | } else if !bytes.Equal(scanner.Bytes(), []byte("PING\r\n")) { 137 | test.Fatalf("Expected %q, got %q instead", "+PING\r\n", scanner.Bytes()) 138 | } 139 | // Write a pong response directly to the socket, this will be the first response 140 | if _, err := fd.Write([]byte("+PONG\r\n")); err != nil { 141 | test.Fatalf("Error writing to sock: %s", err) 142 | } 143 | 144 | // Now read a second ping 145 | if !scanner.Scan() { 146 | test.Fatalf("Error scanning for bytes: %s", scanner.Err()) 147 | } else if !bytes.Equal(scanner.Bytes(), []byte("PING\r\n")) { 148 | test.Fatalf("Expected %q, got %q instead", "+PING\r\n", scanner.Bytes()) 149 | } 150 | // and hang in response 151 | }() 152 | 153 | // First attempt should have a pong 154 | if !connectionPool.CheckConnectionState() { 155 | test.Fatal("Valid connection's check connection failed") 156 | } 157 | 158 | // Second one should time out 159 | if connectionPool.CheckConnectionState() { 160 | test.Fatal("In-valid connection's check connection succeeded when it should not have") 161 | } 162 | 163 | wg.Wait() 164 | } 165 | -------------------------------------------------------------------------------- /connection/connection_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package connection 27 | 28 | import ( 29 | "bufio" 30 | "bytes" 31 | "fmt" 32 | "github.com/salesforce/rmux/writer" 33 | "net" 34 | "testing" 35 | "time" 36 | ) 37 | 38 | func verifySelectDatabaseSuccess(test *testing.T, database int) { 39 | testSocket := "/tmp/rmuxConnectionTest" 40 | listenSock, err := net.Listen("unix", testSocket) 41 | if err != nil { 42 | test.Fatal("Failed to listen on test socket ", testSocket) 43 | } 44 | defer listenSock.Close() 45 | testConnection := NewConnection("unix", testSocket, 10*time.Millisecond, 10*time.Millisecond, 10*time.Millisecond) 46 | testConnection.ReconnectIfNecessary() 47 | 48 | //read buffer does't matter 49 | readBuf := bufio.NewReader(bytes.NewBufferString("+OK\r\n")) 50 | //write buffer will be used for verification 51 | w := new(bytes.Buffer) 52 | w.Reset() 53 | testConnection.Reader = readBuf 54 | testConnection.Writer = writer.NewFlexibleWriter(w) 55 | 56 | // Do the select 57 | if err := testConnection.SelectDatabase(database); err != nil { 58 | test.Fatalf("Error when selecting database: %s", err) 59 | } 60 | 61 | expectedWrite := []byte(fmt.Sprintf("select %d\r\n", database)) 62 | if !bytes.Equal(expectedWrite, w.Bytes()) { 63 | test.Fatalf("Select statement was not written to output buffer got:%q expected:%q", w.Bytes(), expectedWrite) 64 | } 65 | 66 | if err != nil { 67 | test.Fatal("Database select failed") 68 | } 69 | } 70 | 71 | func verifySelectDatabaseError(test *testing.T, database int) { 72 | testSocket := "/tmp/rmuxConnectionTest" 73 | listenSock, err := net.Listen("unix", testSocket) 74 | if err != nil { 75 | test.Fatal("Failed to listen on test socket ", testSocket) 76 | } 77 | defer func() { 78 | listenSock.Close() 79 | }() 80 | testConnection := NewConnection("unix", testSocket, 10*time.Millisecond, 10*time.Millisecond, 10*time.Millisecond) 81 | testConnection.ReconnectIfNecessary() 82 | //read buffer does't matter 83 | readBuf := bufio.NewReader(bytes.NewBufferString("+NOPE\r\n")) 84 | //write buffer will be used for verification 85 | w := new(bytes.Buffer) 86 | w.Reset() 87 | testConnection.Reader = readBuf 88 | testConnection.Writer = writer.NewFlexibleWriter(w) 89 | err = testConnection.SelectDatabase(database) 90 | 91 | expectedWrite := []byte(fmt.Sprintf("select %d\r\n", database)) 92 | if !bytes.Equal(expectedWrite, w.Bytes()) { 93 | test.Fatal("Select statement was not written to output buffer", w.Bytes(), expectedWrite) 94 | } 95 | 96 | if err == nil { 97 | test.Fatal("Database select did not fail, even though bad response code was given") 98 | } 99 | } 100 | 101 | func verifySelectDatabaseTimeout(test *testing.T, database int) { 102 | testSocket := "/tmp/rmuxConnectionTest" 103 | listenSock, err := net.Listen("unix", testSocket) 104 | if err != nil { 105 | test.Fatal("Failed to listen on test socket ", testSocket) 106 | } 107 | defer listenSock.Close() 108 | 109 | testConnection := NewConnection("unix", testSocket, 10*time.Millisecond, 10*time.Millisecond, 10*time.Millisecond) 110 | if err := testConnection.ReconnectIfNecessary(); err != nil { 111 | test.Fatalf("Could not connect to testSocket %s: %s", testSocket, err) 112 | } 113 | 114 | //write buffer will be used for verification 115 | w := new(bytes.Buffer) 116 | //Make a small buffer, just to confirm occasional flushes 117 | testConnection.Writer = writer.NewFlexibleWriter(w) 118 | err = testConnection.SelectDatabase(database) 119 | 120 | expectedWrite := []byte(fmt.Sprintf("select %d\r\n", database)) 121 | if !bytes.Equal(expectedWrite, w.Bytes()) { 122 | test.Fatalf("Select statement was not written to output buffer Got(%q) Expected(%q)", w.Bytes(), expectedWrite) 123 | } 124 | 125 | if err == nil { 126 | test.Fatal("Database select did not fail, even though there was no response") 127 | } 128 | } 129 | 130 | func TestSelectDatabase(test *testing.T) { 131 | verifySelectDatabaseSuccess(test, 0) 132 | verifySelectDatabaseSuccess(test, 1) 133 | verifySelectDatabaseSuccess(test, 123) 134 | 135 | verifySelectDatabaseError(test, 0) 136 | verifySelectDatabaseError(test, 1) 137 | verifySelectDatabaseError(test, 123) 138 | 139 | verifySelectDatabaseTimeout(test, 0) 140 | verifySelectDatabaseTimeout(test, 1) 141 | verifySelectDatabaseTimeout(test, 123) 142 | } 143 | 144 | func TestNewUnixConnection(test *testing.T) { 145 | testSocket := "/tmp/rmuxConnectionTest" 146 | listenSock, err := net.Listen("unix", testSocket) 147 | if err != nil { 148 | test.Fatal("Failed to listen on test socket ", testSocket) 149 | } 150 | defer listenSock.Close() 151 | 152 | connection := NewConnection("unix", testSocket, 10*time.Millisecond, 10*time.Millisecond, 10*time.Millisecond) 153 | connection.ReconnectIfNecessary() 154 | if connection == nil || connection.connection == nil { 155 | test.Fatal("Connection initialization returned nil, binding to unix endpoint failed") 156 | } 157 | 158 | connection = NewConnection("unix", "/tmp/thisdoesnotexist", 10*time.Millisecond, 10*time.Millisecond, 10*time.Millisecond) 159 | connection.ReconnectIfNecessary() 160 | if connection != nil && connection.connection != nil { 161 | test.Fatal("Connection initialization success, binding to fake unix endpoint succeeded????") 162 | } 163 | } 164 | 165 | func TestNewTcpConnection(test *testing.T) { 166 | testEndpoint := "localhost:8886" 167 | listenSock, err := net.Listen("tcp", testEndpoint) 168 | if err != nil { 169 | test.Fatalf("Error listening on tcp sock %s. Error: %s", testEndpoint, err) 170 | } 171 | defer listenSock.Close() 172 | 173 | connection := NewConnection("tcp", testEndpoint, 10*time.Millisecond, 10*time.Millisecond, 10*time.Millisecond) 174 | connection.ReconnectIfNecessary() 175 | if connection == nil || connection.connection == nil { 176 | test.Fatal("Connection initialization returned nil, binding to tcp endpoint failed") 177 | } 178 | 179 | //reserved sock should have nothing on it 180 | connection = NewConnection("tcp", "localhost:49151", 10*time.Millisecond, 10*time.Millisecond, 10*time.Millisecond) 181 | connection.ReconnectIfNecessary() 182 | if connection != nil && connection.connection != nil { 183 | test.Fatal("Connection initialization success, binding to fake tcp endpoint succeeded????") 184 | } 185 | } 186 | 187 | func TestCheckConnection(test *testing.T) { 188 | testSocket := "/tmp/rmuxConnectionTest" 189 | listenSock, err := net.Listen("unix", testSocket) 190 | if err != nil { 191 | test.Fatal("Failed to listen on test socket ", testSocket) 192 | } 193 | defer func() { 194 | listenSock.Close() 195 | }() 196 | 197 | connection := NewConnection("unix", testSocket, 100*time.Millisecond, 100*time.Millisecond, 100*time.Millisecond) 198 | connection.ReconnectIfNecessary() 199 | if connection == nil { 200 | test.Fatal("Connection initialization returned nil, binding to unix endpoint failed") 201 | } 202 | 203 | fd, err := listenSock.Accept() 204 | if err != nil { 205 | test.Fatal("Failed to accept connection") 206 | } 207 | 208 | // Buffering responses, one valid, one not 209 | if _, err := fd.Write([]byte("+PONG\r\nPONG")); err != nil { 210 | test.Fatalf("Failed to write to buffer: %s", err) 211 | } 212 | 213 | if !connection.CheckConnection() { 214 | test.Fatal("Valid connection's check connection failed") 215 | } 216 | 217 | if connection.CheckConnection() { 218 | test.Fatal("Invalid connection's check connection succeeded") 219 | } 220 | 221 | if connection.CheckConnection() { 222 | test.Fatal("Timing-out connection's check connection succeeded") 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /connection/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | 27 | //Package rmux/connection provides a way to open outbound connections to redis servers. 28 | //Connections are aware of redis databases and subscriptions, and come with a connectionPool for recycling. 29 | package connection 30 | -------------------------------------------------------------------------------- /connection/hash_ring.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package connection 27 | 28 | import ( 29 | "errors" 30 | // . "github.com/salesforce/rmux/log" 31 | "github.com/salesforce/rmux/protocol" 32 | ) 33 | 34 | var ERR_HASHRING_DOWN = errors.New("Hash ring is down") 35 | 36 | //An outbound connection to a redis server 37 | //Maintains its own underlying TimedNetReadWriter, and keeps track of its DatabaseId for select() changes 38 | type HashRing struct { 39 | //The connection pools that we will be hashing our connections to 40 | ConnectionPools []*ConnectionPool 41 | //The bitmask to use for all hashed queries 42 | BitMask uint32 43 | //The default connection pool 44 | DefaultConnectionPool *ConnectionPool 45 | // Whether to failover to next pool when the desired one is down 46 | Failover bool 47 | } 48 | 49 | func NewHashRing(connectionPools []*ConnectionPool, failover bool) (newHashRing *HashRing, err error) { 50 | newHashRing = &HashRing{} 51 | //The goal here is to have an even distribution of connection pools for a hash, 52 | //AND ensuring that the distribution stays balanced when a pool goes down 53 | //start out by rounding up to the nearest prime p 54 | poolLength := len(connectionPools) 55 | prime, err := newHashRing.getNextPrime(poolLength) 56 | if err != nil { 57 | return 58 | } 59 | // Debug("Making a hash ring for prime %v", prime) 60 | newHashRing.Failover = failover 61 | newHashRing.setBitMask(prime) 62 | newHashRing.ConnectionPools = make([]*ConnectionPool, newHashRing.BitMask+1) 63 | // Debug("Made a set of connection pools of size %v", len(newHashRing.ConnectionPools)) 64 | 65 | newHashRing.distributeConnectionPools(prime, connectionPools) 66 | return 67 | } 68 | 69 | func (myHashRing *HashRing) distributeConnectionPools(prime int, connectionPools []*ConnectionPool) { 70 | lastTarget := 0 71 | for multiplier := 1; multiplier < prime; multiplier++ { 72 | for value := 0; value < prime; value++ { 73 | if multiplier*value%prime < len(connectionPools) { 74 | lastTarget = multiplier * value % prime 75 | } 76 | myHashRing.ConnectionPools[(multiplier-1)*prime+value] = connectionPools[lastTarget] 77 | } 78 | } 79 | 80 | copy(myHashRing.ConnectionPools[prime*(prime-1):], myHashRing.ConnectionPools) 81 | if len(myHashRing.ConnectionPools) > 0 { 82 | myHashRing.DefaultConnectionPool = myHashRing.ConnectionPools[0] 83 | } 84 | } 85 | 86 | func (myHashRing *HashRing) getNextPrime(poolLength int) (int, error) { 87 | if poolLength == 0 { 88 | return -1, errors.New("At least one connection pool is required") 89 | } 90 | primes := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101} 91 | for _, curPrime := range primes { 92 | if poolLength <= curPrime { 93 | return curPrime, nil 94 | } 95 | } 96 | return -1, errors.New("Prime list isn't big enough") 97 | } 98 | 99 | func (myHashRing *HashRing) setBitMask(prime int) { 100 | myHashRing.BitMask = 1 101 | var minimumSize uint32 = uint32(prime * (prime - 1)) 102 | for minimumSize > myHashRing.BitMask { 103 | myHashRing.BitMask = myHashRing.BitMask << 1 104 | } 105 | myHashRing.BitMask = myHashRing.BitMask - 1 106 | } 107 | 108 | //Gets the connectionKey, for a to-be-multiplexed command 109 | //Uses the bernstein hash, which is one of the fastest key-distribution algorithms out there 110 | func (myHashRing *HashRing) GetConnectionPool(command protocol.Command) (connectionPool *ConnectionPool, err error) { 111 | var hash uint32 = 0 112 | if command.GetArgCount() > 0 { 113 | //The bernstein hash is one of the faster key-distribution algorithms out there, for small character keys 114 | //An alternate (but slower) algorithm would be to use go's built-in hash/fnv, if this proves insufficient 115 | for _, char := range command.GetFirstArg() { 116 | hash = hash<<5 + hash + uint32(char) 117 | } 118 | } 119 | 120 | hash = myHashRing.BitMask & hash 121 | targetHash := hash 122 | connectionPool = myHashRing.ConnectionPools[hash] 123 | 124 | for myHashRing.Failover && !connectionPool.IsConnected() { 125 | if hash == myHashRing.BitMask { 126 | hash = 0 127 | } else { 128 | hash = hash + 1 129 | } 130 | 131 | // If we've cycled through everything, break out 132 | if hash == targetHash { 133 | break 134 | } 135 | 136 | connectionPool = myHashRing.ConnectionPools[hash] 137 | } 138 | 139 | if !connectionPool.IsConnected() { 140 | return nil, ERR_HASHRING_DOWN 141 | } else { 142 | return connectionPool, nil 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | //Package rmux provides a connection-pooling, multiplexing redis server. 27 | //Commands are parsed, and multiplexed out based on their arguments. 28 | //Package rmux/main includes a working server implementation, if no customization is needed 29 | package rmux 30 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | Configuration can be handled either via command-line arguments or via config file. 4 | 5 | ### Command-line arguments 6 | ``` 7 | -host="localhost": The host to listen for incoming connections on 8 | -localReadTimeout=0: Timeout to set locally (read) 9 | -localTimeout=0: Timeout to set locally (read+write) 10 | -localWriteTimeout=0: Timeout to set locally (write) 11 | -maxProcesses=0: The number of processes to use. If this is not defined, go's default is used. 12 | -poolSize=50: The size of the connection pools to use 13 | -port="6379": The port to listen for incoming connections on 14 | -remoteConnectTimeout=0: Timeout to set for remote redises (connect) 15 | -remoteReadTimeout=0: Timeout to set for remote redises (read) 16 | -remoteTimeout=0: Timeout to set for remote redises (connect+read+write) 17 | -remoteWriteTimeout=0: Timeout to set for remote redises (write) 18 | -socket="": The socket to listen for incoming connections on. If this is provided, host and port are ignored 19 | -tcpConnections="localhost:6380 localhost:6381": TCP connections (destination redis servers) to multiplex over 20 | -unixConnections="": Unix connections (destination redis servers) to multiplex over 21 | -config="": Path to configuration file 22 | ``` 23 | 24 | ### Configuration file 25 | `rmux` accepts a `-config=/path/to/file.json` argument that specifies a path to a json configuration file. The format 26 | for the configuration json is as follows: 27 | ``` 28 | [ 29 | { 30 | "host": string, 31 | "port": int, 32 | "socket": string, 33 | "maxProcesses": int, 34 | "poolSize": int, 35 | "tcpConnections": [string, string, ...], 36 | "unixConnections": [string, string, ...], 37 | 38 | "localTimeout": int, 39 | "localReadTimeout": int, 40 | "localWriteTimeout": int, 41 | 42 | "remoteTimeout": int, 43 | "remoteReadTimeout": int, 44 | "remoteWriteTimeout": int, 45 | "remoteConnectTimeout": int 46 | }, 47 | ... 48 | ] 49 | ``` 50 | 51 | `[host, port]` or `socket` is required, as is at least one of `tcpConnections` or `unixConnections`. Using the configuration file 52 | you are capable of specifying and creating multiple rmux pools. 53 | -------------------------------------------------------------------------------- /example/config-mux.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "socket": "/usr/local/var/run/rmux-mux.sock", 4 | "tcpConnections": [ "localhost:6379", "localhost:6379" ] 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /example/config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "socket": "/tmp/rmux-example-run.sock", 4 | "tcpConnections": [ "localhost:6379" ], 5 | "poolSize": 2, 6 | "remoteTimeout": 2000 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /example/redis.conf: -------------------------------------------------------------------------------- 1 | # Redis configuration file for integration tests 2 | 3 | # By default Redis does not run as a daemon. Use 'yes' if you need it. 4 | daemonize yes 5 | 6 | # When running daemonized, Redis writes a pid file in /var/run/redis.pid by 7 | # default. You can specify a custom pid file location here. 8 | pidfile /tmp/redis-test-instance-for-rmux.pid 9 | 10 | # Accept connections on the specified port, default is 6379. 11 | # If port 0 is specified Redis will not listen on a TCP socket. 12 | port 0 13 | 14 | # Specify the path for the Unix socket that will be used to listen for 15 | # incoming connections. There is no default, so Redis will not listen 16 | # on a unix socket when not specified. 17 | unixsocket /tmp/redis-test.sock 18 | unixsocketperm 700 19 | 20 | # Set the number of databases. The default database is DB 0, you can select 21 | # a different one on a per-connection basis using SELECT where 22 | # dbid is a number between 0 and 'databases'-1 23 | databases 16 24 | -------------------------------------------------------------------------------- /func_client_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /* 4 | * Copyright (c) 2015, Salesforce.com, Inc. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 8 | * following conditions are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 11 | * disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 14 | * disclaimer in the documentation and/or other materials provided with the distribution. 15 | * 16 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 20 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | package rmux 29 | 30 | import ( 31 | "testing" 32 | "time" 33 | "net" 34 | "github.com/salesforce/rmux/connection" 35 | "bytes" 36 | ) 37 | 38 | func TestConnectTimeout(t *testing.T) { 39 | timeouts := []time.Duration { 40 | 10 * time.Millisecond, 41 | 20 * time.Millisecond, 42 | 30 * time.Millisecond, 43 | 1 * time.Millisecond, 44 | } 45 | 46 | rmux, err := NewRedisMultiplexer("unix", "/tmp/rmuxTestConnectTimeout.sock", 1) 47 | if err != nil { 48 | t.Errorf("Error from creating a new rmux instance: %s", err) 49 | return 50 | } 51 | defer rmux.Listener.Close() 52 | 53 | 54 | for _, timeout := range timeouts { 55 | rmux.EndpointConnectTimeout = timeout 56 | 57 | // Shamelessly stole this address from the net.DialTimeout test code 58 | rmux.AddConnection("tcp", "127.0.71.111:49151") 59 | 60 | start := time.Now() 61 | 62 | _, err = rmux.PrimaryConnectionPool.GetConnection() 63 | if err == nil { 64 | t.Errorf("Should have received a connect error from timing out") 65 | return 66 | } else if e, ok := err.(*net.OpError); !ok || !e.Timeout() { 67 | t.Errorf("Should have received a timeout error") 68 | return 69 | } 70 | 71 | diff := time.Now().Sub(start) 72 | if diff < timeout || diff > timeout + (10 * time.Millisecond) { 73 | t.Errorf("Should have timed out in the given interval between %s and %s, but instead timed out in %s", 74 | timeout, timeout + 10 * time.Millisecond, diff) 75 | } 76 | 77 | // Clear the pool 78 | rmux.ConnectionCluster = []*connection.ConnectionPool{} 79 | } 80 | } 81 | 82 | func TestReadTimeout(t *testing.T) { 83 | rmux, err := NewRedisMultiplexer("unix", "/tmp/rmuxTestConnectTimeout.sock", 1) 84 | if err != nil { 85 | t.Errorf("Error from creating a new rmux instance: %s", err) 86 | return 87 | } 88 | defer rmux.Listener.Close() 89 | 90 | mockRedis, err := net.Listen("tcp", "localhost:8090") 91 | if err != nil { 92 | t.Errorf("Error from listening on socket: %s", err) 93 | return 94 | } 95 | defer mockRedis.Close() 96 | 97 | rmux.SetAllTimeouts(10 * time.Millisecond) 98 | rmux.AddConnection("tcp", "localhost:8090") 99 | 100 | conn, err := rmux.PrimaryConnectionPool.GetConnection() 101 | if err != nil { 102 | t.Errorf("Error when getting connection: %s", err) 103 | return 104 | } 105 | 106 | conn.Writer.Write([]byte("+PING\r\n")) 107 | if err := conn.Writer.Flush(); err != nil { 108 | t.Errorf("Error when writing to redis: %s", err) 109 | } 110 | 111 | b := make([]byte, 2048) 112 | _, err = conn.Reader.Read(b) 113 | if err == nil { 114 | t.Errorf("Should have errored when reading from server that isn't responding") 115 | return 116 | } else if e, ok := err.(*net.OpError); !ok || !e.Timeout() { 117 | t.Errorf("Should have received a timeout error from read. Got %s", err) 118 | return 119 | } 120 | } 121 | 122 | func TestRmuxTimeoutConnectionTeardown(t *testing.T) { 123 | rmux, err := NewRedisMultiplexer("unix", "/tmp/rmuxTimeoutConnectionTeardown.sock", 1) 124 | if err != nil { 125 | t.Fatalf("Error creating new rmux instance: %s", err) 126 | } 127 | defer rmux.Listener.Close() 128 | 129 | redisSock := "/tmp/rmuxTimeoutConnectionTeardown-redis-1.sock" 130 | unixAddr, err := net.ResolveUnixAddr("unix", redisSock) 131 | if err != nil { 132 | t.Errorf("Error resolving socket: %s", err) 133 | } 134 | mockRedis, err := net.ListenUnix("unix", unixAddr) 135 | if err != nil { 136 | t.Errorf("Error listening no socket: %s", err) 137 | } 138 | defer mockRedis.Close() 139 | 140 | rmux.SetAllTimeouts(2 * time.Millisecond) 141 | rmux.AddConnection("unix", redisSock) 142 | 143 | conn, err := rmux.PrimaryConnectionPool.GetConnection() 144 | if err != nil { 145 | t.Errorf("Error when getting connection from pool: %s", err) 146 | } 147 | 148 | mockRedis.SetDeadline(time.Now().Add(10 * time.Millisecond)) 149 | mockConn, err := mockRedis.Accept() 150 | if err != nil { 151 | t.Errorf("Error when accepting connection: %s", err) 152 | } 153 | 154 | // Sends a ping, which should time out and disconnect the connection. 155 | if conn.CheckConnection() { 156 | t.Errorf("connection.CheckConnection should have returned false") 157 | } 158 | if conn.IsConnected() { 159 | t.Errorf("connection.CheckConnection should have flagged the connection as not connected") 160 | } 161 | 162 | // Read and respond from the server intentionally late... 163 | redisReadBuffer := make([]byte, 1024) 164 | nRead, err := mockConn.Read(redisReadBuffer) 165 | if err != nil { 166 | t.Errorf("Error reading: %s", err) 167 | } else if !bytes.Equal(redisReadBuffer[:nRead], []byte("PING\r\n")) { 168 | t.Errorf("Did not receive expected ping from client. Received %q", redisReadBuffer[:nRead]) 169 | } 170 | _, err = mockConn.Write([]byte("+PONG\r\n")) 171 | 172 | // Recycle the connection 173 | rmux.PrimaryConnectionPool.RecycleRemoteConnection(conn) 174 | 175 | // Get that same handler back since we're using a pool size of 1 176 | conn, err = rmux.PrimaryConnectionPool.GetConnection() 177 | if err != nil { 178 | t.Errorf("Error getting a connection: %s", err) 179 | } else if !conn.IsConnected() { 180 | t.Errorf("Got a disconnected client") 181 | } 182 | 183 | // Make sure there's nothing to read... The read timeout is 2 milliseconds, so this won't block 184 | clientReadBuffer := make([]byte, 1024) 185 | n, err := conn.Reader.Read(clientReadBuffer) 186 | if err == nil { 187 | t.Errorf("Read data when we should not have. Read: %q", clientReadBuffer[:n]) 188 | } 189 | 190 | // And this time successfully respond to a ping 191 | go func() { 192 | mockRedis.SetDeadline(time.Now().Add(100 * time.Millisecond)) 193 | conn, err := mockRedis.Accept() 194 | if err != nil { 195 | t.Errorf("Error when accepting connection: %s", err) 196 | } 197 | 198 | conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 199 | n, err := conn.Read(redisReadBuffer) 200 | if err != nil { 201 | t.Errorf("Error reading in redis: %s", err) 202 | } else if !bytes.Equal([]byte("PING\r\n"), redisReadBuffer[:n]) { 203 | t.Errorf("Expected +PING, got %q", redisReadBuffer[:n]) 204 | } 205 | 206 | _, err = conn.Write([]byte("+PONG\r\n")) 207 | if err != nil { 208 | t.Errorf("Error writing to client from redis: %s", err) 209 | } 210 | }() 211 | 212 | if !conn.CheckConnection() { 213 | t.Errorf("Should have been able to establish that the connection is valid") 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /graphite/graphite.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package graphite 27 | 28 | import ( 29 | "os" 30 | "net" 31 | "fmt" 32 | "strings" 33 | "strconv" 34 | "time" 35 | ) 36 | 37 | var udpConn *net.UDPConn = nil 38 | var prefix string 39 | var timingsEnabled bool = false 40 | 41 | func SetEndpoint(endpoint string) error { 42 | addr, err := net.ResolveUDPAddr("udp", endpoint) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | hostname, err := os.Hostname() 48 | if err != nil { 49 | return err 50 | } 51 | // replace any dots in the hostname with dashes 52 | hostname = strings.Replace(hostname, ".", "-", -1) 53 | 54 | conn, err := net.DialUDP("udp", nil, addr); 55 | if err != nil { 56 | return err 57 | } 58 | 59 | udpConn = conn 60 | prefix = fmt.Sprintf("rmux.%s.", hostname) 61 | return nil 62 | } 63 | 64 | func EnableTimings() { 65 | timingsEnabled = true 66 | } 67 | 68 | func Increment(metric string) { 69 | if Enabled() { 70 | sd := prefix + metric + ":1|c" 71 | udpConn.Write([]byte(sd)) 72 | } 73 | } 74 | 75 | func Gauge(metric string, value int) { 76 | if Enabled() { 77 | sd := prefix + metric + ":" + strconv.Itoa(value) + "|g" 78 | udpConn.Write([]byte(sd)) 79 | } 80 | } 81 | 82 | func Timing(metric string, value time.Duration) { 83 | if Enabled() && timingsEnabled { 84 | sd := fmt.Sprintf("%s%s:%.4f|ms", prefix, metric, float64(value)/float64(time.Millisecond)) 85 | udpConn.Write([]byte(sd)) 86 | } 87 | } 88 | 89 | func Enabled() bool { 90 | return udpConn != nil 91 | } 92 | 93 | // Todo: Maybe this should aggregate increments 94 | -------------------------------------------------------------------------------- /graphs/direct_upper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/rmux/ec21fc72ddfb5530cfe48a57ed579c9b8a9b2bc6/graphs/direct_upper.png -------------------------------------------------------------------------------- /graphs/direct_upper90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/rmux/ec21fc72ddfb5530cfe48a57ed579c9b8a9b2bc6/graphs/direct_upper90.png -------------------------------------------------------------------------------- /graphs/rmux_upper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/rmux/ec21fc72ddfb5530cfe48a57ed579c9b8a9b2bc6/graphs/rmux_upper.png -------------------------------------------------------------------------------- /graphs/rmux_upper90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/rmux/ec21fc72ddfb5530cfe48a57ed579c9b8a9b2bc6/graphs/rmux_upper90.png -------------------------------------------------------------------------------- /integration_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /* 4 | * Copyright (c) 2015, Salesforce.com, Inc. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 8 | * following conditions are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 11 | * disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 14 | * disclaimer in the documentation and/or other materials provided with the distribution. 15 | * 16 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 20 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | package rmux 29 | 30 | import ( 31 | "bytes" 32 | "net" 33 | "strings" 34 | "testing" 35 | "time" 36 | "strconv" 37 | "io" 38 | ) 39 | 40 | type tRmux struct { 41 | t *testing.T 42 | s *RedisMultiplexer 43 | } 44 | 45 | var ( 46 | redisSock string = "/tmp/redis-test.sock" 47 | rmuxSock string = "/tmp/rmux-test.sock" 48 | ) 49 | 50 | func StartRmux(t *testing.T, servers int) (r *tRmux) { 51 | r = &tRmux{} 52 | r.t = t 53 | 54 | // start rmux 55 | rs, err := NewRedisMultiplexer("unix", rmuxSock, 2) 56 | if err != nil { 57 | r.t.Fatalf("Error when creating new redis multiplexer: %s", err) 58 | } 59 | 60 | rs.SetAllTimeouts(2000 * time.Millisecond) 61 | 62 | r.s = rs 63 | 64 | for i := 0; i < servers; i++ { 65 | r.s.AddConnection("unix", redisSock) 66 | } 67 | 68 | if r.s.countActiveConnections() != servers { 69 | r.t.Errorf("Not as many connections are active as expected.") 70 | } 71 | 72 | go r.s.Start() 73 | 74 | return 75 | } 76 | 77 | func (r *tRmux) Cleanup() { 78 | r.s.active = false 79 | r.s.Listener.Close() 80 | } 81 | 82 | func TestStartRmux(t *testing.T) { 83 | r := StartRmux(t, 1) 84 | defer r.Cleanup() 85 | } 86 | 87 | func flushRedis(t *testing.T) { 88 | s, err := net.Dial("unix", redisSock) 89 | if err != nil { 90 | t.Fatalf("Error when dialing to redis for flush: %s", err) 91 | } 92 | defer s.Close() 93 | 94 | s.Write([]byte("flushall\r\n")) 95 | 96 | b := make([]byte, 2048) 97 | n, err := s.Read(b) 98 | if err != nil { 99 | t.Fatalf("Failed to read on flushall: %s", err) 100 | } 101 | 102 | if !bytes.Equal(b[:n], []byte("+OK\r\n")) { 103 | t.Fatalf("Expected OK response, got %q", b[:n]) 104 | } 105 | } 106 | 107 | func checkResponse(t *testing.T, in string, expected string) { 108 | var err error 109 | 110 | flushRedis(t) 111 | 112 | r := StartRmux(t, 1) 113 | defer r.Cleanup() 114 | 115 | sock, err := net.Dial("unix", rmuxSock) 116 | if err != nil { 117 | t.Fatalf("Error dialing rmux socket: %s", err) 118 | } 119 | defer sock.Close() 120 | 121 | _, err = sock.Write([]byte(in)) 122 | if err != nil { 123 | t.Fatalf("Error writing command: %s", err) 124 | } 125 | 126 | b := new(bytes.Buffer) 127 | for b.Len() < len(expected) { 128 | buf := make([]byte, 8*1024) 129 | sock.SetDeadline(time.Now().Add(1000 * time.Millisecond)) 130 | n, err := sock.Read(buf) 131 | if err == io.EOF { 132 | break 133 | } else if err != nil { 134 | t.Fatalf("Error reading from sock: %s", err) 135 | } 136 | 137 | b.Write(buf[:n]) 138 | } 139 | 140 | if read := b.Next(len(expected)); bytes.Compare(read, []byte(expected)) != 0 { 141 | t.Errorf("Did not read the expected response.\r\nExpected %q\r\nGot %q\r\n", expected, read) 142 | } 143 | } 144 | 145 | func checkMuxResponse(t *testing.T, in string, expected string) { 146 | var err error 147 | 148 | flushRedis(t) 149 | 150 | r := StartRmux(t, 2) 151 | defer r.Cleanup() 152 | 153 | sock, err := net.Dial("unix", rmuxSock) 154 | if err != nil { 155 | t.Fatalf("Error dialing rmux socket: %s", err) 156 | } 157 | defer sock.Close() 158 | 159 | _, err = sock.Write([]byte(in)) 160 | if err != nil { 161 | t.Fatalf("Error writing command: %s", err) 162 | } 163 | 164 | b := new(bytes.Buffer) 165 | for b.Len() < len(expected) { 166 | buf := make([]byte, 8*1024) 167 | sock.SetDeadline(time.Now().Add(1000 * time.Millisecond)) 168 | n, err := sock.Read(buf) 169 | if err == io.EOF { 170 | break 171 | } else if err != nil { 172 | t.Fatalf("Error reading from sock: %s", err) 173 | } 174 | 175 | b.Write(buf[:n]) 176 | } 177 | 178 | if read := b.Next(len(expected)); bytes.Compare(read, []byte(expected)) != 0 { 179 | t.Errorf("Did not read the expected response.\r\nGot %q\r\n", read) 180 | } 181 | } 182 | 183 | // given a simple command, construct a multi-bulk command 184 | func makeCommand(str string) string { 185 | splits := strings.Split(str, " ") 186 | 187 | cmd := "*" + strconv.Itoa(len(splits)) + "\r\n" 188 | 189 | for _, s := range splits { 190 | cmd = cmd + "$" + strconv.Itoa(len(s)) + "\r\n" + s + "\r\n" 191 | } 192 | 193 | return cmd 194 | } 195 | 196 | func TestResponse(t *testing.T) { 197 | cmd := "+PING\r\n" 198 | expected := "+PONG\r\n" 199 | checkResponse(t, cmd, expected) 200 | } 201 | 202 | func TestLargeResponse(t *testing.T) { 203 | cmd := "*3\r\n$4\r\nEVAL\r\n$47\r\nreturn cjson.encode(string.rep('a', 65 * 1024))\r\n$1\r\n0\r\n" 204 | expected := "$66562\r\n\"" + strings.Repeat("a", 66560) + "\"\r\n" 205 | checkResponse(t, cmd, expected) 206 | } 207 | 208 | func TestPipelineResponse(t *testing.T) { 209 | cmd := makeCommand("get key1") + makeCommand("set key1 test") + makeCommand("get key1") 210 | expected := "$-1\r\n+OK\r\n$4\r\ntest\r\n" 211 | checkResponse(t, cmd, expected) 212 | } 213 | 214 | func TestPipelineImmediateResponse(t *testing.T) { 215 | cmd := makeCommand("get key1") + makeCommand("set key1 test") + makeCommand("PING") + makeCommand("get key1") 216 | expected := "$-1\r\n+OK\r\n+PONG\r\n$4\r\ntest\r\n" 217 | checkResponse(t, cmd, expected) 218 | } 219 | 220 | func TestMuxPipelineResponse(t *testing.T) { 221 | cmd := makeCommand("get key1") + makeCommand("set key1 test") + makeCommand("get key1") 222 | expected := "$-1\r\n+OK\r\n$4\r\ntest\r\n" 223 | checkMuxResponse(t, cmd, expected) 224 | } 225 | 226 | func TestLargeResponseWithValidation(t *testing.T) { 227 | script := "local str = \"\"\r\nfor i=1,4000 do\r\nstr = str .. i .. \" \"\r\nend\r\nreturn str\r\n" 228 | cmd := "*3\r\n$4\r\nEVAL\r\n$" + strconv.Itoa(len(script)) + "\r\n" + script + "\r\n$1\r\n0\r\n" 229 | 230 | // construct the expected string 231 | expected := "" 232 | for i := 1; i <= 4000; i++ { 233 | expected = expected + strconv.Itoa(i) + " " 234 | } 235 | expectedResp := "$" + strconv.Itoa(len(expected)) + "\r\n" + expected + "\r\n" 236 | 237 | checkResponse(t, cmd, expectedResp) 238 | } 239 | 240 | func TestLargeRequest(t *testing.T) { 241 | // The data to set: 26 bytes * 3000 = 78000 bytes 242 | setData := strings.Repeat("abcdefghijklmnopqrstuvwxyz", 3000); 243 | cmd := makeCommand("set somekey " + setData) 244 | expected := "+OK\r\n" 245 | 246 | checkResponse(t, cmd, expected) 247 | } 248 | 249 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package log 27 | 28 | import ( 29 | "fmt" 30 | "log/syslog" 31 | "runtime/debug" 32 | ) 33 | 34 | const ( 35 | // From /usr/include/sys/syslog.h. 36 | // These are the same on Linux, BSD, and OS X. 37 | LOG_EMERG = iota 38 | LOG_ALERT 39 | LOG_CRIT 40 | LOG_ERR 41 | LOG_WARNING 42 | LOG_NOTICE 43 | LOG_INFO 44 | LOG_DEBUG 45 | ) 46 | 47 | var slw *syslog.Writer 48 | var _enableSyslog = true 49 | var _level = LOG_INFO 50 | 51 | func SetLogLevel(level int) { 52 | _level = level 53 | } 54 | 55 | func UseSyslog(useSyslog bool) { 56 | _enableSyslog = useSyslog 57 | if useSyslog { 58 | var e error 59 | slw, e = syslog.New(syslog.LOG_INFO, "rmux") 60 | if e != nil { 61 | fmt.Printf("Error initializing syslog: %s\r\n", e) 62 | } 63 | } 64 | } 65 | 66 | func Info(format string, a ...interface{}) { 67 | out := fmt.Sprintf(format, a...) 68 | 69 | if _enableSyslog && slw != nil { 70 | slw.Info(out) 71 | } 72 | if LOG_INFO <= _level { 73 | fmt.Println(out) 74 | } 75 | } 76 | 77 | func Debug(format string, a ...interface{}) { 78 | if LOG_DEBUG <= _level { 79 | out := fmt.Sprintf(format, a...) 80 | 81 | if _enableSyslog && slw != nil { 82 | slw.Info(out) 83 | } 84 | fmt.Println(out) 85 | } 86 | } 87 | 88 | func Error(format string, a ...interface{}) { 89 | out := fmt.Sprintf(format, a...) 90 | 91 | if _enableSyslog && slw != nil { 92 | slw.Err(out) 93 | } 94 | if LOG_ERR <= _level { 95 | fmt.Println(out) 96 | } 97 | } 98 | 99 | func LogPanic(r interface{}) { 100 | Error("Panic: %s\r\nStack: %s\r\n", r, debug.Stack()) 101 | } 102 | 103 | func Warn(format string, a ...interface{}) { 104 | out := fmt.Sprintf(format, a...) 105 | 106 | if _enableSyslog && slw != nil { 107 | slw.Warning(out) 108 | } 109 | if LOG_WARNING <= _level { 110 | fmt.Println(out) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /log/log_dev.go: -------------------------------------------------------------------------------- 1 | // +build dev 2 | 3 | /* 4 | * Copyright (c) 2015, Salesforce.com, Inc. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 8 | * following conditions are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 11 | * disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 14 | * disclaimer in the documentation and/or other materials provided with the distribution. 15 | * 16 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 20 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | 29 | package log 30 | 31 | func init() { 32 | SetLogLevel(LOG_DEBUG) 33 | } 34 | -------------------------------------------------------------------------------- /log/log_prod.go: -------------------------------------------------------------------------------- 1 | // +build !dev 2 | 3 | /* 4 | * Copyright (c) 2015, Salesforce.com, Inc. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 8 | * following conditions are met: 9 | * 10 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 11 | * disclaimer. 12 | * 13 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 14 | * disclaimer in the documentation and/or other materials provided with the distribution. 15 | * 16 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 20 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | package log 29 | 30 | func init() { 31 | SetLogLevel(LOG_INFO) 32 | } 33 | -------------------------------------------------------------------------------- /main/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "encoding/json" 30 | "io/ioutil" 31 | ) 32 | 33 | type PoolConfig struct { 34 | Host string `json:"host"` 35 | Port int `json:"port"` 36 | Socket string `json:"socket"` 37 | MaxProcesses int `json:"maxProcesses"` 38 | PoolSize int `json:"poolSize"` 39 | TcpConnections []string `json:"tcpConnections"` 40 | UnixConnections []string `json:"unixConnections"` 41 | LocalTimeout int64 `json:"localTimeout"` 42 | LocalReadTimeout int64 `json:"localReadTimeout"` 43 | LocalWriteTimeout int64 `json:"localWriteTimeout"` 44 | RemoteTimeout int64 `json:"remoteTimeout"` 45 | RemoteReadTimeout int64 `json:"remoteReadTimeout"` 46 | RemoteWriteTimeout int64 `json:"remoteWriteTimeout"` 47 | RemoteConnectTimeout int64 `json:"remoteConnectTimeout"` 48 | Failover bool `json:"failover"` 49 | } 50 | 51 | func ReadConfigFromFile(configFile string) ([]PoolConfig, error) { 52 | fileContents, err := ioutil.ReadFile(configFile) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | configs, err := ParseConfigJson(fileContents) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return configs, nil 63 | } 64 | 65 | func ParseConfigJson(configJson []byte) ([]PoolConfig, error) { 66 | var configs []PoolConfig 67 | 68 | if err := json.Unmarshal(configJson, &configs); err != nil { 69 | return nil, err 70 | } 71 | 72 | return configs, nil 73 | } 74 | -------------------------------------------------------------------------------- /main/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "reflect" 30 | "testing" 31 | ) 32 | 33 | var json1 = []byte(` 34 | [ 35 | { 36 | "socket": "/tmp/rmux-redis1.sock", 37 | "tcpConnections": [ "localhost:8001" ] 38 | }, 39 | { 40 | "socket": "/tmp/rmux-redis2.sock", 41 | "tcpConnections": [ "localhost:8002" ] 42 | } 43 | ] 44 | `) 45 | 46 | func TestParseConfigJson_Json1(test *testing.T) { 47 | config, err := ParseConfigJson(json1) 48 | if err != nil { 49 | test.Fatalf("Should not have errored parsing json1") 50 | } 51 | 52 | expects := []PoolConfig{{ 53 | Socket: "/tmp/rmux-redis1.sock", 54 | TcpConnections: []string{"localhost:8001"}, 55 | }, { 56 | Socket: "/tmp/rmux-redis2.sock", 57 | TcpConnections: []string{"localhost:8002"}, 58 | }} 59 | 60 | if !reflect.DeepEqual(expects, config) { 61 | test.Errorf("Did not parse configuration string as expected") 62 | } 63 | } 64 | 65 | var json2 = []byte(` 66 | [{ 67 | "host": "localhost", 68 | "port": 10001, 69 | "maxProcesses": 2, 70 | "poolSize": 30, 71 | 72 | "tcpConnections": [ "localhost:8001", "localhost:8002" ], 73 | 74 | "localTimeout": 30, 75 | "localReadTimeout": 35, 76 | "localWriteTimeout": 40, 77 | 78 | "remoteTimeout": 45, 79 | "remoteReadTimeout": 50, 80 | "remoteWriteTimeout": 55, 81 | "remoteConnectTimeout": 60 82 | }, 83 | { 84 | "host": "localhost", 85 | "port": 10001, 86 | "maxProcesses": 2, 87 | "poolSize": 30, 88 | "failover": true, 89 | 90 | "tcpConnections": [ "localhost:8001", "localhost:8002" ], 91 | 92 | "localTimeout": 30, 93 | "localReadTimeout": 35, 94 | "localWriteTimeout": 40, 95 | 96 | "remoteTimeout": 45, 97 | "remoteReadTimeout": 50, 98 | "remoteWriteTimeout": 55, 99 | "remoteConnectTimeout": 60 100 | }] 101 | `) 102 | 103 | func TestParseConfigJson_Json2(test *testing.T) { 104 | config, err := ParseConfigJson(json2) 105 | if err != nil { 106 | test.Fatalf("Should not have errored parsing json2") 107 | } 108 | 109 | expects := []PoolConfig{{ 110 | Host: "localhost", 111 | Port: 10001, 112 | MaxProcesses: 2, 113 | PoolSize: 30, 114 | Failover: false, 115 | 116 | TcpConnections: []string{"localhost:8001", "localhost:8002"}, 117 | 118 | LocalTimeout: 30, 119 | LocalReadTimeout: 35, 120 | LocalWriteTimeout: 40, 121 | 122 | RemoteTimeout: 45, 123 | RemoteReadTimeout: 50, 124 | RemoteWriteTimeout: 55, 125 | RemoteConnectTimeout: 60, 126 | }, { 127 | Host: "localhost", 128 | Port: 10001, 129 | MaxProcesses: 2, 130 | PoolSize: 30, 131 | Failover: true, 132 | 133 | TcpConnections: []string{"localhost:8001", "localhost:8002"}, 134 | 135 | LocalTimeout: 30, 136 | LocalReadTimeout: 35, 137 | LocalWriteTimeout: 40, 138 | 139 | RemoteTimeout: 45, 140 | RemoteReadTimeout: 50, 141 | RemoteWriteTimeout: 55, 142 | RemoteConnectTimeout: 60, 143 | }} 144 | 145 | if !reflect.DeepEqual(expects, config) { 146 | test.Errorf("Did not parse configuration string as expected") 147 | } 148 | } 149 | 150 | var json3 = []byte(` 151 | [{ 152 | "host": "localhost", 153 | "port": 10001, 154 | 155 | "tcpConnections": "localhost:8001" 156 | }] 157 | `) 158 | 159 | func TestParseConfigJson_Json3_Error(test *testing.T) { 160 | _, err := ParseConfigJson(json3) 161 | if err == nil { 162 | test.Fatalf("Should have errored attempting to parse json3") 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /main/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "errors" 30 | "flag" 31 | "fmt" 32 | "github.com/salesforce/rmux" 33 | . "github.com/salesforce/rmux/log" 34 | "net" 35 | "os" 36 | "runtime" 37 | "runtime/pprof" 38 | "strconv" 39 | "strings" 40 | "sync" 41 | "syscall" 42 | "github.com/salesforce/rmux/graphite" 43 | "time" 44 | ) 45 | 46 | const DEFAULT_POOL_SIZE = 20 47 | 48 | var host = flag.String("host", "localhost", "The host to listen for incoming connections on") 49 | var port = flag.Int("port", 6379, "The port to listen for incoming connections on") 50 | var socket = flag.String("socket", "", "The socket to listen for incoming connections on. If this is provided, host and port are ignored") 51 | var maxProcesses = flag.Int("maxProcesses", 0, "The number of processes to use. If this is not defined, go's default is used.") 52 | var poolSize = flag.Int("poolSize", DEFAULT_POOL_SIZE, "The size of the connection pools to use") 53 | var tcpConnections = flag.String("tcpConnections", "localhost:6380 localhost:6381", "TCP connections (destination redis servers) to multiplex over") 54 | var unixConnections = flag.String("unixConnections", "", "Unix connections (destination redis servers) to multiplex over") 55 | var localTimeout = flag.Int64("localTimeout", 0, "Timeout to set locally in milliseconds (read+write)") 56 | var localReadTimeout = flag.Int64("localReadTimeout", 0, "Timeout to set locally in milliseconds (read)") 57 | var localWriteTimeout = flag.Int64("localWriteTimeout", 0, "Timeout to set locally (write)") 58 | var remoteTimeout = flag.Int64("remoteTimeout", 0, "Timeout to set for remote redises (connect+read+write)") 59 | var remoteReadTimeout = flag.Int64("remoteReadTimeout", 0, "Timeout to set for remote redises (read)") 60 | var remoteWriteTimeout = flag.Int64("remoteWriteTimeout", 0, "Timeout to set for remote redises (write)") 61 | var remoteConnectTimeout = flag.Int64("remoteConnectTimeout", 0, "Timeout to set for remote redises (connect)") 62 | var cpuProfile = flag.String("cpuProfile", "", "Direct CPU Profile to target file") 63 | var configFile = flag.String("config", "", "Configuration file (JSON)") 64 | var doDebug = flag.Bool("debug", false, "Debug mode") 65 | var graphiteServer = flag.String("graphite", "", "Graphite statsd endpoint") 66 | var doTiming = flag.Bool("timing", false, "Send command timings to graphite") 67 | var failover = flag.Bool("failover", false, "Failover to another connection pool if target pool is down in mux mode") 68 | var useSyslog = flag.Bool("useSyslog", true, "If true, outputs to syslog as well as stdout") 69 | 70 | func main() { 71 | flag.Parse() 72 | 73 | var configs []PoolConfig 74 | var err error 75 | 76 | if *cpuProfile != "" { 77 | f, err := os.Create(*cpuProfile) 78 | terminateIfError(err, "Error when creating cpu profile file: %s\r\n") 79 | 80 | pprof.StartCPUProfile(f) 81 | defer pprof.StopCPUProfile() 82 | } 83 | 84 | if *doDebug { 85 | SetLogLevel(LOG_DEBUG) 86 | } else { 87 | SetLogLevel(LOG_INFO) 88 | } 89 | UseSyslog(*useSyslog) 90 | 91 | if *graphiteServer != "" { 92 | Info("Enabling graphite stats") 93 | err := graphite.SetEndpoint(*graphiteServer) 94 | if err != nil { 95 | Error("Error when setting graphite endpoint: %s", err) 96 | } 97 | } 98 | 99 | if *doTiming { 100 | Info("Enabling graphite timings") 101 | graphite.EnableTimings() 102 | } 103 | 104 | if *configFile != "" { 105 | configs, err = ReadConfigFromFile(*configFile) 106 | } else { 107 | configs, err = configureFromArgs() 108 | } 109 | terminateIfError(err, "Error parsing configuration options: %s\r\n") 110 | 111 | rmuxInstances, err := createInstances(configs) 112 | terminateIfError(err, "Error creating rmux instances: %s\r\n") 113 | 114 | Info("Starting %d rmux instances", len(rmuxInstances)) 115 | 116 | start(rmuxInstances) 117 | } 118 | 119 | func configureFromArgs() ([]PoolConfig, error) { 120 | var arrTcpConnections []string 121 | if *tcpConnections != "" { 122 | arrTcpConnections = strings.Split(*tcpConnections, " ") 123 | } else { 124 | arrTcpConnections = []string{} 125 | } 126 | 127 | var arrUnixConnections []string 128 | if *unixConnections != "" { 129 | arrUnixConnections = strings.Split(*unixConnections, " ") 130 | } else { 131 | arrUnixConnections = []string{} 132 | } 133 | 134 | config := []PoolConfig{{ 135 | Host: *host, 136 | Port: *port, 137 | Socket: *socket, 138 | MaxProcesses: *maxProcesses, 139 | PoolSize: *poolSize, 140 | Failover: *failover, 141 | 142 | TcpConnections: arrTcpConnections, 143 | UnixConnections: arrUnixConnections, 144 | 145 | LocalTimeout: *localTimeout, 146 | LocalReadTimeout: *localReadTimeout, 147 | LocalWriteTimeout: *localWriteTimeout, 148 | 149 | RemoteTimeout: *remoteTimeout, 150 | RemoteReadTimeout: *remoteReadTimeout, 151 | RemoteWriteTimeout: *remoteWriteTimeout, 152 | RemoteConnectTimeout: *remoteConnectTimeout, 153 | }} 154 | 155 | return config, nil 156 | } 157 | 158 | func createInstances(configs []PoolConfig) (rmuxInstances []*rmux.RedisMultiplexer, err error) { 159 | rmuxInstances = make([]*rmux.RedisMultiplexer, len(configs)) 160 | 161 | defer func() { 162 | if err != nil { 163 | for _, instance := range rmuxInstances { 164 | if instance == nil { 165 | continue 166 | } 167 | 168 | instance.Listener.Close() 169 | } 170 | 171 | rmuxInstances = nil 172 | } 173 | }() 174 | 175 | for i, config := range configs { 176 | var rmuxInstance *rmux.RedisMultiplexer 177 | 178 | if config.MaxProcesses > 0 { 179 | Info("Max processes increased to: %d from: %d", config.MaxProcesses, runtime.GOMAXPROCS(config.MaxProcesses)) 180 | } 181 | 182 | if config.PoolSize < 1 { 183 | Info("Pool size must be positive - defaulting to %d", DEFAULT_POOL_SIZE) 184 | config.PoolSize = DEFAULT_POOL_SIZE 185 | } 186 | 187 | if config.Socket != "" { 188 | syscall.Umask(0111) 189 | Info("Initializing rmux server on socket %s", config.Socket) 190 | rmuxInstance, err = rmux.NewRedisMultiplexer("unix", config.Socket, config.PoolSize) 191 | } else { 192 | Info("Initializing rmux server on host: %s and port: %d", config.Host, config.Port) 193 | rmuxInstance, err = rmux.NewRedisMultiplexer("tcp", net.JoinHostPort(config.Host, strconv.Itoa(config.Port)), config.PoolSize) 194 | } 195 | 196 | rmuxInstances[i] = rmuxInstance 197 | 198 | if err != nil { 199 | return 200 | } 201 | 202 | rmuxInstance.Failover = config.Failover 203 | 204 | if config.LocalTimeout != 0 { 205 | timeout := time.Duration(config.LocalTimeout) * time.Millisecond 206 | rmuxInstance.ClientReadTimeout = timeout 207 | rmuxInstance.ClientWriteTimeout = timeout 208 | Info("Setting local client read and write timeouts to: %s", timeout) 209 | } 210 | 211 | if config.LocalReadTimeout != 0 { 212 | timeout := time.Duration(config.LocalReadTimeout) * time.Millisecond 213 | rmuxInstance.ClientReadTimeout = timeout 214 | Info("Setting local client read timeout to: %s", timeout) 215 | } 216 | 217 | if config.LocalWriteTimeout != 0 { 218 | timeout := time.Duration(config.LocalWriteTimeout) * time.Millisecond 219 | rmuxInstance.ClientWriteTimeout = timeout 220 | Info("Setting local client write timeout to: %s", timeout) 221 | } 222 | 223 | if config.RemoteTimeout != 0 { 224 | duration := time.Duration(config.RemoteTimeout) * time.Millisecond 225 | rmuxInstance.EndpointConnectTimeout = duration 226 | rmuxInstance.EndpointReadTimeout = duration 227 | rmuxInstance.EndpointWriteTimeout = duration 228 | Info("Setting remote redis connect, read, and write timeouts to: %s", duration) 229 | } 230 | 231 | if config.RemoteConnectTimeout != 0 { 232 | duration := time.Duration(config.RemoteConnectTimeout) * time.Millisecond 233 | rmuxInstance.EndpointConnectTimeout = duration 234 | Info("Setting remote redis connect timeout to: %s", duration) 235 | } 236 | 237 | if config.RemoteReadTimeout != 0 { 238 | duration := time.Duration(config.RemoteReadTimeout) * time.Millisecond 239 | rmuxInstance.EndpointReadTimeout = duration 240 | Info("Setting remote redis read timeouts to: %s", duration) 241 | } 242 | 243 | if config.RemoteWriteTimeout != 0 { 244 | duration := time.Duration(config.RemoteWriteTimeout) * time.Millisecond 245 | rmuxInstance.EndpointWriteTimeout = duration 246 | Info("Setting remote redis write timeout to: %s", duration) 247 | } 248 | 249 | if len(config.TcpConnections) > 0 { 250 | for _, tcpConnection := range config.TcpConnections { 251 | Info("Adding tcp (destination) connection: %s", tcpConnection) 252 | rmuxInstance.AddConnection("tcp", tcpConnection) 253 | } 254 | } 255 | 256 | if len(config.UnixConnections) > 0 { 257 | for _, unixConnection := range config.UnixConnections { 258 | Info("Adding unix (destination) connection: %s", unixConnection) 259 | rmuxInstance.AddConnection("unix", unixConnection) 260 | } 261 | } 262 | 263 | if rmuxInstance.PrimaryConnectionPool == nil { 264 | err = errors.New("You must have at least one connection defined") 265 | return 266 | } 267 | } 268 | 269 | return rmuxInstances, nil 270 | } 271 | 272 | func start(rmuxInstances []*rmux.RedisMultiplexer) { 273 | var waitGroup sync.WaitGroup 274 | 275 | defer func() { 276 | for _, rmuxInstance := range rmuxInstances { 277 | rmuxInstance.Listener.Close() 278 | } 279 | }() 280 | 281 | for i, rmuxInstance := range rmuxInstances { 282 | waitGroup.Add(1) 283 | 284 | go func(instance *rmux.RedisMultiplexer) { 285 | defer waitGroup.Done() 286 | 287 | err := instance.Start() 288 | if err != nil { 289 | fmt.Fprintf(os.Stderr, "Error starting rmux instance %d: %s", i, err) 290 | return 291 | } 292 | }(rmuxInstance) 293 | } 294 | 295 | waitGroup.Wait() 296 | } 297 | 298 | // Terminates the program if the passed in error does not evaluate to nil. 299 | // err will be the first value of formatted string 300 | func terminateIfError(err error, format string, a ...interface{}) { 301 | if err != nil { 302 | allArgs := append([]interface{}{err}, a...) 303 | 304 | Error(format, allArgs...) 305 | os.Exit(1) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /protocol/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | //Represents a redis client that is connected to our rmux server 29 | type Command interface { 30 | GetCommand() []byte 31 | GetBuffer() []byte 32 | GetFirstArg() []byte 33 | GetArgCount() int 34 | } 35 | -------------------------------------------------------------------------------- /protocol/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | //Package rmux/protocol provides a standard way to listen in on the redis protocol, 27 | //look ahead to what commands are about to be executed, and ignore them or pass them 28 | //on to another buffer, as desired 29 | package protocol 30 | -------------------------------------------------------------------------------- /protocol/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | type RecoverableError struct { 29 | errMsg string 30 | } 31 | 32 | func (e *RecoverableError) Error() string { 33 | return e.errMsg 34 | } 35 | -------------------------------------------------------------------------------- /protocol/inline_command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bytes" 30 | ) 31 | 32 | type InlineCommand struct { 33 | Buffer []byte 34 | Command []byte 35 | // Usually denotes the key 36 | FirstArg []byte 37 | ArgCount int 38 | } 39 | 40 | func NewInlineCommand() *InlineCommand { 41 | c := &InlineCommand{} 42 | c.Buffer = nil 43 | c.Command = nil 44 | c.FirstArg = nil 45 | return c 46 | } 47 | 48 | func ParseInlineCommand(b []byte) (*InlineCommand, error) { 49 | c := NewInlineCommand() 50 | 51 | // Copy the bytes, probably not going to have access to that dataspace later 52 | c.Buffer = make([]byte, len(b)) 53 | copy(c.Buffer, b) 54 | 55 | parts := bytes.Split(c.Buffer[:len(c.Buffer)-2], []byte(" ")) 56 | 57 | for i, part := range parts { 58 | if i == 0 { 59 | c.Command = part 60 | 61 | for i := 0; i < len(c.Command); i++ { 62 | if c.Command[i] >= 'A' && c.Command[i] <= 'Z' { 63 | c.Command[i] = c.Command[i] + 0x20 64 | } 65 | } 66 | 67 | continue 68 | } 69 | 70 | if len(part) == 0 { 71 | continue 72 | } 73 | 74 | if c.FirstArg == nil { 75 | c.FirstArg = part 76 | } 77 | 78 | c.ArgCount++ 79 | } 80 | 81 | return c, nil 82 | } 83 | 84 | // Satisfy Command Interface 85 | func (this *InlineCommand) GetCommand() []byte { 86 | return this.Command 87 | } 88 | 89 | func (this *InlineCommand) GetBuffer() []byte { 90 | return this.Buffer 91 | } 92 | 93 | func (this *InlineCommand) GetFirstArg() []byte { 94 | return this.FirstArg 95 | } 96 | 97 | func (this *InlineCommand) GetArgCount() int { 98 | return this.ArgCount 99 | } 100 | -------------------------------------------------------------------------------- /protocol/inline_command_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "testing" 30 | ) 31 | 32 | var inlineTestData = map[string]commandTestData{ 33 | "PING\r\n": commandTestData{ 34 | "ping", 35 | "", 36 | "ping\r\n", 37 | 0, 38 | }, 39 | 40 | "BGREWRITEAOF\r\n": commandTestData{ 41 | "bgrewriteaof", 42 | "", 43 | "bgrewriteaof\r\n", 44 | 0, 45 | }, 46 | 47 | "command\r\n": commandTestData{ 48 | "command", 49 | "", 50 | "command\r\n", 51 | 0, 52 | }, 53 | 54 | "dbsize\r\n": commandTestData{ 55 | "dbsize", 56 | "", 57 | "dbsize\r\n", 58 | 0, 59 | }, 60 | 61 | "discard\r\n": commandTestData{ 62 | "discard", 63 | "", 64 | "discard\r\n", 65 | 0, 66 | }, 67 | 68 | // Test the expansion of the internal buffer slice 69 | "someverylongcommandthatprobablydoesntexist\r\n": commandTestData{ 70 | "someverylongcommandthatprobablydoesntexist", 71 | "", 72 | "someverylongcommandthatprobablydoesntexist\r\n", 73 | 0, 74 | }, 75 | 76 | "SOMEVERYLONGCOMMANDTHATPROBABLYDOESNTEXIST\r\n": commandTestData{ 77 | "someverylongcommandthatprobablydoesntexist", 78 | "", 79 | "someverylongcommandthatprobablydoesntexist\r\n", 80 | 0, 81 | }, 82 | 83 | "keys *\r\n": commandTestData{ 84 | "keys", 85 | "*", 86 | "keys *\r\n", 87 | 1, 88 | }, 89 | 90 | "KEYS *\r\n": commandTestData{ 91 | "keys", 92 | "*", 93 | "keys *\r\n", 94 | 1, 95 | }, 96 | 97 | "quit\r\n": commandTestData{ 98 | "quit", 99 | "", 100 | "quit\r\n", 101 | 0, 102 | }, 103 | 104 | "QUIT\r\n": commandTestData{ 105 | "quit", 106 | "", 107 | "quit\r\n", 108 | 0, 109 | }, 110 | 111 | // Larger than the internal buffer (64), will have to reallocate internally 112 | "1234567890123456789012345678901234567890 123456789012345678901234567890\r\n": commandTestData{ 113 | "1234567890123456789012345678901234567890", 114 | "123456789012345678901234567890", 115 | "1234567890123456789012345678901234567890 123456789012345678901234567890\r\n", 116 | 1, 117 | }, 118 | 119 | "del key1 key2 key3 key4\r\n": commandTestData{ 120 | "del", 121 | "key1", 122 | "del key1 key2 key3 key4\r\n", 123 | 4, 124 | }, 125 | 126 | "del key1 key2 key3 key4 \r\n": commandTestData{ 127 | "del", 128 | "key1", 129 | "del key1 key2 key3 key4 \r\n", 130 | 4, 131 | }, 132 | } 133 | 134 | func TestInlineCommand(test *testing.T) { 135 | tester := commandTester{test} 136 | 137 | for input, expected := range inlineTestData { 138 | command, err := ParseInlineCommand([]byte(input)) 139 | 140 | tester.checkCommandOutput(expected, command, err, input) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /protocol/multibulk_command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bytes" 30 | ) 31 | 32 | var NIL_STRING []byte = nil 33 | 34 | type MultibulkCommand struct { 35 | Buffer []byte 36 | Command []byte 37 | // Usually denotes the key 38 | FirstArg []byte 39 | ArgCount int 40 | } 41 | 42 | func ParseMultibulkCommand(b []byte) (*MultibulkCommand, error) { 43 | c := &MultibulkCommand{} 44 | c.Buffer = make([]byte, len(b)) 45 | copy(c.Buffer, b) 46 | 47 | if c.Buffer[0] != '*' { 48 | return nil, ERROR_COMMAND_PARSE 49 | } 50 | 51 | newlinePos := bytes.Index(c.Buffer, REDIS_NEWLINE) 52 | if newlinePos < 0 { 53 | return nil, ERROR_COMMAND_PARSE 54 | } 55 | 56 | count, err := ParseInt(c.Buffer[1:newlinePos]) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if count > 0 { 62 | c.ArgCount = count - 1 63 | } 64 | 65 | cBuf := c.Buffer[newlinePos+2:] 66 | for i := 0; i < 2 && i < count; i++ { 67 | if cBuf[0] != '$' { 68 | return nil, ERROR_COMMAND_PARSE 69 | } 70 | 71 | newlinePos := bytes.Index(cBuf, REDIS_NEWLINE) 72 | if newlinePos < 0 { 73 | return nil, ERROR_COMMAND_PARSE 74 | } 75 | 76 | count, err := ParseInt(cBuf[1:newlinePos]) 77 | if err != nil { 78 | return nil, err 79 | } else if count < 0 { 80 | cBuf = cBuf[newlinePos+2:] 81 | continue 82 | } 83 | 84 | if i == 0 { 85 | c.Command = cBuf[newlinePos+2 : newlinePos+2+count] 86 | } else { 87 | c.FirstArg = cBuf[newlinePos+2 : newlinePos+2+count] 88 | } 89 | 90 | cBuf = cBuf[newlinePos+2+count+2:] 91 | } 92 | 93 | for i := 0; i < len(c.Command); i++ { 94 | if char := c.Command[i]; char >= 'A' && char <= 'Z' { 95 | c.Command[i] = c.Command[i] + 0x20 96 | } 97 | } 98 | 99 | return c, nil 100 | } 101 | 102 | // Satisfy Command Interface 103 | func (this *MultibulkCommand) GetCommand() []byte { 104 | return this.Command 105 | } 106 | 107 | func (this *MultibulkCommand) GetBuffer() []byte { 108 | return this.Buffer 109 | } 110 | 111 | func (this *MultibulkCommand) GetFirstArg() []byte { 112 | return this.FirstArg 113 | } 114 | 115 | func (this *MultibulkCommand) GetArgCount() int { 116 | return this.ArgCount 117 | } 118 | -------------------------------------------------------------------------------- /protocol/multibulk_command_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "testing" 30 | ) 31 | 32 | var multibulkTestData = map[string]commandTestData{ 33 | "*2\r\n$4\r\nkeys\r\n$1\r\n*\r\n": commandTestData{ 34 | "keys", 35 | "*", 36 | "*2\r\n$4\r\nkeys\r\n$1\r\n*\r\n", 37 | 1, 38 | }, 39 | 40 | "*2\r\n$4\r\nKEYS\r\n$1\r\n*\r\n": commandTestData{ 41 | "keys", 42 | "*", 43 | "*2\r\n$4\r\nkeys\r\n$1\r\n*\r\n", 44 | 1, 45 | }, 46 | 47 | "*1\r\n$4\r\nquit\r\n": commandTestData{ 48 | "quit", 49 | "", 50 | "*1\r\n$4\r\nquit\r\n", 51 | 0, 52 | }, 53 | 54 | "*1\r\n$4\r\nQUIT\r\n": commandTestData{ 55 | "quit", 56 | "", 57 | "*1\r\n$4\r\nquit\r\n", 58 | 0, 59 | }, 60 | 61 | // Larger than the internal buffer (64), will have to reallocate internally 62 | "*2\r\n$40\r\n1234567890123456789012345678901234567890\r\n$30\r\n123456789012345678901234567890\r\n": commandTestData{ 63 | "1234567890123456789012345678901234567890", 64 | "123456789012345678901234567890", 65 | "*2\r\n$40\r\n1234567890123456789012345678901234567890\r\n$30\r\n123456789012345678901234567890\r\n", 66 | 1, 67 | }, 68 | 69 | "*5\r\n$3\r\nDEL\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n$4\r\nkey4\r\n": commandTestData{ 70 | "del", 71 | "key1", 72 | "*5\r\n$3\r\ndel\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n$4\r\nkey4\r\n", 73 | 4, 74 | }, 75 | 76 | // Handle Null Bulk Strings (http://redis.io/topics/protocol) 77 | "*2\r\n$3\r\ndel\r\n$-1\r\n": commandTestData{ 78 | "del", 79 | string(NIL_STRING), 80 | "*2\r\n$3\r\ndel\r\n$-1\r\n", 81 | 1, 82 | }, 83 | 84 | "*1\r\n$4\r\nPING\r\n": commandTestData{ 85 | "ping", 86 | "", 87 | "*1\r\n$4\r\nping\r\n", 88 | 0, 89 | }, 90 | } 91 | 92 | func TestMultibulkCommand(test *testing.T) { 93 | tester := commandTester{test} 94 | 95 | for input, expected := range multibulkTestData { 96 | command, err := ParseMultibulkCommand([]byte(input)) 97 | 98 | tester.checkCommandOutput(expected, command, err, input) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /protocol/protocol_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bufio" 30 | "bytes" 31 | "github.com/salesforce/rmux/writer" 32 | "strings" 33 | "testing" 34 | ) 35 | 36 | type ProtocolTester struct { 37 | *testing.T 38 | } 39 | 40 | func (test *ProtocolTester) compareInt(int1, int2 int) { 41 | if int1 != int2 { 42 | test.Errorf("Did not receive correct int values %d %d", int1, int2) 43 | } 44 | } 45 | 46 | func (test *ProtocolTester) verifyParseIntError(fakeInt []byte) { 47 | _, err := ParseInt(fakeInt) 48 | if err == nil { 49 | test.Errorf("ParseInt did not error on %q", fakeInt) 50 | } 51 | } 52 | 53 | func (test *ProtocolTester) verifyParseIntResponse(fakeInt []byte, expected int) { 54 | value, err := ParseInt(fakeInt) 55 | if err != nil { 56 | test.Fatalf("ParseInt fataled on %q", fakeInt) 57 | } 58 | 59 | test.compareInt(value, expected) 60 | } 61 | 62 | func TestParseInt(test *testing.T) { 63 | tester := &ProtocolTester{test} 64 | tester.verifyParseIntError([]byte("invalid int")) 65 | tester.verifyParseIntError([]byte("01b")) 66 | tester.verifyParseIntError([]byte("0b1")) 67 | tester.verifyParseIntError([]byte("b1")) 68 | 69 | tester.verifyParseIntResponse([]byte("-1"), -1) 70 | tester.verifyParseIntResponse([]byte("12345"), 12345) 71 | tester.verifyParseIntResponse([]byte("01"), 1) 72 | tester.verifyParseIntResponse([]byte("10"), 10) 73 | } 74 | 75 | func (test *ProtocolTester) compareString(str1, str2 string) { 76 | if str1 != str2 { 77 | test.Errorf("Did not receive correct string values %s %s", str1, str2) 78 | } 79 | } 80 | 81 | func TestWriteLine(test *testing.T) { 82 | w := new(bytes.Buffer) 83 | w.Reset() 84 | //Make a small buffer, just to confirm occasional flushes 85 | buf := writer.NewFlexibleWriter(w) 86 | //buffer of length 10 (8 plus \r\n) 87 | ten_bytes := []byte("0123456789") 88 | WriteLine(ten_bytes, buf, false) 89 | written := w.Bytes() 90 | if len(written) != 0 { 91 | test.Fatal("Buffer flushed prematurely") 92 | } 93 | WriteLine(ten_bytes, buf, false) 94 | written = w.Bytes() 95 | if len(written) != 0 { 96 | test.Fatal("Buffer flushed prematurely") 97 | } 98 | WriteLine(ten_bytes, buf, false) 99 | written = w.Bytes() 100 | if len(written) != 0 { 101 | test.Fatal("Buffer flushed prematurely") 102 | } 103 | WriteLine([]byte{'1'}, buf, true) 104 | written = w.Bytes() 105 | if len(written) != 39 { 106 | test.Fatalf("Buffer did not flush correctly. got:%d expected:%d", len(written), 38) 107 | } 108 | } 109 | 110 | func TestFlushLine(test *testing.T) { 111 | w := new(bytes.Buffer) 112 | w.Reset() 113 | //Make a small buffer, just to confirm occasional flushes 114 | buf := writer.NewFlexibleWriter(w) 115 | //buffer of length 10 (8 plus \r\n) 116 | ten_bytes := []byte("0123456789") 117 | WriteLine(ten_bytes, buf, false) 118 | written := w.Bytes() 119 | if len(written) != 0 { 120 | test.Fatal("Buffer flushed prematurely") 121 | } 122 | WriteLine(ten_bytes, buf, true) 123 | 124 | written = w.Bytes() 125 | if len(written) != 24 { 126 | test.Fatal("Buffer did not flush") 127 | } 128 | } 129 | 130 | func (test *ProtocolTester) verifyGoodCopyServerResponse(goodMessage, extraMessage string) { 131 | w := new(bytes.Buffer) 132 | w.Reset() 133 | //Make a small buffer, just to confirm occasional flushes 134 | writer := writer.NewFlexibleWriter(w) 135 | 136 | reader := bufio.NewReader(bytes.NewBufferString(strings.Join([]string{goodMessage, extraMessage}, ""))) 137 | 138 | err := CopyServerResponses(reader, writer, 1) 139 | if err != nil { 140 | test.Fatalf("CopyServerResponse fataled on %q", goodMessage) 141 | } 142 | if reader.Buffered() != len(extraMessage) { 143 | test.Fatalf("CopyServerResponse did not leave the right stuff on the buffer %q", goodMessage) 144 | } 145 | 146 | if !bytes.Equal(w.Bytes(), []byte(goodMessage)) { 147 | test.Fatalf("Our buffer is missing data? %q %q", w.Bytes(), []byte(goodMessage)) 148 | } 149 | } 150 | 151 | func BenchmarkGoodParseInt(bench *testing.B) { 152 | for i := 0; i < bench.N; i++ { 153 | ParseInt([]byte("12345")) 154 | } 155 | } 156 | 157 | func BenchmarkBadParseInt(bench *testing.B) { 158 | for i := 0; i < bench.N; i++ { 159 | ParseInt([]byte("a1")) 160 | } 161 | } 162 | 163 | var testDataAllRedisCommands = []struct { 164 | Command string 165 | SupportsMux bool 166 | SupportsNonMux bool 167 | }{ 168 | {"append", true, true}, 169 | {"auth", false, false}, 170 | {"bgrewriteaof", false, false}, 171 | {"bgsave", false, false}, 172 | {"bitcount", true, true}, 173 | {"bitop", false, true}, // has a different format than other commands 174 | {"bitpos", true, true}, 175 | {"blpop", false, true}, // key [key ...] timeout 176 | {"brpop", false, true}, // key [key ...] timeout 177 | {"brpoplpush", false, true}, // source destination timeout - source and destination are keys 178 | {"client", false, false}, // dangerous 179 | {"cluster", false, false}, // dangerous 180 | {"command", false, false}, // shouldn't need it 181 | {"config", false, false}, // dangerous 182 | {"dbsize", false, false}, // considered dangerous 183 | {"debug", false, false}, // dangerous 184 | {"decr", true, true}, 185 | {"decrby", true, true}, 186 | {"del", true, true}, 187 | {"discard", false, false}, // dont support transactions 188 | {"dump", true, true}, 189 | {"echo", true, true}, 190 | {"eval", false, true}, // can operate on several keys 191 | {"evalsha", false, true}, 192 | {"exec", false, false}, 193 | {"exists", true, true}, 194 | {"expireat", true, true}, 195 | {"flushall", false, true}, 196 | {"flushdb", false, true}, 197 | {"get", true, true}, 198 | {"getbit", true, true}, 199 | {"getrange", true, true}, 200 | {"getset", true, true}, 201 | {"hdel", true, true}, 202 | {"hexists", true, true}, 203 | {"hget", true, true}, 204 | {"hgetall", true, true}, 205 | {"hincrby", true, true}, 206 | {"hincrbyfloat", true, true}, 207 | {"hkeys", true, true}, 208 | {"hlen", true, true}, 209 | {"hmget", true, true}, 210 | {"hmset", true, true}, 211 | {"hsetnx", true, true}, 212 | {"hstrlen", true, true}, 213 | {"hvals", true, true}, 214 | {"incr", true, true}, 215 | {"incrby", true, true}, 216 | {"incrbyfloat", true, true}, 217 | {"info", true, true}, 218 | {"keys", false, true}, // can glob many keys, not supported over mux 219 | {"lastsave", false, false}, // system related information 220 | {"lindex", true, true}, 221 | {"linsert", true, true}, 222 | {"llen", true, true}, 223 | {"lpop", true, true}, 224 | {"lpush", true, true}, 225 | {"lpushx", true, true}, 226 | {"lrange", true, true}, 227 | {"lrem", true, true}, 228 | {"lset", true, true}, 229 | {"ltrim", true, true}, 230 | {"mget", false, true}, 231 | {"migrate", false, false}, // system related operation - dangerous 232 | {"monitor", false, false}, // system related operation - dangerous 233 | {"move", false, false}, // moves between dbs, let's not support 234 | {"mset", false, true}, // should operate on multiple keys 235 | {"multi", false, false}, // transaction related 236 | {"object", false, false}, // to inspect internals 237 | {"persist", true, true}, 238 | {"pexpire", true, true}, 239 | {"pexpireat", true, true}, 240 | {"pfadd", true, true}, 241 | {"pfcount", true, true}, 242 | {"pfmerge", false, true}, 243 | {"ping", true, true}, 244 | {"psetex", true, true}, 245 | {"psubscribe", false, false}, 246 | {"pubsub", false, false}, 247 | {"pttl", true, true}, 248 | {"publish", true, true}, 249 | {"punsubscribe", false, false}, 250 | {"quit", true, true}, 251 | {"randomkey", false, true}, 252 | {"rename", false, true}, 253 | {"renamenx", false, true}, 254 | {"restore", true, true}, 255 | {"role", false, false}, // returns role in replication 256 | {"rpop", true, true}, 257 | {"rpoplpush", false, true}, 258 | {"rpush", true, true}, 259 | {"rpushx", true, true}, 260 | {"sadd", true, true}, 261 | {"save", false, false}, 262 | {"scard", true, true}, 263 | {"script", true, true}, 264 | {"sdiff", false, true}, 265 | {"sdiffstore", false, true}, 266 | {"select", true, true}, 267 | {"set", true, true}, 268 | {"setbit", true, true}, 269 | {"setex", true, true}, 270 | {"setnx", true, true}, 271 | {"setrange", true, true}, 272 | {"shutdown", false, false}, // system related operation - dangerous 273 | {"sinter", false, true}, 274 | {"sinterstore", false, true}, 275 | {"sismember", true, true}, 276 | {"slaveof", false, false}, // system related operation - dangerous 277 | {"slowlog", false, false}, // system related operation - dangerous 278 | {"smembers", true, true}, 279 | {"smove", false, true}, 280 | {"sort", true, true}, 281 | {"spop", true, true}, 282 | {"srandmember", true, true}, 283 | {"srem", true, true}, 284 | {"strlen", true, true}, 285 | {"subscribe", false, false}, 286 | {"sunion", false, true}, 287 | {"sunionstore", false, true}, 288 | {"sync", false, false}, // used for replication 289 | {"time", true, true}, 290 | {"ttl", true, true}, 291 | {"type", true, true}, 292 | {"unsubscribe", false, false}, 293 | {"unwatch", false, false}, // transaction related 294 | {"watch", false, false}, // transaction related 295 | {"zadd", true, true}, 296 | {"zcard", true, true}, 297 | {"zcount", true, true}, 298 | {"zincrby", true, true}, 299 | {"zinterstore", false, true}, 300 | {"zlexcount", true, true}, 301 | {"zrange", true, true}, 302 | {"zrangebylex", true, true}, 303 | {"zrevrangebylex", true, true}, 304 | {"zrangebyscore", true, true}, 305 | {"zrank", true, true}, 306 | {"zrem", true, true}, 307 | {"zremrangebylex", true, true}, 308 | {"zremrangebyrank", true, true}, 309 | {"zremrangebyscore", true, true}, 310 | {"zrevremrange", true, true}, 311 | {"zrevrangebyscore", true, true}, 312 | {"zrevrank", true, true}, 313 | {"zscore", true, true}, 314 | {"zunionscore", false, true}, 315 | {"scan", false, true}, 316 | {"sscan", true, true}, 317 | {"hscan", true, true}, 318 | {"zscan", true, true}, 319 | } 320 | 321 | func TestIsSupportedFunction_NotMultipleKeys(test *testing.T) { 322 | for _, command := range testDataAllRedisCommands { 323 | bcommand := []byte(command.Command) 324 | 325 | if IsSupportedFunction(bcommand, true, false) != command.SupportsMux { 326 | if command.SupportsMux { 327 | test.Errorf("Should be supported in multiplexing mode but is not: %s", command.Command) 328 | } else { 329 | test.Errorf("Should not be supported in multiplexing mode but is: %s", command.Command) 330 | } 331 | } 332 | 333 | if IsSupportedFunction(bcommand, false, false) != command.SupportsNonMux { 334 | if command.SupportsNonMux { 335 | test.Errorf("Should be supported in non-multiplexing mode but is not: %s", command.Command) 336 | } else { 337 | test.Errorf("Should not be supported in non-multiplexing mode but is: %s", command.Command) 338 | } 339 | } 340 | } 341 | } 342 | 343 | func TestIsSupportedFunction_MultipleKeys(test *testing.T) { 344 | unsupportedWithMultipleKeys := map[string]bool{ 345 | // given multiple keys, the following should not be supported in mux mode 346 | "bitop": true, 347 | "blpop": true, 348 | "brpop": true, 349 | "del": true, 350 | "mget": true, 351 | "pfcount": true, 352 | "pfmerge": true, 353 | "sdiff": true, 354 | "sdiffstore": true, 355 | "sinter": true, 356 | "sinterstore": true, 357 | "sunion": true, 358 | "sunionstore": true, 359 | "watch": true, 360 | "zinterstore": true, 361 | "zunionstore": true, 362 | } 363 | 364 | for _, command := range testDataAllRedisCommands { 365 | bcommand := []byte(command.Command) 366 | 367 | isSupported := IsSupportedFunction(bcommand, true, true) 368 | 369 | if unsupportedWithMultipleKeys[command.Command] { 370 | if isSupported { 371 | test.Errorf("Should not be supported with multiple args in multiplexing mode but is: %s", command.Command) 372 | } 373 | } else { 374 | if isSupported != command.SupportsMux { 375 | test.Errorf("Should be supported if has multiple args in multiplexing mode but is not: %s", command.Command) 376 | } 377 | } 378 | } 379 | } 380 | 381 | func BenchmarkIsSupportedFunction(b *testing.B) { 382 | slice := []byte("sismember") 383 | 384 | for i := 0; i < b.N; i++ { 385 | IsSupportedFunction(slice, true, true) 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /protocol/read_writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "net" 30 | "time" 31 | ) 32 | 33 | //A ReadWriter for a NetConnection's read/writer, that allows for sane & reliable timeouts applied to all of its operations 34 | type TimedNetReadWriter struct { 35 | //The underlying connection used by our remote (redis) connection 36 | NetConnection net.Conn 37 | //Timeout to use for read operations 38 | ReadTimeout time.Duration 39 | //Timeout to use for write operations 40 | WriteTimeout time.Duration 41 | } 42 | 43 | //Wraps the net.connection's write function with a WriteDeadline 44 | func (myReadWriter *TimedNetReadWriter) Write(line []byte) (n int, err error) { 45 | if myReadWriter.WriteTimeout > 0 { 46 | myReadWriter.NetConnection.SetWriteDeadline(time.Now().Add(myReadWriter.WriteTimeout)) 47 | defer myReadWriter.NetConnection.SetWriteDeadline(time.Time{}) 48 | } 49 | n, err = myReadWriter.NetConnection.Write(line) 50 | return 51 | } 52 | 53 | //Wraps the net.connection's read function with a ReadDeadline 54 | func (myReadWriter *TimedNetReadWriter) Read(line []byte) (n int, err error) { 55 | if myReadWriter.ReadTimeout > 0 { 56 | myReadWriter.NetConnection.SetReadDeadline(time.Now().Add(myReadWriter.ReadTimeout)) 57 | defer myReadWriter.NetConnection.SetReadDeadline(time.Time{}) 58 | } 59 | n, err = myReadWriter.NetConnection.Read(line) 60 | return 61 | } 62 | 63 | //Initializes a TimedNetReadWriter, with the given timeouts 64 | func NewTimedNetReadWriter(connection net.Conn, readTimeout, writeTimeout time.Duration) (newReadWriter *TimedNetReadWriter) { 65 | newReadWriter = &TimedNetReadWriter{connection, readTimeout, writeTimeout} 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /protocol/scan.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bytes" 30 | ) 31 | 32 | func ScanResp(data []byte, atEOF bool) (advance int, token []byte, err error) { 33 | // if len(data) > 0 { 34 | // // Debug("Scanning %q", data) 35 | // } 36 | 37 | if atEOF && len(data) == 0 { 38 | return 0, nil, nil 39 | } 40 | 41 | if len(data) == 0 { 42 | return 0, nil, nil 43 | } 44 | 45 | switch peek := data[0]; peek { 46 | case '+': 47 | advance, token, err = ScanSimpleString(data, atEOF) 48 | case '$': 49 | advance, token, err = ScanBulkString(data, atEOF) 50 | case ':': 51 | advance, token, err = ScanInteger(data, atEOF) 52 | case '-': 53 | advance, token, err = ScanError(data, atEOF) 54 | case '*': 55 | advance, token, err = ScanArray(data, atEOF) 56 | default: 57 | advance, token, err = ScanInlineString(data, atEOF) 58 | } 59 | 60 | return 61 | } 62 | 63 | func scanNewline(data []byte, atEOF bool) (advance int, token []byte, err error) { 64 | dlen := len(data) 65 | 66 | if atEOF && dlen == 0 { 67 | return 0, nil, nil 68 | } 69 | 70 | s := 0 71 | for { 72 | ndxNL := bytes.IndexByte(data[s:], '\n') 73 | 74 | if ndxNL == 0 || ndxNL == 1 { 75 | // the newline is at 0 or 1! what! parse error. 76 | return dlen, nil, ERROR_COMMAND_PARSE 77 | } else if ndxNL > 1 { 78 | if data[s+ndxNL-1] == '\r' { 79 | // If we match \r\n, then advance and return that 80 | advance = s + ndxNL + 1 81 | return advance, data[:advance], nil 82 | } else { 83 | // Didn't match a CRNL, scan past the newline 84 | s += ndxNL + 1 85 | continue 86 | } 87 | } else if ndxNL < 0 { 88 | if atEOF { 89 | // Advance to the end, don't return anything 90 | return dlen, nil, nil 91 | } else { 92 | // No newline found, ask for more 93 | return 0, nil, nil 94 | } 95 | } 96 | } 97 | } 98 | 99 | // =============== Simple String ============== 100 | func ScanSimpleString(data []byte, atEOF bool) (advance int, token []byte, err error) { 101 | if atEOF && len(data) == 0 { 102 | return 0, nil, nil 103 | } 104 | 105 | if data[0] != '+' { 106 | return 0, nil, ERROR_COMMAND_PARSE 107 | } 108 | 109 | // Find the newline 110 | return scanNewline(data, atEOF) 111 | } 112 | 113 | // =============== Bulk String ============== 114 | func ScanBulkString(data []byte, atEOF bool) (advance int, token []byte, err error) { 115 | if atEOF && len(data) == 0 { 116 | return 0, nil, nil 117 | } 118 | 119 | if data[0] != '$' { 120 | return 0, nil, ERROR_COMMAND_PARSE 121 | } 122 | 123 | advance, token, err = scanNewline(data, atEOF) 124 | if err != nil || advance == 0 { 125 | return advance, token, err 126 | } 127 | 128 | if len(token) < 4 { 129 | return 0, nil, nil 130 | } 131 | 132 | strLenBytes := token[1 : len(token)-2] 133 | if len(strLenBytes) == 0 { 134 | return 0, nil, ERROR_COMMAND_PARSE 135 | } 136 | 137 | strLen, err := ParseInt(strLenBytes) 138 | if err != nil { 139 | return 0, nil, err 140 | } 141 | 142 | if strLen < 0 { 143 | // There's a negative string length, so it's a 'null' string. Return what we read 144 | return advance, data[:advance], nil 145 | } 146 | 147 | if len(data[advance:]) < 2+strLen { 148 | // Ask for more if we can't read what we have 149 | return 0, nil, nil 150 | } 151 | 152 | advance = advance + strLen + 2 153 | return advance, data[:advance], nil 154 | } 155 | 156 | // =============== Errors ============== 157 | func ScanError(data []byte, atEOF bool) (advance int, token []byte, err error) { 158 | if atEOF && len(data) == 0 { 159 | return 0, nil, nil 160 | } 161 | 162 | if data[0] != '-' { 163 | return 0, nil, ERROR_COMMAND_PARSE 164 | } 165 | 166 | return scanNewline(data, atEOF) 167 | } 168 | 169 | // =============== Integer ============== 170 | func ScanInteger(data []byte, atEOF bool) (advance int, token []byte, err error) { 171 | if atEOF && len(data) == 0 { 172 | return 0, nil, nil 173 | } 174 | 175 | if data[0] != ':' { 176 | return 0, nil, ERROR_COMMAND_PARSE 177 | } 178 | 179 | return scanNewline(data, atEOF) 180 | } 181 | 182 | // =============== Inline String ============== 183 | func ScanInlineString(data []byte, atEOF bool) (advance int, token []byte, err error) { 184 | return scanNewline(data, atEOF) 185 | } 186 | 187 | // =============== Array ============== 188 | func ScanArray(data []byte, atEOF bool) (advance int, token []byte, err error) { 189 | if atEOF && len(data) == 0 { 190 | return 0, nil, nil 191 | } 192 | 193 | if len(data) == 0 { 194 | return 0, nil, nil 195 | } 196 | 197 | if data[0] != '*' { 198 | return 0, nil, ERROR_COMMAND_PARSE 199 | } 200 | 201 | advance, token, err = scanNewline(data, atEOF) 202 | if err != nil { 203 | return 0, nil, err 204 | } else if advance == 0 || token == nil || len(token) < 3 { 205 | if len(token) < 3 && len(token) > 0 { 206 | // Debug("Hm. %q", token) 207 | } 208 | // Asking for more data 209 | return 0, nil, nil 210 | } 211 | 212 | arrayCountBytes := token[1 : len(token)-2] 213 | if len(arrayCountBytes) == 0 { 214 | return 0, nil, ERROR_COMMAND_PARSE 215 | } 216 | 217 | arrayCount, err := ParseInt(arrayCountBytes) 218 | if err != nil { 219 | return 0, nil, err 220 | } 221 | 222 | s := advance 223 | rData := data[s:] 224 | for i := 0; i < arrayCount; i++ { 225 | advance, token, err = ScanResp(rData, atEOF) 226 | if token == nil || err != nil { 227 | if advance == 0 { 228 | return 0, token, err 229 | } else { 230 | return s + advance, token, err 231 | } 232 | } 233 | 234 | s += advance 235 | 236 | rData = data[s:] 237 | } 238 | 239 | return s, data[:s], nil 240 | } 241 | 242 | -------------------------------------------------------------------------------- /protocol/scan_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bufio" 30 | "bytes" 31 | "testing" 32 | ) 33 | 34 | func TestScanResp(t *testing.T) { 35 | testData := []struct { 36 | inBytes string 37 | outResp []string 38 | }{ 39 | {"-Error statement\r\n-Another\r\n", []string{"-Error statement\r\n", "-Another\r\n"}}, 40 | {"+OK\r\n+PONG\r\n", []string{"+OK\r\n", "+PONG\r\n"}}, 41 | {":5\r\n:1\r\n", []string{":5\r\n", ":1\r\n"}}, 42 | {"$5\r\nbulks\r\n$4\r\nbulk\r\n", []string{"$5\r\nbulks\r\n", "$4\r\nbulk\r\n"}}, 43 | { 44 | "*2\r\n-Error Thing\r\n+OK\r\n*5\r\n$4\r\nping\r\n$3\r\nget\r\n$2\r\nok\r\n:5\r\n+ok\r\n", 45 | []string{ 46 | "*2\r\n-Error Thing\r\n+OK\r\n", 47 | "*5\r\n$4\r\nping\r\n$3\r\nget\r\n$2\r\nok\r\n:5\r\n+ok\r\n", 48 | }, 49 | }, 50 | { 51 | "*2\r\n*2\r\n+OK\r\n+PING\r\n*2\r\n$6\r\nSELECT\r\n:5\r\n+Test\r\n", 52 | []string{ 53 | "*2\r\n*2\r\n+OK\r\n+PING\r\n*2\r\n$6\r\nSELECT\r\n:5\r\n", 54 | "+Test\r\n", 55 | }, 56 | }, 57 | {"$-1\r\n$-1\r\n", []string{"$-1\r\n", "$-1\r\n"}}, 58 | {"*2\r\n$-1\r\n$-1\r\n", []string{"*2\r\n$-1\r\n$-1\r\n"}}, 59 | 60 | // Check for panic case in testing 61 | {"$", []string{}}, 62 | 63 | { 64 | "*2\r\n$3\r\nGET\r\n$14\r\nmonitor_master\r\n*3\r\n$7\r\nEVALSHA\r\n$40\r\n29e86375a0dc24139361139c9e8853cb34aa16a6\r\n$1\r\n0\r\nasdf", 65 | []string{ 66 | "*2\r\n$3\r\nGET\r\n$14\r\nmonitor_master\r\n", 67 | "*3\r\n$7\r\nEVALSHA\r\n$40\r\n29e86375a0dc24139361139c9e8853cb34aa16a6\r\n$1\r\n0\r\n", 68 | }, 69 | }, 70 | } 71 | 72 | for _, d := range testData { 73 | s := NewRespScanner(getReader(d.inBytes)) 74 | 75 | scanned := [][]byte{} 76 | for i := 0; s.Scan(); i++ { 77 | b := s.Bytes() 78 | scanned = append(scanned, b) 79 | 80 | if len(d.outResp) < i+1 { 81 | t.Errorf("Did not expect a %d-th response from %q", i, d.inBytes) 82 | continue 83 | } 84 | 85 | if bytes.Compare([]byte(d.outResp[i]), b) != 0 { 86 | t.Errorf("Did not scan expected resp data from %q. Expected %q, Got %q", d.inBytes, d.outResp[i], b) 87 | } 88 | } 89 | 90 | if len(scanned) != len(d.outResp) { 91 | t.Errorf("Did not receive expected number of scan results from %q. Expected %d, Got %d", d.inBytes, len(d.outResp), len(scanned)) 92 | t.Errorf("Received results %q", scanned) 93 | } 94 | } 95 | } 96 | 97 | func TestScanNewline(t *testing.T) { 98 | testData := []struct { 99 | inBytes string 100 | outResp []string 101 | }{ 102 | {"-Error statement\r\n-Another\r\n", []string{"-Error statement\r\n", "-Another\r\n"}}, 103 | {"Test newline\nin middle\r\nOf a grouping\r\n", []string{"Test newline\nin middle\r\n", "Of a grouping\r\n"}}, 104 | } 105 | 106 | for _, d := range testData { 107 | s := bufio.NewScanner(getReader(d.inBytes)) 108 | s.Split(scanNewline) 109 | 110 | for i := 0; s.Scan(); i++ { 111 | b := s.Bytes() 112 | 113 | if len(d.outResp) < i+1 { 114 | t.Errorf("Did not expect a %d-th response", i) 115 | continue 116 | } 117 | 118 | if bytes.Compare([]byte(d.outResp[i]), b) != 0 { 119 | t.Errorf("Did not scan expected resp data. Expected %q, Got %q", d.outResp[i], b) 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /protocol/scanner.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bytes" 30 | "io" 31 | ) 32 | 33 | // A partially-built scanner that can handle >64kb 34 | type RespScanner struct { 35 | r io.Reader 36 | token []byte 37 | tmp [2048]byte // temp read buffer 38 | b *bytes.Buffer 39 | err error 40 | 41 | empties int 42 | } 43 | 44 | func NewRespScanner(r io.Reader) *RespScanner { 45 | return &RespScanner{ 46 | r: r, 47 | b: new(bytes.Buffer), 48 | } 49 | } 50 | 51 | func (s *RespScanner) Scan() bool { 52 | for { 53 | if s.b.Len() > 0 || s.err != nil { 54 | // See if we can get a token with what we already have. 55 | advance, token, err := ScanResp(s.b.Bytes(), s.err != nil) 56 | 57 | if err != nil { 58 | s.setErr(err) 59 | return false 60 | } 61 | 62 | if !s.advance(advance) { 63 | return false 64 | } 65 | 66 | s.token = token 67 | if token != nil { 68 | if s.err == nil || advance > 0 { 69 | s.empties = 0 70 | } else { 71 | // Returning tokens not advancing input at EOF. 72 | s.empties++ 73 | if s.empties > 100 { 74 | panic("rmux.protocol.Scanner: 100 empty tokens without progressing") 75 | } 76 | } 77 | 78 | return true 79 | } 80 | } 81 | 82 | // We cannot generate a token with what we are holding. 83 | // If we've already hit EOF or an I/O error, we are done. 84 | if s.err != nil { 85 | return false 86 | } 87 | 88 | // Time to read data. 89 | for loop := 0; ; { 90 | n, err := s.r.Read(s.tmp[:]) 91 | 92 | if err != nil { 93 | s.setErr(err) 94 | break 95 | } 96 | 97 | s.b.Write(s.tmp[:n]) 98 | 99 | if n > 0 { 100 | s.empties = 0 101 | break 102 | } 103 | 104 | loop++ 105 | if loop > 100 { 106 | s.setErr(io.ErrNoProgress) 107 | break 108 | } 109 | } 110 | } 111 | } 112 | 113 | func (s *RespScanner) setErr(err error) { 114 | if s.err == nil || s.err == io.EOF { 115 | s.err = err 116 | } 117 | } 118 | 119 | func (s *RespScanner) Err() error { 120 | // EOF is an expected error when dealing with streams 121 | if s.err == io.EOF { 122 | return nil 123 | } 124 | return s.err 125 | } 126 | 127 | func (s *RespScanner) advance(n int) bool { 128 | s.b.Next(n) 129 | return true 130 | } 131 | 132 | // Returns the most recent token generated by a successful call to Scan() 133 | // The array's contents may be invalid on the next call to scan, make sure 134 | // to copy it somewhere safe. 135 | func (s *RespScanner) Bytes() []byte { 136 | return s.token 137 | } 138 | -------------------------------------------------------------------------------- /protocol/simple_command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "fmt" 30 | ) 31 | 32 | type SimpleCommand struct { 33 | Buffer []byte 34 | Command []byte 35 | } 36 | 37 | func ParseSimpleCommand(b []byte) (*SimpleCommand, error) { 38 | c := &SimpleCommand{} 39 | c.Buffer = make([]byte, len(b)) 40 | copy(c.Buffer, b) 41 | 42 | if c.Buffer[0] != '+' { 43 | return nil, fmt.Errorf("Expected '+', got '%c'", c.Buffer[0]) 44 | } 45 | 46 | c.Command = c.Buffer[1 : len(c.Buffer)-2] 47 | for i := 0; i < len(c.Command); i++ { 48 | // lowercase it 49 | if char := c.Command[i]; char >= 'A' && char <= 'Z' { 50 | c.Command[i] = c.Command[i] + 0x20 51 | } 52 | } 53 | 54 | return c, nil 55 | } 56 | 57 | // Satisfy Command Interface 58 | func (this *SimpleCommand) GetCommand() []byte { 59 | return this.Command 60 | } 61 | 62 | func (this *SimpleCommand) GetBuffer() []byte { 63 | return this.Buffer 64 | } 65 | 66 | func (this *SimpleCommand) GetFirstArg() []byte { 67 | return nil 68 | } 69 | 70 | func (this *SimpleCommand) GetArgCount() int { 71 | return 0 72 | } 73 | -------------------------------------------------------------------------------- /protocol/simple_command_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "testing" 30 | ) 31 | 32 | var simpleTestData = map[string]commandTestData{ 33 | "+PING\r\n": commandTestData{ 34 | "ping", 35 | "", 36 | "+ping\r\n", 37 | 0, 38 | }, 39 | 40 | "+BGREWRITEAOF\r\n": commandTestData{ 41 | "bgrewriteaof", 42 | "", 43 | "+bgrewriteaof\r\n", 44 | 0, 45 | }, 46 | 47 | "+command\r\n": commandTestData{ 48 | "command", 49 | "", 50 | "+command\r\n", 51 | 0, 52 | }, 53 | 54 | "+dbsize\r\n": commandTestData{ 55 | "dbsize", 56 | "", 57 | "+dbsize\r\n", 58 | 0, 59 | }, 60 | 61 | "+discard\r\n": commandTestData{ 62 | "discard", 63 | "", 64 | "+discard\r\n", 65 | 0, 66 | }, 67 | 68 | // Test the expansion of the internal buffer slice 69 | "+someverylongcommandthatprobablydoesntexist\r\n": commandTestData{ 70 | "someverylongcommandthatprobablydoesntexist", 71 | "", 72 | "+someverylongcommandthatprobablydoesntexist\r\n", 73 | 0, 74 | }, 75 | 76 | "+SOMEVERYLONGCOMMANDTHATPROBABLYDOESNTEXIST\r\n": commandTestData{ 77 | "someverylongcommandthatprobablydoesntexist", 78 | "", 79 | "+someverylongcommandthatprobablydoesntexist\r\n", 80 | 0, 81 | }, 82 | } 83 | 84 | func TestSimpleCommand(test *testing.T) { 85 | tester := commandTester{test} 86 | 87 | for input, expected := range simpleTestData { 88 | command, err := ParseSimpleCommand([]byte(input)) 89 | 90 | tester.checkCommandOutput(expected, command, err, input) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /protocol/string_command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bytes" 30 | ) 31 | 32 | type StringCommand struct { 33 | Buffer []byte 34 | Command []byte 35 | } 36 | 37 | func ParseStringCommand(b []byte) (*StringCommand, error) { 38 | c := &StringCommand{} 39 | c.Buffer = make([]byte, len(b)) 40 | copy(c.Buffer, b) 41 | 42 | if c.Buffer[0] != '$' { 43 | return nil, ERROR_COMMAND_PARSE 44 | } 45 | 46 | newlinePos := bytes.Index(c.Buffer, REDIS_NEWLINE) 47 | if newlinePos < 0 { 48 | return nil, ERROR_COMMAND_PARSE 49 | } 50 | 51 | strLen, err := ParseInt(c.Buffer[1:newlinePos]) 52 | if err != nil { 53 | return nil, err 54 | } else if strLen < 0 { 55 | return c, err 56 | } 57 | 58 | c.Command = c.Buffer[newlinePos+2 : newlinePos+2+strLen] 59 | for i := 0; i < len(c.Command); i++ { 60 | // lowercase it 61 | if char := c.Command[i]; char >= 'A' && char <= 'Z' { 62 | c.Command[i] = c.Command[i] + 0x20 63 | } 64 | } 65 | 66 | return c, nil 67 | } 68 | 69 | // Satisfy Command Interface 70 | func (this *StringCommand) GetCommand() []byte { 71 | return this.Command 72 | } 73 | 74 | func (this *StringCommand) GetBuffer() []byte { 75 | return this.Buffer 76 | } 77 | 78 | func (this *StringCommand) GetFirstArg() []byte { 79 | return nil 80 | } 81 | 82 | func (this *StringCommand) GetArgCount() int { 83 | return 0 84 | } 85 | -------------------------------------------------------------------------------- /protocol/string_command_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "testing" 30 | ) 31 | 32 | var stringTestData = map[string]commandTestData{ 33 | "$4\r\nPING\r\n": commandTestData{ 34 | "ping", 35 | "", 36 | "$4\r\nping\r\n", 37 | 0, 38 | }, 39 | 40 | "$12\r\nBGREWRITEAOF\r\n": commandTestData{ 41 | "bgrewriteaof", 42 | "", 43 | "$12\r\nbgrewriteaof\r\n", 44 | 0, 45 | }, 46 | 47 | "$7\r\ncommand\r\n": commandTestData{ 48 | "command", 49 | "", 50 | "$7\r\ncommand\r\n", 51 | 0, 52 | }, 53 | 54 | "$6\r\ndbsize\r\n": commandTestData{ 55 | "dbsize", 56 | "", 57 | "$6\r\ndbsize\r\n", 58 | 0, 59 | }, 60 | 61 | "$7\r\ndiscard\r\n": commandTestData{ 62 | "discard", 63 | "", 64 | "$7\r\ndiscard\r\n", 65 | 0, 66 | }, 67 | 68 | // Test the expansion of the internal buffer slice 69 | "$42\r\nsomeverylongcommandthatprobablydoesntexist\r\n": commandTestData{ 70 | "someverylongcommandthatprobablydoesntexist", 71 | "", 72 | "$42\r\nsomeverylongcommandthatprobablydoesntexist\r\n", 73 | 0, 74 | }, 75 | 76 | "$42\r\nSOMEVERYLONGCOMMANDTHATPROBABLYDOESNTEXIST\r\n": commandTestData{ 77 | "someverylongcommandthatprobablydoesntexist", 78 | "", 79 | "$42\r\nsomeverylongcommandthatprobablydoesntexist\r\n", 80 | 0, 81 | }, 82 | 83 | "$-1\r\n": commandTestData{ 84 | "", 85 | "", 86 | "$-1\r\n", 87 | 0, 88 | }, 89 | } 90 | 91 | func TestStringCommand(test *testing.T) { 92 | tester := commandTester{test} 93 | 94 | for input, expected := range stringTestData { 95 | command, err := ParseStringCommand([]byte(input)) 96 | 97 | tester.checkCommandOutput(expected, command, err, input) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /protocol/test_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package protocol 27 | 28 | import ( 29 | "bufio" 30 | "bytes" 31 | "strings" 32 | "testing" 33 | ) 34 | 35 | // Common functions and structs useful for tests in this package 36 | 37 | func getReader(str string) *bufio.Reader { 38 | return bufio.NewReader(strings.NewReader(str)) 39 | } 40 | 41 | type commandTestData struct { 42 | command string 43 | arg1 string 44 | buffer string 45 | argCount int 46 | } 47 | 48 | type commandTester struct { 49 | *testing.T 50 | } 51 | 52 | func (this *commandTester) checkCommandOutput(expects commandTestData, command Command, err error, input string) { 53 | if err != nil { 54 | this.Fatalf("Error parsing %q: %s", input, err) 55 | } 56 | 57 | if bytes.Compare([]byte(expects.buffer), command.GetBuffer()) != 0 { 58 | this.Errorf("Expected buffer to contain the full message.\r\nExpected:\r\n%q\r\nGot:\r\n%q", expects.buffer, command.GetBuffer()) 59 | } 60 | 61 | if bytes.Compare([]byte(expects.command), command.GetCommand()) != 0 { 62 | this.Errorf("Expected parsed command to match. Expected %q, got %q", expects.command, command.GetCommand()) 63 | } 64 | 65 | if bytes.Compare([]byte(expects.arg1), command.GetFirstArg()) != 0 { 66 | this.Errorf("Expected parsed arg1 to match. Expected %q, got %q", expects.arg1, command.GetFirstArg()) 67 | } 68 | 69 | if expects.argCount != command.GetArgCount() { 70 | this.Errorf("GetArgCount() did not match expectations.\r\nExpected: %d\r\nGot: %d", expects.argCount, command.GetArgCount()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package rmux 27 | 28 | import ( 29 | "fmt" 30 | "github.com/salesforce/rmux/connection" 31 | "github.com/salesforce/rmux/graphite" 32 | . "github.com/salesforce/rmux/log" 33 | "github.com/salesforce/rmux/protocol" 34 | "io" 35 | "net" 36 | "os" 37 | "os/signal" 38 | "runtime" 39 | "sync" 40 | "sync/atomic" 41 | "syscall" 42 | "time" 43 | "bytes" 44 | ) 45 | 46 | var ( 47 | //Response code for when a command (that operates on multiple keys) is used on a server that is multiplexing 48 | MULTIPLEX_OPERATION_UNSUPPORTED_RESPONSE = []byte("This command is not supported for multiplexing servers") 49 | //Response code for when a client can't connect to any target servers 50 | CONNECTION_DOWN_RESPONSE = []byte("Connection down") 51 | ) 52 | 53 | var version string = "dev" 54 | 55 | //The main RedisMultiplexer 56 | //Listens on a specified socket or port, and assigns out queries to any number of connection pools 57 | //If more than one connection pool is given multi-key operations are blocked 58 | type RedisMultiplexer struct { 59 | HashRing *connection.HashRing 60 | //hashmap of [connection endpoint] -> connectionPools 61 | ConnectionCluster []*connection.ConnectionPool 62 | //The net.listener for our server 63 | Listener net.Listener 64 | //The amount of connections to store, in each of our connectionpools 65 | PoolSize int 66 | //The primary connection key to use. If we're not operating on a key-based operation, it will go here 67 | PrimaryConnectionPool *connection.ConnectionPool 68 | //And overridable connect timeout. Defaults to EXTERN_CONNECT_TIMEOUT 69 | EndpointConnectTimeout time.Duration 70 | //An overridable read timeout. Defaults to EXTERN_READ_TIMEOUT 71 | EndpointReadTimeout time.Duration 72 | //An overridable write timeout. Defaults to EXTERN_WRITE_TIMEOUT 73 | EndpointWriteTimeout time.Duration 74 | //An overridable read timeout. Defaults to EXTERN_READ_TIMEOUT 75 | ClientReadTimeout time.Duration 76 | //An overridable write timeout. Defaults to EXTERN_WRITE_TIMEOUT 77 | ClientWriteTimeout time.Duration 78 | // The graphite statsd server to ping with metrics 79 | GraphiteServer *string 80 | //Whether or not the multiplexer is active. Used to determine when a tear-down should be occuring 81 | active bool 82 | //The amount of active (outbound) connections that we have 83 | activeConnectionCount int 84 | //The amount of total (incoming) connections that we have 85 | connectionCount int32 86 | //whether or not we are multiplexing 87 | multiplexing bool 88 | // Cached 'info' command response for multiplexing servers 89 | infoResponse []byte 90 | // Read/Write mutex for above infoResponse slice 91 | infoMutex sync.RWMutex 92 | // Whether to failover to another connection pool if the target connection pool is down (in multiplexing mode) 93 | Failover bool 94 | } 95 | 96 | //Sub-task that handles the cleanup when a server goes down 97 | func (this *RedisMultiplexer) initializeCleanup() { 98 | //Make a single-item channel for sigterm requests 99 | c := make(chan os.Signal, 1) 100 | signal.Notify(c, os.Interrupt) 101 | signal.Notify(c, syscall.SIGTERM) 102 | // Block until we have a kill-request to pop off 103 | <-c 104 | //Flag ourselves as cleaning up 105 | this.active = false 106 | //And close our listener 107 | this.Listener.Close() 108 | //Give ourselves a bit to clean up 109 | time.Sleep(time.Millisecond * 150) 110 | os.Exit(0) 111 | } 112 | 113 | //Initializes a new redis multiplexer, listening on the given protocol/endpoint, with a set connectionPool size 114 | //ex: "unix", "/tmp/myAwesomeSocket", 50 115 | func NewRedisMultiplexer(listenProtocol, listenEndpoint string, poolSize int) (newRedisMultiplexer *RedisMultiplexer, err error) { 116 | newRedisMultiplexer = &RedisMultiplexer{} 117 | newRedisMultiplexer.Listener, err = net.Listen(listenProtocol, listenEndpoint) 118 | if err != nil { 119 | println("listen error", err.Error()) 120 | return nil, err 121 | } 122 | newRedisMultiplexer.ConnectionCluster = make([]*connection.ConnectionPool, 0) 123 | newRedisMultiplexer.PoolSize = poolSize 124 | newRedisMultiplexer.active = true 125 | newRedisMultiplexer.EndpointConnectTimeout = connection.EXTERN_CONNECT_TIMEOUT 126 | newRedisMultiplexer.EndpointReadTimeout = connection.EXTERN_READ_TIMEOUT 127 | newRedisMultiplexer.EndpointWriteTimeout = connection.EXTERN_WRITE_TIMEOUT 128 | newRedisMultiplexer.ClientReadTimeout = connection.EXTERN_READ_TIMEOUT 129 | newRedisMultiplexer.ClientWriteTimeout = connection.EXTERN_WRITE_TIMEOUT 130 | newRedisMultiplexer.infoMutex = sync.RWMutex{} 131 | // Debug("Redis Multiplexer Initialized") 132 | return 133 | } 134 | 135 | //Adds a connection to the redis multiplexer, for the given protocol and endpoint 136 | func (this *RedisMultiplexer) AddConnection(remoteProtocol, remoteEndpoint string) { 137 | connectionCluster := connection.NewConnectionPool(remoteProtocol, remoteEndpoint, this.PoolSize, 138 | this.EndpointConnectTimeout, this.EndpointReadTimeout, this.EndpointWriteTimeout) 139 | this.ConnectionCluster = append(this.ConnectionCluster, connectionCluster) 140 | if len(this.ConnectionCluster) == 1 { 141 | this.PrimaryConnectionPool = connectionCluster 142 | } else { 143 | this.multiplexing = true 144 | } 145 | } 146 | 147 | //Counts the number of active endpoints on the server 148 | func (this *RedisMultiplexer) countActiveConnections() (activeConnections int) { 149 | activeConnections = 0 150 | for _, connectionPool := range this.ConnectionCluster { 151 | if connectionPool.CheckConnectionState() { 152 | activeConnections++ 153 | } 154 | } 155 | return 156 | } 157 | 158 | //Checks the status of all connections, and calculates how many of them are currently up 159 | func (this *RedisMultiplexer) maintainConnectionStates() { 160 | var m runtime.MemStats 161 | for this.active { 162 | this.activeConnectionCount = this.countActiveConnections() 163 | // // Debug("We have %d connections", this.connectionCount) 164 | runtime.ReadMemStats(&m) 165 | // // Debug("Memory profile: InUse(%d) Idle (%d) Released(%d)", m.HeapInuse, m.HeapIdle, m.HeapReleased) 166 | this.generateMultiplexInfo() 167 | time.Sleep(100 * time.Millisecond) 168 | } 169 | } 170 | 171 | //Generates the Info response for a multiplexed server 172 | func (this *RedisMultiplexer) generateMultiplexInfo() { 173 | tmpSlice := fmt.Sprintf("rmux_version: %s\r\ngo_version: %s\r\nprocess_id: %d\r\nconnected_clients: %d\r\nactive_endpoints: %d\r\ntotal_endpoints: %d\r\nrole: master\r\n", version, runtime.Version(), os.Getpid(), this.connectionCount, this.activeConnectionCount, len(this.ConnectionCluster)) 174 | this.infoMutex.Lock() 175 | this.infoResponse = []byte(fmt.Sprintf("$%d\r\n%s", len(tmpSlice), tmpSlice)) 176 | this.infoMutex.Unlock() 177 | } 178 | 179 | //Called when a rmux server is ready to begin accepting connections 180 | func (this *RedisMultiplexer) Start() (err error) { 181 | this.HashRing, err = connection.NewHashRing(this.ConnectionCluster, this.Failover) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | go this.maintainConnectionStates() 187 | go this.initializeCleanup() 188 | //if graphite.Enabled() { 189 | // go this.GraphiteCheckin() 190 | //} 191 | 192 | for this.active { 193 | fd, err := this.Listener.Accept() 194 | if err != nil { 195 | // Debug("Start: Error received from listener.Accept: %s", err.Error()) 196 | continue 197 | } 198 | // Debug("Accepted connection.") 199 | graphite.Increment("accepted") 200 | 201 | go this.initializeClient(fd) 202 | } 203 | time.Sleep(100 * time.Millisecond) 204 | return 205 | } 206 | 207 | //Initializes a client's connection to our server. Sets up our disconnect hooks and then passes the client off for request handling 208 | func (this *RedisMultiplexer) initializeClient(localConnection net.Conn) { 209 | defer func() { 210 | atomic.AddInt32(&this.connectionCount, -1) 211 | }() 212 | atomic.AddInt32(&this.connectionCount, 1) 213 | //Add the connection to our internal list 214 | myClient := NewClient(localConnection, this.ClientReadTimeout, this.ClientWriteTimeout, 215 | this.multiplexing, this.HashRing) 216 | 217 | defer func() { 218 | if r := recover(); r != nil { 219 | // DebugPanic(r) 220 | if val, ok := r.(string); ok { 221 | // If we paniced, push that to the client before closing the connection 222 | protocol.WriteError([]byte(val), myClient.Writer, true) 223 | } 224 | } 225 | 226 | // Debug("Closing client connection.") 227 | myClient.Connection.Close() 228 | }() 229 | 230 | this.HandleClientRequests(myClient) 231 | } 232 | 233 | //Sends the pre-generated Info response for a multiplexed server 234 | func (this *RedisMultiplexer) sendMultiplexInfo(myClient *Client) (err error) { 235 | this.infoMutex.RLock() 236 | err = protocol.WriteLine(this.infoResponse, myClient.Writer, true) 237 | this.infoMutex.RUnlock() 238 | return 239 | } 240 | 241 | func (this *RedisMultiplexer) GraphiteCheckin() { 242 | for this.active { 243 | time.Sleep(time.Millisecond * 100) 244 | for _, pool := range this.ConnectionCluster { 245 | pool.ReportGraphite() 246 | } 247 | } 248 | } 249 | 250 | //Handles requests for a client. 251 | //Inspects all incoming commands, to find if they are key-driven or not. 252 | //If they are, finds the appropriate connection pool, and passes the request off to it. 253 | func (this *RedisMultiplexer) HandleClientRequests(client *Client) { 254 | // Create background i/o thread 255 | go client.ReadLoop(this) 256 | 257 | defer func() { 258 | // Debug("Client command handling loop closing") 259 | // If the multiplexer goes down, deactivate this client. 260 | client.Active = false 261 | }() 262 | 263 | for this.active && client.Active { 264 | select { 265 | case item := <-client.ReadChannel: 266 | if item.command != nil { 267 | this.HandleCommandChunk(client, item.command) 268 | } 269 | if item.err != nil { 270 | this.HandleError(client, item.err) 271 | } 272 | case <-time.After(time.Second * 1): 273 | // Allow heartbeat checks to happen once a second 274 | } 275 | } 276 | 277 | // TODO defer closing stuff? 278 | } 279 | 280 | // This looks a lot like HandleClientRequests above, but will break and flush to redis if there is nothing to read. 281 | // Will allow it to handle a pipeline of commands without spinning indefinitely. 282 | func (this *RedisMultiplexer) HandleCommandChunk(client *Client, command protocol.Command) { 283 | this.HandleCommand(client, command) 284 | 285 | ChunkLoop: 286 | for this.active && client.Active { 287 | select { 288 | case item := <-client.ReadChannel: 289 | if item.command != nil { 290 | this.HandleCommand(client, item.command) 291 | } 292 | if item.err != nil { 293 | this.HandleError(client, item.err) 294 | } 295 | default: 296 | break ChunkLoop 297 | } 298 | } 299 | 300 | client.FlushRedisAndRespond() 301 | } 302 | 303 | func (this *RedisMultiplexer) HandleCommand(client *Client, command protocol.Command) { 304 | if this.multiplexing && bytes.Equal(command.GetCommand(), protocol.INFO_COMMAND) { 305 | this.sendMultiplexInfo(client) 306 | return 307 | } 308 | 309 | // Debug("Writing out %q", command) 310 | immediateResponse, err := client.ParseCommand(command) 311 | 312 | if immediateResponse != nil { 313 | // Respond with anything we have queued 314 | if client.HasQueued() { 315 | client.FlushRedisAndRespond() 316 | } 317 | 318 | err = client.WriteLine(immediateResponse) 319 | if err != nil { 320 | // Debug("Error received when writing an immediate response: %s", err) 321 | } 322 | 323 | return 324 | } else if err != nil { 325 | if err == ERR_QUIT { 326 | client.WriteLine(protocol.OK_RESPONSE) 327 | client.ReadChannel <- readItem{nil, err} 328 | return 329 | } else if recErr, ok := err.(*protocol.RecoverableError); ok { 330 | client.WriteError(recErr, false) 331 | } else { 332 | panic("Not sure how to handle this error: " + err.Error()) 333 | } 334 | 335 | return 336 | } 337 | 338 | // Otherwise, the command is ready to buffer to the connection. 339 | client.Queue(command) 340 | 341 | // If we're multiplexing, just handle one command at a time 342 | if this.multiplexing && client.HasQueued() { 343 | client.FlushRedisAndRespond() 344 | } 345 | } 346 | 347 | func (this *RedisMultiplexer) HandleError(client *Client, err error) { 348 | if err == nil { 349 | return 350 | } 351 | 352 | if err == ERR_QUIT { 353 | client.Active = false 354 | return 355 | } else if recErr, ok := err.(*protocol.RecoverableError); ok { 356 | // Since we can recover, flush an error to the client 357 | Error("Error from server: %s", recErr) 358 | client.FlushError(recErr) 359 | return 360 | } else if err == io.EOF { 361 | // Stream EOF-ed. Deactivate this client and break out. 362 | client.FlushRedisAndRespond() 363 | client.Active = false 364 | return 365 | } else if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 366 | // We had a read timeout. Let the client know that the connection is down 367 | graphite.Increment("nettimeout") 368 | client.FlushError(ERR_TIMEOUT) 369 | return 370 | } else { 371 | // This is something we've never seen before! Panic panic panic 372 | panic("New Client Read Error: " + err.Error()) 373 | } 374 | } 375 | 376 | func (rm *RedisMultiplexer) SetAllTimeouts(t time.Duration) { 377 | rm.EndpointConnectTimeout = t 378 | rm.EndpointReadTimeout = t 379 | rm.EndpointWriteTimeout = t 380 | rm.ClientReadTimeout = t 381 | rm.ClientWriteTimeout = t 382 | } 383 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package rmux 27 | 28 | import ( 29 | "bufio" 30 | "net" 31 | "testing" 32 | "time" 33 | ) 34 | 35 | func StartPongResponseServer(t *testing.T, sock string) net.Listener { 36 | listenSock, err := net.Listen("unix", sock) 37 | if err != nil { 38 | t.Errorf("Cannot listen on %s: %s", sock, err) 39 | return nil 40 | } 41 | 42 | go func() { 43 | for { 44 | c, err := listenSock.Accept() 45 | if err != nil { 46 | break 47 | } 48 | rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)) 49 | rw.ReadLine() 50 | rw.Write([]byte("+PONG\r\n")) 51 | rw.Flush() 52 | } 53 | }() 54 | 55 | return listenSock 56 | } 57 | 58 | func StartNoResponseServer(t *testing.T, sock string) net.Listener { 59 | listenSock, err := net.Listen("unix", sock) 60 | if err != nil { 61 | t.Errorf("Cannot listen on %s: %s", sock, err) 62 | return nil 63 | } 64 | 65 | go func() { 66 | for { 67 | _, err := listenSock.Accept() 68 | if err != nil { 69 | break 70 | } 71 | // do nothing 72 | } 73 | }() 74 | 75 | return listenSock 76 | } 77 | 78 | func TestCountActiveConnections_NoResponse(t *testing.T) { 79 | server, err := NewRedisMultiplexer("unix", "/tmp/rmuxTest.sock", 5) 80 | if err != nil { 81 | t.Fatal("Cannot listen on /tmp/rmuxTest.sock: ", err) 82 | } 83 | defer func() { 84 | server.active = false 85 | server.Listener.Close() 86 | }() 87 | 88 | server.EndpointConnectTimeout = 10 * time.Millisecond 89 | server.EndpointReadTimeout = 10 * time.Millisecond 90 | server.EndpointWriteTimeout = 10 * time.Millisecond 91 | 92 | server.AddConnection("unix", "/tmp/rmuxTest1.sock") 93 | server.AddConnection("unix", "/tmp/rmuxTest2.sock") 94 | server.AddConnection("unix", "/tmp/rmuxTest3.sock") 95 | 96 | //create a non-responder on one socket 97 | sock1 := StartNoResponseServer(t, "/tmp/rmuxTest1.sock") 98 | if sock1 == nil { 99 | t.Error("Cannot listen on /tmp/rmuxTest1.sock: ", err) 100 | return 101 | } 102 | defer sock1.Close() 103 | 104 | connectionCount := server.countActiveConnections() 105 | 106 | if connectionCount != 0 { 107 | t.Error("Server thinks there are active connections, when there should be none") 108 | return 109 | } 110 | } 111 | 112 | func TestCountActiveConnections_SomeResponses(t *testing.T) { 113 | server, err := NewRedisMultiplexer("unix", "/tmp/rmuxTest.sock", 5) 114 | if err != nil { 115 | t.Fatal("Cannot listen on /tmp/rmuxTest.sock: ", err) 116 | } 117 | defer func() { 118 | server.active = false 119 | server.Listener.Close() 120 | }() 121 | 122 | server.EndpointConnectTimeout = 10 * time.Millisecond 123 | server.EndpointReadTimeout = 10 * time.Millisecond 124 | server.EndpointWriteTimeout = 10 * time.Millisecond 125 | 126 | server.AddConnection("unix", "/tmp/rmuxTest1.sock") 127 | server.AddConnection("unix", "/tmp/rmuxTest2.sock") 128 | server.AddConnection("unix", "/tmp/rmuxTest3.sock") 129 | 130 | //create a non-responder on one socket 131 | sock1 := StartNoResponseServer(t, "/tmp/rmuxTest1.sock") 132 | if sock1 == nil { 133 | return 134 | } 135 | defer sock1.Close() 136 | 137 | // create a pong-responder on another socket 138 | sock2 := StartPongResponseServer(t, "/tmp/rmuxTest2.sock") 139 | if sock2 == nil { 140 | return 141 | } 142 | defer sock2.Close() 143 | 144 | // no listener on socket 3 145 | 146 | connectionCount := server.countActiveConnections() 147 | if connectionCount != 1 { 148 | t.Errorf("Server's connection count is wrong: %d instead of 1", connectionCount) 149 | } 150 | } 151 | 152 | func TestCountActiveConnections_AllResponses(t *testing.T) { 153 | server, err := NewRedisMultiplexer("unix", "/tmp/rmuxTest.sock", 5) 154 | if err != nil { 155 | t.Fatal("Cannot listen on /tmp/rmuxTest.sock: ", err) 156 | } 157 | defer func() { 158 | server.active = false 159 | server.Listener.Close() 160 | }() 161 | 162 | server.EndpointConnectTimeout = 10 * time.Millisecond 163 | server.EndpointReadTimeout = 10 * time.Millisecond 164 | server.EndpointWriteTimeout = 10 * time.Millisecond 165 | 166 | server.AddConnection("unix", "/tmp/rmuxTest1.sock") 167 | server.AddConnection("unix", "/tmp/rmuxTest2.sock") 168 | server.AddConnection("unix", "/tmp/rmuxTest3.sock") 169 | 170 | // create pong responders 171 | sock1 := StartPongResponseServer(t, "/tmp/rmuxTest1.sock") 172 | if sock1 == nil { 173 | return 174 | } 175 | defer sock1.Close() 176 | sock2 := StartPongResponseServer(t, "/tmp/rmuxTest2.sock") 177 | if sock2 == nil { 178 | return 179 | } 180 | defer sock2.Close() 181 | sock3 := StartPongResponseServer(t, "/tmp/rmuxTest3.sock") 182 | if sock3 == nil { 183 | return 184 | } 185 | defer sock3.Close() 186 | 187 | connectionCount := server.countActiveConnections() 188 | if connectionCount != 3 { 189 | t.Errorf("Server's connection count is wrong: %d instead of 1", connectionCount) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /writer/writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package writer 27 | 28 | import ( 29 | "bytes" 30 | "io" 31 | ) 32 | 33 | const ( 34 | defaultFlexibleWriterSize = 64 35 | ) 36 | 37 | type FlexibleWriter struct { 38 | *bytes.Buffer 39 | writer io.Writer 40 | } 41 | 42 | func NewFlexibleWriter(writer io.Writer) *FlexibleWriter { 43 | w := &FlexibleWriter{} 44 | w.writer = writer 45 | buf := make([]byte, 0, defaultFlexibleWriterSize) 46 | w.Buffer = bytes.NewBuffer(buf) 47 | return w 48 | } 49 | 50 | func (this *FlexibleWriter) Flush() (err error) { 51 | _, err = this.Buffer.WriteTo(this.writer) 52 | 53 | return 54 | } 55 | 56 | func (this *FlexibleWriter) Buffered() int { 57 | return this.Len() 58 | } 59 | -------------------------------------------------------------------------------- /writer/writer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Salesforce.com, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 6 | * following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following 9 | * disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials provided with the distribution. 13 | * 14 | * * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 18 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | package writer 27 | 28 | import ( 29 | "bytes" 30 | "strings" 31 | "testing" 32 | ) 33 | 34 | func TestFlexibleWriter_WriteFlush(t *testing.T) { 35 | b := new(bytes.Buffer) 36 | fw := NewFlexibleWriter(b) 37 | 38 | toWrite := []byte("First write") 39 | 40 | if n, err := fw.Write(toWrite); err != nil { 41 | t.Errorf("fw.Write errored: %s", err) 42 | } else if n != len(toWrite) { 43 | t.Errorf("fw.Write did not write expected number of bytes. got:%d expected:%d", n, len(toWrite)) 44 | } else if b.Len() != 0 { 45 | t.Error("Should have not flushed on a write.") 46 | } 47 | 48 | fw.Flush() 49 | if !bytes.Equal(toWrite, b.Bytes()) { 50 | t.Error("Should have flushed after call to Flush()") 51 | } 52 | } 53 | 54 | func TestFlexibleWriter_HugeWrite(t *testing.T) { 55 | b := new(bytes.Buffer) 56 | fw := NewFlexibleWriter(b) 57 | 58 | // 10 * 64KB of data 59 | hugeWrite := []byte(strings.Repeat("0123456789", 64*1024)) 60 | 61 | // Sanity check 62 | if len(hugeWrite) != 64*1024*10 { 63 | t.Errorf("Sanity check failed. Byte slice size does not match. got:%d expected:%d", 64*1024*10, len(hugeWrite)) 64 | } 65 | 66 | if n, err := fw.Write(hugeWrite); err != nil { 67 | t.Errorf("fw.Write errored: %s", err) 68 | } else if n != len(hugeWrite) { 69 | t.Errorf("fw.Write did not write expected number of bytes. got:%d expected:%d", n, len(hugeWrite)) 70 | } else if b.Len() != 0 { 71 | t.Error("Should have not flushed on a write.") 72 | } 73 | 74 | fw.Flush() 75 | if !bytes.Equal(hugeWrite, b.Bytes()) { 76 | t.Error("Should have flushed after call to Flush()") 77 | } 78 | } 79 | --------------------------------------------------------------------------------